Skip to content

Commit 7000b9b

Browse files
committed
Use @extends on the correct comment and handle typeof types
1 parent 8afc3d4 commit 7000b9b

3 files changed

Lines changed: 77 additions & 29 deletions

File tree

README.md

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,11 @@ In the above snippet, `"src"` is the directory that contains the source files. I
2929

3030
## What this plugin does
3131

32-
Types defined in a project are converted to a JSDoc module paths, so they can be documented and linked properly. In addition to types that are used in the same file that they are defined in, imported types are also supported.
32+
When using the `class` keyword for defining classes (required by TypeScript), JSDoc requires `@classdesc` and `@extends` annotations. With this plugin, no `@classdesc` and `@extends` annotations are needed.
33+
34+
Types defined in a project are converted to JSDoc module paths, so they can be documented and linked properly.
35+
36+
In addition to types that are used in the same file that they are defined in, imported types are also supported.
3337

3438
TypeScript and JSDoc use a different syntax for imported types:
3539

@@ -49,6 +53,13 @@ TypeScript and JSDoc use a different syntax for imported types:
4953
*/
5054
```
5155

56+
**typeof type:**
57+
```js
58+
/**
59+
* @type {typeof import("./path/to/module").exportName}
60+
*/
61+
```
62+
5263
### JSDoc
5364

5465
**Named export:**
@@ -65,11 +76,18 @@ TypeScript and JSDoc use a different syntax for imported types:
6576
*/
6677
```
6778

68-
This syntax is also used when refering to types of `@typedef`s and `@enum`s.
79+
This syntax is also used when referring to types of `@typedef`s and `@enum`s.
6980

7081
**Anonymous default export:**
7182
```js
7283
/**
7384
* @type {module:path/to/module}
7485
*/
7586
```
87+
88+
**typeof type:**
89+
```js
90+
/**
91+
* @type {Class<module:path/to/module.exportName>}
92+
*/
93+
```

index.js

Lines changed: 56 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ if (!fs.existsSync(moduleRootAbsolute)) {
1515
throw new Error('Directory "' + moduleRootAbsolute + '" does not exist. Check the "typescript.moduleRoot" config option for jsdoc-plugin-typescript');
1616
}
1717

18-
const importRegEx = /(typeof )?import\("([^"]*)"\)\.([^ \.\|\}><,\)=\n]*)([ \.\|\}><,\)=\n])/g;
18+
const importRegEx = /import\("([^"]*)"\)\.([^ \.\|\}><,\)=\n]*)([ \.\|\}><,\)=\n])/g;
1919
const typedefRegEx = /@typedef \{[^\}]*\} ([^ \r?\n?]*)/;
20+
const noClassdescRegEx = /@(typedef|module|type)/;
2021

2122
const moduleInfos = {};
2223
const fileNodes = {};
@@ -98,21 +99,50 @@ exports.astNodeVisitor = {
9899
};
99100
}
100101

101-
// Add class inheritance information because JSDoc does not honor
102-
// the ES6 class's `extends` keyword
103-
if (node.superClass && node.leadingComments) {
104-
const leadingComment = node.leadingComments[node.leadingComments.length - 1];
105-
const lines = leadingComment.value.split(/\r?\n/);
106-
lines.push(lines[lines.length - 1]);
107-
const identifier = identifiers[node.superClass.name];
108-
if (identifier) {
109-
const absolutePath = path.resolve(path.dirname(currentSourceName), identifier.value);
110-
const moduleId = path.relative(path.join(process.cwd(), moduleRoot), absolutePath).replace(/\.js$/, '');
111-
const exportName = identifier.defaultImport ? getDefaultExportName(moduleId, parser) : node.superClass.name;
112-
const delimiter = identifier.defaultImport ? '~' : getDelimiter(moduleId, exportName, parser);
113-
lines[lines.length - 2] = ' * @extends ' + `module:${moduleId}${exportName ? delimiter + exportName : ''}`;
114-
} else {
115-
lines[lines.length - 2] = ' * @extends ' + node.superClass.name;
102+
if (!node.leadingComments) {
103+
node.leadingComments = [];
104+
// Restructure named exports of classes so only the class, but not
105+
// the export are documented
106+
if (node.parent && node.parent.type === 'ExportNamedDeclaration' && node.parent.leadingComments) {
107+
for (let i = node.parent.leadingComments.length - 1; i >= 0; --i) {
108+
const comment = node.parent.leadingComments[i];
109+
if (comment.value.indexOf('@classdesc') !== -1 || !noClassdescRegEx.test(comment.value)) {
110+
node.leadingComments.push(comment);
111+
node.parent.leadingComments.splice(i, 1);
112+
const ignore = parser.astBuilder.build('/** @ignore */').comments[0];
113+
node.parent.leadingComments.push(ignore);
114+
}
115+
}
116+
}
117+
}
118+
const leadingComments = node.leadingComments;
119+
if (leadingComments.length === 0 || leadingComments[leadingComments.length - 1].value.indexOf('@classdesc') === -1 &&
120+
noClassdescRegEx.test(leadingComments[leadingComments.length - 1].value)) {
121+
// Create a suitable comment node if we don't have one on the class yet
122+
const comment = parser.astBuilder.build('/**\n */', 'helper').comments[0];
123+
node.leadingComments.push(comment);
124+
}
125+
const leadingComment = leadingComments[node.leadingComments.length - 1];
126+
const lines = leadingComment.value.split(/\r?\n/);
127+
// Add @classdesc to make JSDoc show the class description
128+
if (leadingComment.value.indexOf('@classdesc') === -1) {
129+
lines[0] += ' @classdesc';
130+
}
131+
if (node.superClass) {
132+
// Add class inheritance information because JSDoc does not honor
133+
// the ES6 class's `extends` keyword
134+
if (leadingComment.value.indexOf('@extends') === -1) {
135+
lines.push(lines[lines.length - 1]);
136+
const identifier = identifiers[node.superClass.name];
137+
if (identifier) {
138+
const absolutePath = path.resolve(path.dirname(currentSourceName), identifier.value);
139+
const moduleId = path.relative(path.join(process.cwd(), moduleRoot), absolutePath).replace(/\.js$/, '');
140+
const exportName = identifier.defaultImport ? getDefaultExportName(moduleId, parser) : node.superClass.name;
141+
const delimiter = identifier.defaultImport ? '~' : getDelimiter(moduleId, exportName, parser);
142+
lines[lines.length - 2] = ' * @extends ' + `module:${moduleId}${exportName ? delimiter + exportName : ''}`;
143+
} else {
144+
lines[lines.length - 2] = ' * @extends ' + node.superClass.name;
145+
}
116146
}
117147
leadingComment.value = lines.join('\n');
118148
}
@@ -122,27 +152,27 @@ exports.astNodeVisitor = {
122152
}
123153
if (node.comments) {
124154
node.comments.forEach(comment => {
125-
//TODO Handle typeof, to indicate that a constructor instead of an
126-
// instance is needed.
127-
comment.value = comment.value.replace(/typeof /g, '');
155+
// Replace typeof Foo with Class<Foo>
156+
comment.value = comment.value.replace(/typeof ([^,\|\}\>]*)([,\|\}\>])/g, 'Class<$1>$2');
157+
debugger
128158

129159
// Convert `import("path/to/module").export` to
130160
// `module:path/to/module~Name`
131161
let importMatch;
132162
while ((importMatch = importRegEx.exec(comment.value))) {
133163
importRegEx.lastIndex = 0;
134164
let replacement;
135-
if (importMatch[2].charAt(0) !== '.') {
165+
if (importMatch[1].charAt(0) !== '.') {
136166
// simplified replacement for external packages
137-
replacement = `module:${importMatch[2]}${importMatch[3] === 'default' ? '' : '~' + importMatch[3]}`;
167+
replacement = `module:${importMatch[1]}${importMatch[2] === 'default' ? '' : '~' + importMatch[2]}`;
138168
} else {
139-
const rel = path.resolve(path.dirname(currentSourceName), importMatch[2]);
169+
const rel = path.resolve(path.dirname(currentSourceName), importMatch[1]);
140170
const importModule = path.relative(path.join(process.cwd(), moduleRoot), rel).replace(/\.js$/, '');
141-
const exportName = importMatch[3] === 'default' ? getDefaultExportName(importModule, parser) : importMatch[3];
142-
const delimiter = importMatch[3] === 'default' ? '~': getDelimiter(importModule, exportName, parser);
171+
const exportName = importMatch[2] === 'default' ? getDefaultExportName(importModule, parser) : importMatch[2];
172+
const delimiter = importMatch[2] === 'default' ? '~': getDelimiter(importModule, exportName, parser);
143173
replacement = `module:${importModule}${exportName ? delimiter + exportName : ''}`;
144174
}
145-
comment.value = comment.value.replace(importMatch[0], replacement + importMatch[4]);
175+
comment.value = comment.value.replace(importMatch[0], replacement + importMatch[3]);
146176
}
147177

148178
// Treat `@typedef`s like named exports
@@ -155,7 +185,7 @@ exports.astNodeVisitor = {
155185

156186
// Replace local types with the full `module:` path
157187
Object.keys(identifiers).forEach(key => {
158-
const regex = new RegExp(`(@fires |[\{<\|,] ?)${key}`, 'g');
188+
const regex = new RegExp(`(@fires |[\{<\|,] ?!?)${key}`, 'g');
159189
if (regex.test(comment.value)) {
160190
const identifier = identifiers[key];
161191
const absolutePath = path.resolve(path.dirname(currentSourceName), identifier.value);

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "jsdoc-plugin-typescript",
3-
"version": "1.0.0",
3+
"version": "1.0.1",
44
"description": "Plugin to make TypeScript's JSDoc type annotations work with JSDoc",
55
"main": "index.js",
66
"scripts": {

0 commit comments

Comments
 (0)