Skip to content

Commit 323a133

Browse files
bary12gajus
authored andcommitted
feat: add a type parser and two new rules for types in JSdoc comments (#67)
* Added type parser and support for nested types in checkTypes * Added rule: valid-types * update readme * Added no-undefined-types rule * Added rule no-undefined-types * add support for @typedef, document no-unused-vars
1 parent fab6471 commit 323a133

File tree

13 files changed

+397
-33
lines changed

13 files changed

+397
-33
lines changed

.README/README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ This table maps the rules between `eslint-plugin-jsdoc` and `jscs-jsdoc`.
2727
| [`require-param-type`](https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-param-type) | [`requireParamTypes`](https://github.com/jscs-dev/jscs-jsdoc#requireparamtypes) |
2828
| [`require-returns-description`](https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-returns-description) | [`requireReturnDescription`](https://github.com/jscs-dev/jscs-jsdoc#requirereturndescription) |
2929
| [`require-returns-type`](https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-returns-type) | [`requireReturnTypes`](https://github.com/jscs-dev/jscs-jsdoc#requirereturntypes) |
30+
| [`valid-types`](https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-valid-types) | N/A |
31+
| [`no-undefined-types`](https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-no-undefined-types) | N/A |
3032
| N/A | [`checkReturnTypes`](https://github.com/jscs-dev/jscs-jsdoc#checkreturntypes) |
3133
| N/A | [`checkRedundantParams`](https://github.com/jscs-dev/jscs-jsdoc#checkredundantparams) |
3234
| N/A | [`checkReturnTypes`](https://github.com/jscs-dev/jscs-jsdoc#checkreturntypes) |
@@ -69,6 +71,7 @@ Finally, enable all of the rules that you would like to use.
6971
"jsdoc/check-tag-names": 1,
7072
"jsdoc/check-types": 1,
7173
"jsdoc/newline-after-description": 1,
74+
"jsdoc/no-undefined-types": 1,
7275
"jsdoc/require-description-complete-sentence": 1,
7376
"jsdoc/require-example": 1,
7477
"jsdoc/require-hyphen-before-param-description": 1,
@@ -77,7 +80,8 @@ Finally, enable all of the rules that you would like to use.
7780
"jsdoc/require-param-name": 1,
7881
"jsdoc/require-param-type": 1,
7982
"jsdoc/require-returns-description": 1,
80-
"jsdoc/require-returns-type": 1
83+
"jsdoc/require-returns-type": 1,
84+
"jsdoc/valid-types": 1
8185
}
8286
}
8387
```
@@ -126,6 +130,7 @@ Use `settings.jsdoc.additionalTagNames` to configure additional, allowed JSDoc t
126130
{"gitdown": "include", "file": "./rules/check-tag-names.md"}
127131
{"gitdown": "include", "file": "./rules/check-types.md"}
128132
{"gitdown": "include", "file": "./rules/newline-after-description.md"}
133+
{"gitdown": "include", "file": "./rules/no-undefined-types.md"}
129134
{"gitdown": "include", "file": "./rules/require-description-complete-sentence.md"}
130135
{"gitdown": "include", "file": "./rules/require-example.md"}
131136
{"gitdown": "include", "file": "./rules/require-hyphen-before-param-description.md"}
@@ -135,3 +140,4 @@ Use `settings.jsdoc.additionalTagNames` to configure additional, allowed JSDoc t
135140
{"gitdown": "include", "file": "./rules/require-param-type.md"}
136141
{"gitdown": "include", "file": "./rules/require-returns-description.md"}
137142
{"gitdown": "include", "file": "./rules/require-returns-type.md"}
143+
{"gitdown": "include", "file": "./rules/valid-types.md"}

.README/rules/no-undefined-types.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
### `no-undefined-types`
2+
3+
Checks that types in jsdoc comments are defined. This can be used to check unimported types.
4+
5+
When enabling this rule, types in jsdoc comments will resolve as used variables, i.e. will not be marked as unused by `no-unused-vars`.
6+
7+
8+
|||
9+
|---|---|
10+
|Context|`ArrowFunctionExpression`, `FunctionDeclaration`, `FunctionExpression`|
11+
|Tags|`param`, `returns`|
12+
13+
<!-- assertions noUndefinedTypes -->

.README/rules/valid-types.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
### `valid-types`
2+
3+
Requires all types to be valid JSDoc or Closure compiler types without syntax errors.
4+
5+
|||
6+
|---|---|
7+
|Context|`ArrowFunctionExpression`, `FunctionDeclaration`, `FunctionExpression`|
8+
|Tags|`param`, `returns`|
9+
10+
<!-- assertions validTypes -->

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
},
77
"dependencies": {
88
"comment-parser": "^0.4.2",
9+
"jsdoctypeparser": "^2.0.0-alpha-8",
910
"lodash": "^4.17.4"
1011
},
1112
"description": "JSDoc linting rules for ESLint.",
@@ -17,7 +18,7 @@
1718
"babel-preset-es2015": "^6.24.1",
1819
"babel-register": "^6.26.0",
1920
"chai": "^4.1.2",
20-
"eslint": "^4.7.2",
21+
"eslint": "^4.19.1",
2122
"eslint-config-canonical": "^9.3.1",
2223
"gitdown": "^2.5.1",
2324
"globby": "^6.1.0",

src/index.js

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
/* eslint-disable import/max-dependencies */
12
import checkParamNames from './rules/checkParamNames';
23
import checkTagNames from './rules/checkTagNames';
34
import checkTypes from './rules/checkTypes';
45
import newlineAfterDescription from './rules/newlineAfterDescription';
6+
import noUndefinedTypes from './rules/noUndefinedTypes';
57
import requireDescriptionCompleteSentence from './rules/requireDescriptionCompleteSentence';
68
import requireExample from './rules/requireExample';
79
import requireHyphenBeforeParamDescription from './rules/requireHyphenBeforeParamDescription';
@@ -11,6 +13,7 @@ import requireParamDescription from './rules/requireParamDescription';
1113
import requireParamType from './rules/requireParamType';
1214
import requireReturnsDescription from './rules/requireReturnsDescription';
1315
import requireReturnsType from './rules/requireReturnsType';
16+
import validTypes from './rules/validTypes';
1417

1518
export default {
1619
configs: {
@@ -20,6 +23,7 @@ export default {
2023
'jsdoc/check-tag-names': 'warn',
2124
'jsdoc/check-types': 'warn',
2225
'jsdoc/newline-after-description': 'warn',
26+
'jsdoc/no-undefined-types': 'warn',
2327
'jsdoc/require-description-complete-sentence': 'off',
2428
'jsdoc/require-example': 'off',
2529
'jsdoc/require-hyphen-before-param-description': 'off',
@@ -28,7 +32,8 @@ export default {
2832
'jsdoc/require-param-name': 'warn',
2933
'jsdoc/require-param-type': 'warn',
3034
'jsdoc/require-returns-description': 'warn',
31-
'jsdoc/require-returns-type': 'warn'
35+
'jsdoc/require-returns-type': 'warn',
36+
'jsdoc/valid-types': 'warn'
3237
}
3338
}
3439
},
@@ -37,6 +42,7 @@ export default {
3742
'check-tag-names': checkTagNames,
3843
'check-types': checkTypes,
3944
'newline-after-description': newlineAfterDescription,
45+
'no-undefined-types': noUndefinedTypes,
4046
'require-description-complete-sentence': requireDescriptionCompleteSentence,
4147
'require-example': requireExample,
4248
'require-hyphen-before-param-description': requireHyphenBeforeParamDescription,
@@ -45,13 +51,15 @@ export default {
4551
'require-param-name': requireParamName,
4652
'require-param-type': requireParamType,
4753
'require-returns-description': requireReturnsDescription,
48-
'require-returns-type': requireReturnsType
54+
'require-returns-type': requireReturnsType,
55+
'valid-types': validTypes
4956
},
5057
rulesConfig: {
5158
'check-param-names': 'off',
5259
'check-tag-names': 'off',
5360
'check-types': 'off',
5461
'newline-after-description': 'off',
62+
'no-undefined-types': 'off',
5563
'require-description-complete-sentence': 'off',
5664
'require-example': 'off',
5765
'require-hyphen-before-param-description': 'off',
@@ -60,6 +68,7 @@ export default {
6068
'require-param-name': 'off',
6169
'require-param-type': 'off',
6270
'require-returns-description': 'off',
63-
'require-returns-type': 'off'
71+
'require-returns-type': 'off',
72+
'valid-types': 'off'
6473
}
6574
};

src/iterateJsdoc.js

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,27 @@ const curryUtils = (functionNode, jsdoc, tagNamePreference, additionalTagNames)
3232
return utils;
3333
};
3434

35+
export const parseComment = (commentNode) => {
36+
// Preserve JSDoc block start/end indentation.
37+
const indent = _.repeat(' ', commentNode.loc.start.column);
38+
39+
return commentParser(indent + '/*' + commentNode.value + indent + '*/', {
40+
// @see https://github.com/yavorskiy/comment-parser/issues/21
41+
parsers: [
42+
commentParser.PARSERS.parse_tag,
43+
commentParser.PARSERS.parse_type,
44+
(str, data) => {
45+
if (_.includes(['return', 'returns'], data.tag)) {
46+
return null;
47+
}
48+
49+
return commentParser.PARSERS.parse_name(str, data);
50+
},
51+
commentParser.PARSERS.parse_description
52+
]
53+
})[0] || {};
54+
};
55+
3556
export default (iterator) => {
3657
return (context) => {
3758
const sourceCode = context.getSourceCode();
@@ -45,23 +66,9 @@ export default (iterator) => {
4566
return;
4667
}
4768

48-
// Preserve JSDoc block start/end indentation.
4969
const indent = _.repeat(' ', jsdocNode.loc.start.column);
50-
const jsdoc = commentParser(indent + '/*' + jsdocNode.value + indent + '*/', {
51-
// @see https://github.com/yavorskiy/comment-parser/issues/21
52-
parsers: [
53-
commentParser.PARSERS.parse_tag,
54-
commentParser.PARSERS.parse_type,
55-
(str, data) => {
56-
if (_.includes(['return', 'returns'], data.tag)) {
57-
return null;
58-
}
59-
60-
return commentParser.PARSERS.parse_name(str, data);
61-
},
62-
commentParser.PARSERS.parse_description
63-
]
64-
})[0] || {};
70+
71+
const jsdoc = parseComment(jsdocNode);
6572

6673
const report = (message, fixer = null) => {
6774
if (fixer === null) {

src/rules/checkTypes.js

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import _ from 'lodash';
2+
import {parse, traverse, publish} from 'jsdoctypeparser';
23
import iterateJsdoc from './../iterateJsdoc';
34

45
let targetTags = [
@@ -51,18 +52,36 @@ export default iterateJsdoc(({
5152
});
5253

5354
_.forEach(jsdocTags, (jsdocTag) => {
54-
_.some(strictNativeTypes, (strictNativeType) => {
55-
if (strictNativeType.toLowerCase() === jsdocTag.type.toLowerCase() && strictNativeType !== jsdocTag.type) {
56-
const fix = (fixer) => {
57-
return fixer.replaceText(jsdocNode, sourceCode.getText(jsdocNode).replace('{' + jsdocTag.type + '}', '{' + strictNativeType + '}'));
58-
};
55+
const invalidTypes = [];
56+
let typeAst;
5957

60-
report('Invalid JSDoc @' + jsdocTag.tag + ' "' + jsdocTag.name + '" type "' + jsdocTag.type + '".', fix);
58+
try {
59+
typeAst = parse(jsdocTag.type);
60+
} catch (error) {
61+
return;
62+
}
6163

62-
return true;
64+
traverse(typeAst, (node) => {
65+
if (node.type === 'NAME') {
66+
for (const strictNativeType of strictNativeTypes) {
67+
if (strictNativeType.toLowerCase() === node.name.toLowerCase() && strictNativeType !== node.name) {
68+
invalidTypes.push(node.name);
69+
node.name = strictNativeType;
70+
}
71+
}
6372
}
64-
65-
return false;
6673
});
74+
75+
if (invalidTypes) {
76+
const fixedType = publish(typeAst);
77+
78+
_.forEach(invalidTypes, (invalidType) => {
79+
const fix = (fixer) => {
80+
return fixer.replaceText(jsdocNode, sourceCode.getText(jsdocNode).replace('{' + jsdocTag.type + '}', '{' + fixedType + '}'));
81+
};
82+
83+
report('Invalid JSDoc @' + jsdocTag.tag + ' "' + jsdocTag.name + '" type "' + invalidType + '".', fix);
84+
});
85+
}
6786
});
6887
});

src/rules/noUndefinedTypes.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import _ from 'lodash';
2+
import {parse as parseType, traverse} from 'jsdoctypeparser';
3+
import iterateJsdoc, {parseComment} from '../iterateJsdoc';
4+
5+
const extraTypes = ['string', 'number', 'boolean', 'any', '*'];
6+
7+
export default iterateJsdoc(({
8+
context,
9+
jsdoc,
10+
report,
11+
sourceCode
12+
}) => {
13+
const scopeManager = sourceCode.scopeManager;
14+
const globalScope = scopeManager.isModule() ? scopeManager.globalScope.childScopes[0] : scopeManager.globalScope;
15+
16+
const typedefDeclarations = _(context.getAllComments())
17+
.filter((comment) => {
18+
return _.startsWith(comment.value, '*');
19+
})
20+
.map(parseComment)
21+
.flatMap((doc) => {
22+
return doc.tags.filter((tag) => {
23+
return tag.tag === 'typedef';
24+
});
25+
})
26+
.map((tag) => {
27+
return tag.name;
28+
})
29+
.value();
30+
31+
const definedTypes = globalScope.variables.map((variable) => {
32+
return variable.name;
33+
})
34+
.concat(extraTypes)
35+
.concat(typedefDeclarations);
36+
37+
_.forEach(jsdoc.tags, (tag) => {
38+
const parsedType = parseType(tag.type);
39+
40+
traverse(parsedType, (node) => {
41+
if (node.type === 'NAME') {
42+
if (!_.includes(definedTypes, node.name)) {
43+
report('The type \'' + node.name + '\' is undefined.');
44+
} else if (!_.includes(extraTypes, node.name)) {
45+
context.markVariableAsUsed(node.name);
46+
}
47+
}
48+
});
49+
});
50+
});

src/rules/validTypes.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import _ from 'lodash';
2+
import {parse} from 'jsdoctypeparser';
3+
import iterateJsdoc from '../iterateJsdoc';
4+
5+
export default iterateJsdoc(({
6+
jsdoc,
7+
report
8+
}) => {
9+
_.forEach(jsdoc.tags, (tag) => {
10+
if (tag.type) {
11+
try {
12+
parse(tag.type);
13+
} catch (error) {
14+
if (error.name === 'SyntaxError') {
15+
report('Syntax error in type: ' + tag.type);
16+
}
17+
}
18+
}
19+
});
20+
});

0 commit comments

Comments
 (0)