Skip to content

Commit 55e8783

Browse files
authored
Merge pull request #63 from openlayers/extra-ts-syntax-support
Extra TS syntax support
2 parents c0a0dcb + f060f13 commit 55e8783

File tree

5 files changed

+544
-63
lines changed

5 files changed

+544
-63
lines changed

README.md

Lines changed: 66 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -28,85 +28,131 @@ In addition to types that are used in the same file that they are defined in, im
2828

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

31-
### TypeScript
32-
33-
**Named export:**
31+
### Named export
3432

3533
```js
3634
/**
3735
* @type {import("./path/to/module").exportName}
3836
*/
3937
```
4038

41-
**Default export:**
39+
To:
40+
41+
```js
42+
/**
43+
* @type {module:path/to/module.exportName}
44+
*/
45+
```
46+
47+
### Default export
4248

4349
```js
4450
/**
4551
* @type {import("./path/to/module").default}
4652
*/
4753
```
4854

49-
**typeof type:**
55+
To:
56+
57+
```js
58+
/**
59+
* @type {module:path/to/module}
60+
*/
61+
```
62+
63+
When assigned to a variable in the exporting module:
64+
65+
```js
66+
/**
67+
* @type {module:path/to/module~variableOfDefaultExport}
68+
*/
69+
```
70+
71+
This syntax is also used when referring to types of `@typedef`s and `@enum`s.
72+
73+
### `typeof type`
5074

5175
```js
5276
/**
5377
* @type {typeof import("./path/to/module").exportName}
5478
*/
5579
```
5680

57-
**Template literal type**
81+
To:
82+
83+
```js
84+
/**
85+
* @type {Class<module:path/to/module.exportName>}
86+
*/
87+
```
88+
89+
### Template literal type
5890

5991
```js
6092
/**
6193
* @type {`static:${dynamic}`}
6294
*/
6395
```
6496

65-
**@override annotations**
97+
To:
6698

67-
are removed because they make JSDoc stop inheritance
99+
```js
100+
/**
101+
* @type {'static:${dynamic}'}
102+
*/
103+
```
104+
105+
### @override annotations
68106

69-
### JSDoc
107+
are removed because they make JSDoc stop inheritance
70108

71-
**Named export:**
109+
### Interface style semi-colon separators
72110

73111
```js
74112
/**
75-
* @type {module:path/to/module.exportName}
113+
* @type {{a: number; b: string;}}
76114
*/
77115
```
78116

79-
**Default export assigned to a variable in the exporting module:**
117+
To:
80118

81119
```js
82120
/**
83-
* @type {module:path/to/module~variableOfDefaultExport}
121+
* @type {{a: number, b: string}}
84122
*/
85123
```
86124

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

89-
**Anonymous default export:**
127+
### TS inline function syntax
90128

91129
```js
92130
/**
93-
* @type {module:path/to/module}
131+
* @type {(a: number, b: string) => void}
94132
*/
95133
```
96134

97-
**typeof type:**
135+
To:
98136

99137
```js
100138
/**
101-
* @type {Class<module:path/to/module.exportName>}
139+
* @type {function(): void}
102140
*/
103141
```
104142

105-
**Template literal type**
143+
### Bracket notation
106144

107145
```js
108146
/**
109-
* @type {'static:${dynamic}'}
147+
* @type {obj['key']}
148+
*/
149+
```
150+
151+
To:
152+
153+
```js
154+
/**
155+
* @type {obj.key}
110156
*/
111157
```
112158

index.js

Lines changed: 156 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -168,52 +168,166 @@ function ensureJsExt(filePath) {
168168
return filePath.replace(extensionEnsureRegEx, '.js');
169169
}
170170

171+
/**
172+
* Replaces text by indices where each element of `replacements` is `[startIndex, endIndex, replacement]`.
173+
*
174+
* Note: This function does not handle nested replacements.
175+
*
176+
* @param {string} text The text to replace
177+
* @param {Array<[number, number, string]>} replacements The replacements to apply
178+
* @return {string} The text with replacements applied
179+
*/
180+
function replaceByIndices(text, replacements) {
181+
let offset = 0;
182+
let replacedText = text;
183+
184+
replacements.forEach(([startIndex, endIndex, replacement], i) => {
185+
const head = replacedText.slice(0, startIndex + offset);
186+
const tail = replacedText.slice(endIndex + offset);
187+
188+
replacedText = head + replacement + tail;
189+
190+
offset += replacement.length - (endIndex - startIndex);
191+
});
192+
193+
return replacedText;
194+
}
195+
171196
exports.defineTags = function (dictionary) {
172-
['type', 'typedef', 'property', 'return', 'param', 'template'].forEach(
173-
function (tagName) {
174-
const tag = dictionary.lookUp(tagName);
175-
const oldOnTagText = tag.onTagText;
176-
tag.onTagText = function (tagText) {
177-
if (oldOnTagText) {
178-
tagText = oldOnTagText.apply(this, arguments);
179-
}
180-
// Replace `templateliteral` with 'templateliteral'
181-
const startIndex = tagText.search('{');
182-
if (startIndex === -1) {
183-
return tagText;
184-
}
185-
const len = tagText.length;
186-
let open = 0;
187-
let i = startIndex;
188-
while (i < len) {
189-
switch (tagText[i]) {
190-
case '\\':
191-
// Skip escaped character
192-
++i;
193-
break;
194-
case '{':
195-
++open;
196-
break;
197-
case '}':
198-
if (!--open) {
199-
return (
200-
tagText.slice(0, startIndex) +
201-
tagText
202-
.slice(startIndex, i + 1)
203-
.replace(/`([^`]*)`/g, "'$1'") +
204-
tagText.slice(i + 1)
197+
const tags = [
198+
'type',
199+
'typedef',
200+
'property',
201+
'return',
202+
'param',
203+
'template',
204+
'default',
205+
'member',
206+
];
207+
208+
tags.forEach(function (tagName) {
209+
const tag = dictionary.lookUp(tagName);
210+
const oldOnTagText = tag.onTagText;
211+
212+
/**
213+
* @param {string} tagText The tag text
214+
* @return {string} The modified tag text
215+
*/
216+
tag.onTagText = function (tagText) {
217+
if (oldOnTagText) {
218+
tagText = oldOnTagText.apply(this, arguments);
219+
}
220+
221+
const startIndex = tagText.search('{');
222+
if (startIndex === -1) {
223+
return tagText;
224+
}
225+
226+
const len = tagText.length;
227+
228+
/** @type {Array<[number, number, string]>} */
229+
let replacements = [];
230+
let openCurly = 0;
231+
let openRound = 0;
232+
let isWithinString = false;
233+
let quoteChar = '';
234+
let i = startIndex;
235+
let functionStartIndex;
236+
237+
while (i < len) {
238+
switch (tagText[i]) {
239+
case '\\':
240+
// Skip escaped character
241+
++i;
242+
break;
243+
case '"':
244+
case "'":
245+
if (isWithinString && quoteChar === tagText[i]) {
246+
isWithinString = false;
247+
quoteChar = '';
248+
} else if (!isWithinString) {
249+
isWithinString = true;
250+
quoteChar = tagText[i];
251+
}
252+
253+
break;
254+
case ';':
255+
// Replace interface-style semi-colon separators with commas
256+
if (!isWithinString && openCurly > 1) {
257+
const isTrailingSemiColon = /^\s*}/.test(tagText.slice(i + 1));
258+
259+
replacements.push([i, i + 1, isTrailingSemiColon ? '' : ',']);
260+
}
261+
262+
break;
263+
case '(':
264+
if (openRound === 0) {
265+
functionStartIndex = i;
266+
}
267+
268+
++openRound;
269+
270+
break;
271+
case ')':
272+
if (!--openRound) {
273+
// If round brackets form a function
274+
const returnMatch = tagText.slice(i + 1).match(/^\s*(:|=>)/);
275+
276+
// Replace TS inline function syntax with JSDoc
277+
if (returnMatch) {
278+
const functionEndIndex = i + returnMatch[0].length + 1;
279+
const hasFunctionKeyword = /\bfunction\s*$/.test(
280+
tagText.slice(0, functionStartIndex),
205281
);
282+
283+
// Filter out any replacements that are within the function
284+
replacements = replacements.filter(([startIndex]) => {
285+
return startIndex < functionStartIndex || startIndex > i;
286+
});
287+
288+
replacements.push([
289+
functionStartIndex,
290+
functionEndIndex,
291+
hasFunctionKeyword ? '():' : 'function():',
292+
]);
206293
}
207-
break;
208-
default:
209-
break;
210-
}
211-
++i;
294+
295+
functionStartIndex = null;
296+
}
297+
298+
break;
299+
case '{':
300+
++openCurly;
301+
break;
302+
case '}':
303+
if (!--openCurly) {
304+
const head = tagText.slice(0, startIndex);
305+
const tail = tagText.slice(i + 1);
306+
307+
const replaced = replaceByIndices(
308+
tagText.slice(startIndex, i + 1),
309+
replacements,
310+
)
311+
// Replace `templateliteral` with 'templateliteral'
312+
.replace(/`([^`]*)`/g, "'$1'")
313+
// Bracket notation to dot notation
314+
.replace(
315+
/(\w+|>|\)|\])\[(?:'([^']+)'|"([^"]+)")\]/g,
316+
'$1.$2$3',
317+
);
318+
319+
return head + replaced + tail;
320+
}
321+
322+
break;
323+
default:
324+
break;
212325
}
213-
throw new Error("Missing closing '}'");
214-
};
215-
},
216-
);
326+
++i;
327+
}
328+
throw new Error("Missing closing '}'");
329+
};
330+
});
217331
};
218332

219333
exports.astNodeVisitor = {

0 commit comments

Comments
 (0)