Skip to content

Commit

Permalink
Merge pull request #63 from openlayers/extra-ts-syntax-support
Browse files Browse the repository at this point in the history
Extra TS syntax support
  • Loading branch information
ahocevar authored Sep 14, 2024
2 parents c0a0dcb + f060f13 commit 55e8783
Show file tree
Hide file tree
Showing 5 changed files with 544 additions and 63 deletions.
86 changes: 66 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,85 +28,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}`}
*/
```

**@override annotations**
To:

are removed because they make JSDoc stop inheritance
```js
/**
* @type {'static:${dynamic}'}
*/
```

### @override annotations

### JSDoc
are removed because they make JSDoc stop inheritance

**Named export:**
### Interface style semi-colon separators

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

**Default export assigned to a variable in the exporting module:**
To:

```js
/**
* @type {module:path/to/module~variableOfDefaultExport}
* @type {{a: number, b: string}}
*/
```

This syntax is also used when referring to types of `@typedef`s and `@enum`s.
Also removes trailing commas from object types.

**Anonymous default export:**
### TS inline function syntax

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

**typeof type:**
To:

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

**Template literal type**
### Bracket notation

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

To:

```js
/**
* @type {obj.key}
*/
```

Expand Down
198 changes: 156 additions & 42 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -168,52 +168,166 @@ function ensureJsExt(filePath) {
return filePath.replace(extensionEnsureRegEx, '.js');
}

/**
* Replaces text by indices where each element of `replacements` is `[startIndex, endIndex, replacement]`.
*
* Note: This function does not handle nested replacements.
*
* @param {string} text The text to replace
* @param {Array<[number, number, string]>} replacements The replacements to apply
* @return {string} The text with replacements applied
*/
function replaceByIndices(text, replacements) {
let offset = 0;
let replacedText = text;

replacements.forEach(([startIndex, endIndex, replacement], i) => {
const head = replacedText.slice(0, startIndex + offset);
const tail = replacedText.slice(endIndex + offset);

replacedText = head + replacement + tail;

offset += replacement.length - (endIndex - startIndex);
});

return replacedText;
}

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;

/**
* @param {string} tagText The tag text
* @return {string} The modified tag text
*/
tag.onTagText = function (tagText) {
if (oldOnTagText) {
tagText = oldOnTagText.apply(this, arguments);
}

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

const len = tagText.length;

/** @type {Array<[number, number, string]>} */
let replacements = [];
let openCurly = 0;
let openRound = 0;
let isWithinString = false;
let quoteChar = '';
let i = startIndex;
let functionStartIndex;

while (i < len) {
switch (tagText[i]) {
case '\\':
// Skip escaped character
++i;
break;
case '"':
case "'":
if (isWithinString && quoteChar === tagText[i]) {
isWithinString = false;
quoteChar = '';
} else if (!isWithinString) {
isWithinString = true;
quoteChar = tagText[i];
}

break;
case ';':
// Replace interface-style semi-colon separators with commas
if (!isWithinString && openCurly > 1) {
const isTrailingSemiColon = /^\s*}/.test(tagText.slice(i + 1));

replacements.push([i, i + 1, isTrailingSemiColon ? '' : ',']);
}

break;
case '(':
if (openRound === 0) {
functionStartIndex = i;
}

++openRound;

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

// Replace TS inline function syntax with JSDoc
if (returnMatch) {
const functionEndIndex = i + returnMatch[0].length + 1;
const hasFunctionKeyword = /\bfunction\s*$/.test(
tagText.slice(0, functionStartIndex),
);

// Filter out any replacements that are within the function
replacements = replacements.filter(([startIndex]) => {
return startIndex < functionStartIndex || startIndex > i;
});

replacements.push([
functionStartIndex,
functionEndIndex,
hasFunctionKeyword ? '():' : 'function():',
]);
}
break;
default:
break;
}
++i;

functionStartIndex = null;
}

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

const replaced = replaceByIndices(
tagText.slice(startIndex, i + 1),
replacements,
)
// Replace `templateliteral` with 'templateliteral'
.replace(/`([^`]*)`/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

0 comments on commit 55e8783

Please sign in to comment.