Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extra TS syntax support #50

Closed
92 changes: 74 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,75 +37,131 @@ In addition to types that are used in the same file that they are defined in, im

TypeScript and JSDoc use a different syntax for imported types. This plugin converts the TypeScript types so JSDoc can handle them:

### TypeScript
### Named export

**Named export:**
```js
/**
* @type {import("./path/to/module").exportName}
*/
```

**Default export:**
To:

```js
/**
* @type {module:path/to/module.exportName}
*/
```

### Default export

```js
/**
* @type {import("./path/to/module").default}
*/
```

**typeof type:**
To:

```js
/**
* @type {module:path/to/module}
*/
```

When assigned to a variable in the exporting module:

```js
/**
* @type {module:path/to/module~variableOfDefaultExport}
*/
```

This syntax is also used when referring to types of `@typedef`s and `@enum`s.

### `typeof type`

```js
/**
* @type {typeof import("./path/to/module").exportName}
*/
```

**Template literal type**
To:

```js
/**
* @type {Class<module:path/to/module.exportName>}
*/
```

### Template literal type

```js
/**
* @type {`static:${dynamic}`}
*/
```

To:

```js
/**
* @type {'static:${dynamic}'}
*/
```

**@override annotations**
### @override annotations

are removed because they make JSDoc stop inheritance

### JSDoc
### Interface style semi-colon separators

**Named export:**
```js
/**
* @type {module:path/to/module.exportName}
* @type {{a: number; b: string;}}
*/
```

To:

```js
/**
* @type {{a: number, b: string}}
*/
```

**Default export assigned to a variable in the exporting module:**
Also removes trailing commas from object types.

### TS inline function syntax

```js
/**
* @type {module:path/to/module~variableOfDefaultExport}
* @type {(a: number, b: string) => void}
*/
```

This syntax is also used when referring to types of `@typedef`s and `@enum`s.
To:

**Anonymous default export:**
```js
/**
* @type {module:path/to/module}
* @type {function(): void}
*/
```

**typeof type:**
### Bracket notation

```js
/**
* @type {Class<module:path/to/module.exportName>}
* @type {obj['key']}
*/
```

**Template literal type**
To:

```js
/**
* @type {'static:${dynamic}'}
* @type {obj.key}
*/
```

Expand Down
150 changes: 107 additions & 43 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,51 +92,115 @@ function getDelimiter(moduleId, symbol, parser) {
}

exports.defineTags = function (dictionary) {
['type', 'typedef', 'property', 'return', 'param', 'template'].forEach(
function (tagName) {
const tag = dictionary.lookUp(tagName);
const oldOnTagText = tag.onTagText;
tag.onTagText = function (tagText) {
if (oldOnTagText) {
tagText = oldOnTagText.apply(this, arguments);
}
// Replace `templateliteral` with 'templateliteral'
const startIndex = tagText.search('{');
if (startIndex === -1) {
return tagText;
}
const len = tagText.length;
let open = 0;
let i = startIndex;
while (i < len) {
switch (tagText[i]) {
case '\\':
// Skip escaped character
++i;
break;
case '{':
++open;
break;
case '}':
if (!--open) {
return (
tagText.slice(0, startIndex) +
tagText
.slice(startIndex, i + 1)
.replace(/`([^`]*)`/g, "'$1'") +
tagText.slice(i + 1)
);
const tags = [
'type',
'typedef',
'property',
'return',
'param',
'template',
'default',
'member',
];

tags.forEach(function (tagName) {
const tag = dictionary.lookUp(tagName);
const oldOnTagText = tag.onTagText;

tag.onTagText = function (tagText) {
if (oldOnTagText) {
tagText = oldOnTagText.apply(this, arguments);
}

const startIndex = tagText.search('{');
if (startIndex === -1) {
return tagText;
}

const len = tagText.length;
const functionIndices = [];

let openCurly = 0;
let openRound = 0;
let i = startIndex;
let functionStartEnd = [];

while (i < len) {
switch (tagText[i]) {
case '\\':
// Skip escaped character
++i;
break;
case '(':
if (openRound === 0) {
functionStartEnd.push(i);
}

++openRound;

break;
case ')':
if (!--openRound) {
// If round brackets form a function
const returnMatch = tagText.slice(i + 1).match(/^\s*(:|=>)/);

if (returnMatch) {
functionStartEnd.push(i + returnMatch[0].length + 1);
functionIndices.push(functionStartEnd);
}
break;
default:
break;
}
++i;

functionStartEnd = [];
}

break;
case '{':
++openCurly;
break;
case '}':
if (!--openCurly) {
const head = tagText.slice(0, startIndex);
const tail = tagText.slice(i + 1);

let replaced = tagText.slice(startIndex, i + 1);

// Replace TS inline function syntax with JSDoc
functionIndices.reverse().forEach(([start, end]) => {
if (tagText.slice(0, start).trim().endsWith('function')) {
// Already JSDoc syntax
return;
}

replaced =
replaced.slice(0, start - startIndex) +
'function():' +
replaced.slice(end - startIndex);
});

replaced = replaced
// Replace `templateliteral` with 'templateliteral'
.replace(/`([^`]*)`/g, "'$1'")
// Interface style semi-colon separators to commas
.replace(/;/g, ',')
// Remove trailing commas in object types
.replace(/,(\s*\})/g, '$1')
// Bracket notation to dot notation
.replace(
/(\w+|>|\)|\])\[(?:'([^']+)'|"([^"]+)")\]/g,
'$1.$2$3',
);

return head + replaced + tail;
}

break;
default:
break;
}
throw new Error("Missing closing '}'");
};
},
);
++i;
}
throw new Error("Missing closing '}'");
};
});
};

exports.astNodeVisitor = {
Expand Down
Loading