Skip to content
This repository was archived by the owner on Feb 21, 2022. It is now read-only.

Commit 3deb2a3

Browse files
committed
[feat] added valid-jsdoc rule and small fixes (closes #21)
1 parent ec336cc commit 3deb2a3

File tree

7 files changed

+871
-4
lines changed

7 files changed

+871
-4
lines changed

.vscode/launch.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
"NODE_ENV": "development"
1515
},
1616
"sourceMaps": true,
17-
"outDir": "src"
17+
"outDir": "src",
18+
"request": "launch"
1819
}
1920
]
2021
}

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -356,7 +356,7 @@ The following rules point out areas where you might have made mistakes.
356356
"use-isnan": true
357357
```
358358

359-
* [valid-jsdoc](http://eslint.org/docs/rules/valid-jsdoc) => valid-jsdoc (tslint-eslint-rules) [TODO](https://github.com/buzinas/tslint-eslint-rules/issues/21)
359+
* [valid-jsdoc](http://eslint.org/docs/rules/valid-jsdoc) => valid-jsdoc (tslint-eslint-rules)
360360
* Description: Ensure JSDoc comments are valid
361361
* Usage
362362

eslint_tslint.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"no-regex-spaces": true,
1515
"no-empty-character-class": true,
1616
"no-control-regex": true,
17-
"no-irregular-whitespace": true
17+
"no-irregular-whitespace": true,
18+
"valid-jsdoc": true
1819
}
1920
}

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "tslint-eslint-rules",
3-
"version": "0.2.7",
3+
"version": "0.3.0",
44
"description": "Improve your TSLint with the missing ESLint Rules",
55
"main": "index.js",
66
"scripts": {
@@ -34,6 +34,7 @@
3434
"typescript": "^1.6.2"
3535
},
3636
"dependencies": {
37+
"doctrine": "^0.7.1",
3738
"tslint": "^2.5.1"
3839
}
3940
}

src/rules/validJsdocRule.ts

Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
/// <reference path='helper.d.ts' />
2+
// import {ts, Lint} from './helper';
3+
import * as doctrine from 'doctrine';
4+
5+
export class Rule extends Lint.Rules.AbstractRule {
6+
public static FAILURE_STRING = {
7+
missingBrace: 'JSDoc type missing brace',
8+
syntaxError: 'JSDoc syntax error',
9+
missingParameterType: (name: string) => `missing JSDoc parameter type for '${name}'`,
10+
missingParameterDescription: (name: string) => `missing JSDoc parameter description for '${name}'`,
11+
duplicateParameter: (name: string) => `duplicate JSDoc parameter '${name}'`,
12+
unexpectedTag: (title: string) => `unexpected @${title} tag; function has no return statement`,
13+
missingReturnType: 'missing JSDoc return type',
14+
missingReturnDescription: 'missing JSDoc return description',
15+
prefer: (name: string) => `use @${name} instead`,
16+
missingReturn: (param: string) => `missing JSDoc @${param || 'returns'} for function`,
17+
wrongParam: (expected: string, actual: string) => `expected JSDoc for '${expected}'' but found '${actual}'`,
18+
missingParam: (name: string) => `missing JSDoc for parameter '${name}'`,
19+
wrongDescription: 'JSDoc description does not satisfy the regex pattern',
20+
invalidRegexDescription: (error: string) => `configured matchDescription is an invalid RegExp. Error: ${error}`
21+
};
22+
23+
public static prefer: Object = {};
24+
public static requireReturn: boolean = true;
25+
public static requireParamDescription: boolean = true;
26+
public static requireReturnDescription: boolean = true;
27+
public static matchDescription: string;
28+
29+
public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
30+
let opts = this.getOptions().ruleArguments;
31+
if (opts && opts.length > 0) {
32+
if (opts[0].prefer) {
33+
Rule.prefer = opts[0].prefer;
34+
}
35+
36+
Rule.requireReturn = opts[0].requireReturn !== false;
37+
Rule.requireParamDescription = opts[0].requireParamDescription !== false;
38+
Rule.requireReturnDescription = opts[0].requireReturnDescription !== false;
39+
Rule.matchDescription = opts[0].matchDescription;
40+
}
41+
42+
const walker = new ValidJsdocWalker(sourceFile, this.getOptions());
43+
return this.applyWithWalker(walker);
44+
}
45+
}
46+
47+
declare interface IReturnPresent {
48+
node: ts.Node;
49+
returnPresent: boolean;
50+
}
51+
52+
class ValidJsdocWalker extends Lint.SkippableTokenAwareRuleWalker {
53+
private fns: Array<IReturnPresent> = [];
54+
55+
protected visitSourceFile(node: ts.SourceFile) {
56+
super.visitSourceFile(node);
57+
}
58+
59+
protected visitNode(node: ts.Node) {
60+
if (node.kind === ts.SyntaxKind.ClassExpression) {
61+
this.visitClassExpression(node as ts.ClassExpression);
62+
}
63+
else {
64+
super.visitNode(node);
65+
}
66+
}
67+
68+
protected visitArrowFunction(node: ts.ArrowFunction) {
69+
this.startFunction(node);
70+
super.visitArrowFunction(node);
71+
this.checkJSDoc(node);
72+
}
73+
74+
protected visitFunctionExpression(node: ts.FunctionExpression) {
75+
this.startFunction(node);
76+
super.visitFunctionExpression(node);
77+
this.checkJSDoc(node);
78+
}
79+
80+
protected visitFunctionDeclaration(node: ts.FunctionDeclaration) {
81+
this.startFunction(node);
82+
super.visitFunctionDeclaration(node);
83+
this.checkJSDoc(node);
84+
}
85+
86+
private visitClassExpression(node: ts.ClassExpression) {
87+
this.startFunction(node);
88+
super.visitNode(node);
89+
this.checkJSDoc(node);
90+
}
91+
92+
protected visitClassDeclaration(node: ts.ClassDeclaration) {
93+
this.startFunction(node);
94+
super.visitClassDeclaration(node);
95+
this.checkJSDoc(node);
96+
}
97+
98+
protected visitMethodDeclaration(node: ts.MethodDeclaration) {
99+
this.startFunction(node);
100+
super.visitMethodDeclaration(node);
101+
this.checkJSDoc(node);
102+
}
103+
104+
protected visitConstructorDeclaration(node: ts.ConstructorDeclaration) {
105+
this.startFunction(node);
106+
super.visitConstructorDeclaration(node);
107+
this.checkJSDoc(node);
108+
}
109+
110+
protected visitReturnStatement(node: ts.ReturnStatement) {
111+
this.addReturn(node);
112+
super.visitReturnStatement(node);
113+
}
114+
115+
private startFunction(node: ts.Node) {
116+
let returnPresent = false;
117+
118+
if (node.kind === ts.SyntaxKind.ArrowFunction && (node as ts.ArrowFunction).body.kind !== ts.SyntaxKind.Block)
119+
returnPresent = true;
120+
121+
if (this.isTypeClass(node))
122+
returnPresent = true;
123+
124+
this.fns.push({ node, returnPresent });
125+
}
126+
127+
private addReturn(node: ts.ReturnStatement) {
128+
let parent: ts.Node = node;
129+
let nodes = this.fns.map(fn => fn.node);
130+
131+
while (parent && nodes.indexOf(parent) === -1)
132+
parent = parent.parent;
133+
134+
if (parent && node.expression) {
135+
this.fns[nodes.indexOf(parent)].returnPresent = true;
136+
}
137+
}
138+
139+
private isTypeClass(node: ts.Node) {
140+
return node.kind === ts.SyntaxKind.ClassExpression || node.kind === ts.SyntaxKind.ClassDeclaration;
141+
}
142+
143+
private isValidReturnType(tag: doctrine.IJSDocTag) {
144+
return tag.type.name === 'void' || tag.type.type === 'UndefinedLiteral';
145+
}
146+
147+
private getJSDocComment(node: ts.Node) {
148+
const ALLOWED_PARENTS = [
149+
ts.SyntaxKind.BinaryExpression,
150+
ts.SyntaxKind.VariableDeclaration,
151+
ts.SyntaxKind.VariableDeclarationList,
152+
ts.SyntaxKind.VariableStatement
153+
];
154+
155+
if (!node.getFullText().trim().startsWith('/**')) {
156+
if (ALLOWED_PARENTS.indexOf(node.parent.kind) !== -1) {
157+
return this.getJSDocComment(node.parent);
158+
}
159+
return {};
160+
}
161+
162+
let comments = node.getFullText();
163+
comments = comments.substring(comments.indexOf('/**'));
164+
comments = comments.substring(0, comments.indexOf('*/') + 2);
165+
166+
let start = node.pos;
167+
let width = comments.length;
168+
169+
if (!comments.startsWith('/**') || !comments.endsWith('*/')) {
170+
return {};
171+
}
172+
173+
return { comments, start, width };
174+
}
175+
176+
private checkJSDoc(node: ts.Node) {
177+
const {comments, start, width} = this.getJSDocComment(node);
178+
179+
if (!comments)
180+
return;
181+
182+
let jsdoc: doctrine.IJSDocComment;
183+
184+
try {
185+
jsdoc = doctrine.parse(comments, {
186+
strict: true,
187+
unwrap: true,
188+
sloppy: true
189+
});
190+
}
191+
catch (e) {
192+
if (/braces/i.test(e.message)) {
193+
this.addFailure(this.createFailure(start, width, Rule.FAILURE_STRING.missingBrace));
194+
}
195+
else {
196+
this.addFailure(this.createFailure(start, width, Rule.FAILURE_STRING.syntaxError));
197+
}
198+
return;
199+
}
200+
201+
const fn = this.fns.find(f => node === f.node);
202+
let params = {};
203+
let hasReturns = false;
204+
let hasConstructor = false;
205+
let isOverride = false;
206+
207+
for (let tag of jsdoc.tags) {
208+
switch (tag.title) {
209+
case 'param':
210+
case 'arg':
211+
case 'argument':
212+
if (!tag.type) {
213+
this.addFailure(this.createFailure(start, width, Rule.FAILURE_STRING.missingParameterType(tag.name)));
214+
}
215+
216+
if (!tag.description && Rule.requireParamDescription) {
217+
this.addFailure(this.createFailure(start, width, Rule.FAILURE_STRING.missingParameterDescription(tag.name)));
218+
}
219+
220+
if (params[tag.name]) {
221+
this.addFailure(this.createFailure(start, width, Rule.FAILURE_STRING.duplicateParameter(tag.name)));
222+
}
223+
else if (tag.name.indexOf('.') === -1) {
224+
params[tag.name] = true;
225+
}
226+
break;
227+
case 'return':
228+
case 'returns':
229+
hasReturns = true;
230+
231+
if (!Rule.requireReturn && !fn.returnPresent && tag.type.name !== 'void' && tag.type.name !== 'undefined') {
232+
this.addFailure(this.createFailure(start, width, Rule.FAILURE_STRING.unexpectedTag(tag.title)));
233+
}
234+
else {
235+
if (!tag.type) {
236+
this.addFailure(this.createFailure(start, width, Rule.FAILURE_STRING.missingReturnType));
237+
}
238+
239+
if (!this.isValidReturnType(tag) && !tag.description && Rule.requireReturnDescription) {
240+
this.addFailure(this.createFailure(start, width, Rule.FAILURE_STRING.missingReturnDescription));
241+
}
242+
}
243+
break;
244+
case 'constructor':
245+
case 'class':
246+
hasConstructor = true;
247+
break;
248+
case 'override':
249+
case 'inheritdoc':
250+
isOverride = true;
251+
break;
252+
}
253+
254+
// check prefer (we need to ensure it has the property and not inherit from Object - e.g: constructor)
255+
let title = Rule.prefer[tag.title];
256+
if (Rule.prefer.hasOwnProperty(tag.title) && tag.title !== title) {
257+
this.addFailure(this.createFailure(start, width, Rule.FAILURE_STRING.prefer(title)));
258+
}
259+
}
260+
261+
// check for functions missing @returns
262+
if (!isOverride && !hasReturns && !hasConstructor && node.parent.kind !== ts.SyntaxKind.GetKeyword && !this.isTypeClass(node)) {
263+
if (Rule.requireReturn || fn.returnPresent) {
264+
this.addFailure(this.createFailure(start, width, Rule.FAILURE_STRING.missingReturn(Rule.prefer['returns'])));
265+
}
266+
}
267+
268+
// check the parameters
269+
const jsdocParams = Object.keys(params);
270+
const parameters = (node as ts.SignatureDeclaration).parameters;
271+
272+
if (parameters) {
273+
parameters.forEach((param, i) => {
274+
if (param.name.kind === ts.SyntaxKind.Identifier) {
275+
let name = (param.name as ts.Identifier).text;
276+
if (jsdocParams[i] && name !== jsdocParams[i]) {
277+
this.addFailure(this.createFailure(start, width, Rule.FAILURE_STRING.wrongParam(name, jsdocParams[i])));
278+
}
279+
else if (!params[name] && !isOverride) {
280+
this.addFailure(this.createFailure(start, width, Rule.FAILURE_STRING.missingParam(name)));
281+
}
282+
}
283+
});
284+
}
285+
286+
if (Rule.matchDescription) {
287+
try {
288+
const regex = new RegExp(Rule.matchDescription);
289+
if (!regex.test(jsdoc.description)) {
290+
this.addFailure(this.createFailure(start, width, Rule.FAILURE_STRING.wrongDescription));
291+
}
292+
}
293+
catch (e) {
294+
this.addFailure(this.createFailure(start, width, e.message));
295+
}
296+
}
297+
}
298+
}

0 commit comments

Comments
 (0)