Skip to content

Commit f5c485d

Browse files
committed
feat(valid-types): Add more type checks
1 parent 84fb449 commit f5c485d

File tree

4 files changed

+187
-20
lines changed

4 files changed

+187
-20
lines changed

src/iterateJsdoc.js

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,21 @@ const parseComment = (commentNode, indent) => {
99
// @see https://github.com/yavorskiy/comment-parser/issues/21
1010
parsers: [
1111
commentParser.PARSERS.parse_tag,
12-
commentParser.PARSERS.parse_type,
12+
(str, data) => {
13+
if (data.tag === 'see') {
14+
// @see can't contain types, only names or links which might be confused with types
15+
return null;
16+
}
17+
18+
return commentParser.PARSERS.parse_type(str, data);
19+
},
1320
(str, data) => {
1421
if (['return', 'returns', 'throws', 'exception'].includes(data.tag)) {
1522
return null;
1623
}
24+
if (data.tag === 'see' && str.match(/{@link.+?}/)) {
25+
return null;
26+
}
1727

1828
return commentParser.PARSERS.parse_name(str, data);
1929
},
@@ -145,12 +155,19 @@ const getUtils = (
145155
utils.isTagWithType = (tagName) => {
146156
return jsdocUtils.isTagWithType(tagName);
147157
};
158+
utils.isPotentiallyEmptyTypeTag = (tagName) => {
159+
return jsdocUtils.isPotentiallyEmptyTypeTag(tagName);
160+
};
148161

149162
utils.passesEmptyNamepathCheck = (tag) => {
150163
return !tag.name && allowEmptyNamepaths &&
151164
jsdocUtils.isPotentiallyEmptyNamepathTag(tag.tag);
152165
};
153166

167+
utils.isTagWithMandatoryNamepathOrType = (tagName) => {
168+
return jsdocUtils.isTagWithMandatoryNamepathOrType(tagName);
169+
};
170+
154171
utils.hasDefinedTypeReturnTag = (tag) => {
155172
return jsdocUtils.hasDefinedTypeReturnTag(tag);
156173
};

src/jsdocUtils.js

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,20 +186,26 @@ const potentiallyEmptyNamepathTags = [
186186
'event',
187187
'callback',
188188
'class', 'constructor',
189+
'extends', 'augments',
189190
'constant', 'const',
190191
'function', 'func', 'method',
191192
'interface',
192193
'member', 'var',
193194
'mixin',
194195
'namespace',
195-
'listens', 'fires', 'emits'
196+
'listens', 'fires', 'emits',
197+
'see',
198+
199+
// GCC syntax allows typedef to be named through variable declaration rather than jsdoc name
200+
'typedef'
196201
];
197202

198203
const isPotentiallyEmptyNamepathTag = (tag) => {
199204
return potentiallyEmptyNamepathTags.includes(tag);
200205
};
201206

202207
let tagsWithTypes = [
208+
'augments', 'extends',
203209
'class',
204210
'constant',
205211
'enum',
@@ -234,10 +240,35 @@ const tagsWithTypesAliases = [
234240

235241
tagsWithTypes = tagsWithTypes.concat(tagsWithTypesAliases, closureTagsWithTypes);
236242

243+
const potentiallyEmptyTypeTags = [
244+
'class', 'constructor',
245+
'constant', 'const',
246+
'extends', 'augments',
247+
'namespace',
248+
'param', 'arg',
249+
'return', 'returns',
250+
'throws', 'exception',
251+
'yields', 'yield',
252+
'package', 'private', 'protected', 'public', 'static'
253+
];
254+
255+
const isPotentiallyEmptyTypeTag = (tag) => {
256+
return potentiallyEmptyTypeTags.includes(tag);
257+
};
258+
237259
const isTagWithType = (tagName) => {
238260
return tagsWithTypes.includes(tagName);
239261
};
240262

263+
const tagsWithMandatoryNamepathOrType = [
264+
'augments', 'extends',
265+
'param', 'arg',
266+
'typedef'
267+
];
268+
const isTagWithMandatoryNamepathOrType = (tagName) => {
269+
return tagsWithMandatoryNamepathOrType.includes(tagName);
270+
};
271+
241272
const LOOP_STATEMENTS = ['WhileStatement', 'DoWhileStatement', 'ForStatement', 'ForInStatement', 'ForOfStatement'];
242273

243274
const STATEMENTS_WITH_CHILDREN = [
@@ -530,6 +561,8 @@ export default {
530561
isNamepathDefiningTag,
531562
isNamepathTag,
532563
isPotentiallyEmptyNamepathTag,
564+
isPotentiallyEmptyTypeTag,
565+
isTagWithMandatoryNamepathOrType,
533566
isTagWithType,
534567
isValidTag,
535568
parseClosureTemplateTag

src/rules/validTypes.js

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,15 @@ export default iterateJsdoc(({
5252
return true;
5353
};
5454

55+
const hasType = Boolean(tag.type);
56+
const mustHaveType = utils.isTagWithType(tag.tag) && !utils.isPotentiallyEmptyTypeTag(tag.tag);
57+
58+
const hasNamePath = Boolean(tag.name);
59+
const mustHaveNamepath = utils.isNamepathTag(tag.tag) && !utils.passesEmptyNamepathCheck(tag);
60+
61+
const hasEither = hasType || hasNamePath;
62+
const mustHaveEither = utils.isTagWithMandatoryNamepathOrType(tag.tag);
63+
5564
if (tag.tag === 'borrows') {
5665
const thisNamepath = tag.description.replace(asExpression, '');
5766

@@ -66,13 +75,24 @@ export default iterateJsdoc(({
6675

6776
validTypeParsing(thatNamepath);
6877
}
69-
} else if (utils.isNamepathTag(tag.tag)) {
70-
if (utils.passesEmptyNamepathCheck(tag)) {
78+
} else {
79+
if (mustHaveEither && !hasEither) {
80+
report(`Tag @${tag.tag} must have either a type or namepath`);
81+
7182
return;
7283
}
73-
validTypeParsing(tag.name, tag.tag);
74-
} else if (tag.type && utils.isTagWithType(tag.tag)) {
75-
validTypeParsing(tag.type);
84+
85+
if (hasType) {
86+
validTypeParsing(tag.type);
87+
} else if (mustHaveType) {
88+
report(`Tag @${tag.tag} must have a type`);
89+
}
90+
91+
if (hasNamePath) {
92+
validTypeParsing(tag.name, tag.tag);
93+
} else if (mustHaveNamepath) {
94+
report(`Tag @${tag.tag} must have a namepath`);
95+
}
7696
}
7797
});
7898
}, {

test/rules/assertions/validTypes.js

Lines changed: 110 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -161,14 +161,82 @@ export default {
161161
}
162162
`,
163163
errors: [{
164-
line: 3,
165-
message: 'Syntax error in type: '
164+
line: 2,
165+
message: 'Tag @callback must have a namepath'
166166
}],
167167
settings: {
168168
jsdoc: {
169169
allowEmptyNamepaths: false
170170
}
171171
}
172+
},
173+
{
174+
code: `
175+
/**
176+
* @constant {str%ng}
177+
*/
178+
const FOO = 'foo';
179+
`,
180+
errors: [
181+
{
182+
line: 3,
183+
message: 'Syntax error in type: str%ng'
184+
}
185+
]
186+
},
187+
{
188+
code: `
189+
/**
190+
* @typedef {str%ng} UserString
191+
*/
192+
`,
193+
errors: [
194+
{
195+
line: 3,
196+
message: 'Syntax error in type: str%ng'
197+
}
198+
]
199+
},
200+
{
201+
code: `
202+
/**
203+
* @typedef {string} UserStr%ng
204+
*/
205+
`,
206+
errors: [
207+
{
208+
line: 3,
209+
message: 'Syntax error in type: UserStr%ng'
210+
}
211+
]
212+
},
213+
{
214+
code: `
215+
/**
216+
* @extends
217+
*/
218+
class Bar {};
219+
`,
220+
errors: [
221+
{
222+
line: 2,
223+
message: 'Tag @extends must have either a type or namepath'
224+
}
225+
]
226+
},
227+
{
228+
code: `
229+
/**
230+
* @type
231+
*/
232+
let foo;
233+
`,
234+
errors: [
235+
{
236+
line: 2,
237+
message: 'Tag @type must have a type'
238+
}
239+
]
172240
}
173241
],
174242
valid: [
@@ -222,16 +290,6 @@ export default {
222290
}
223291
`
224292
},
225-
{
226-
code: `
227-
/**
228-
* @see foo%
229-
*/
230-
function quux() {
231-
232-
}
233-
`
234-
},
235293
{
236294
code: `
237295
/**
@@ -245,7 +303,7 @@ export default {
245303
{
246304
code: `
247305
/**
248-
* @callback
306+
* @callback foo
249307
*/
250308
function quux() {
251309
@@ -312,6 +370,45 @@ export default {
312370
313371
}
314372
`
373+
},
374+
{
375+
code: `
376+
/**
377+
* @constant {string}
378+
*/
379+
const FOO = 'foo';
380+
`
381+
},
382+
{
383+
code: `
384+
/**
385+
* @extends Foo
386+
*/
387+
class Bar {};
388+
`
389+
},
390+
{
391+
code: `
392+
/**
393+
* @extends {Foo<String>}
394+
*/
395+
class Bar {};
396+
`
397+
},
398+
{
399+
code: `
400+
/**
401+
* @typedef {number|string} UserDefinedType
402+
*/
403+
`
404+
},
405+
{
406+
code: `
407+
/**
408+
* @typedef {number|string}
409+
*/
410+
let UserDefinedGCCType;
411+
`
315412
}
316413
]
317414
};

0 commit comments

Comments
 (0)