Skip to content

Commit fcc7b26

Browse files
authored
fix(no-undefined-types): check existence of class methods/properties (#1405)
1 parent 9bb554a commit fcc7b26

File tree

3 files changed

+85
-10
lines changed

3 files changed

+85
-10
lines changed

docs/rules/no-undefined-types.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,21 @@ const a = new Todo();
337337
* @type {Another}
338338
*/
339339
// Message: The type 'BadImportIgnoredByThisRule' is undefined.
340+
341+
class Filler {
342+
/**
343+
* {@link Filler.methodTwo} non-existent
344+
* {@link Filler.nonStaticMethodTwo} non-existent too
345+
* {@link Filler.methodThree} existent
346+
* @returns {string} A string indicating the method's purpose.
347+
*/
348+
methodOne() {
349+
return 'Method Four';
350+
}
351+
352+
methodThree() {}
353+
}
354+
// Message: The type 'Filler.methodTwo' is undefined.
340355
````
341356

342357

src/rules/noUndefinedTypes.js

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -270,18 +270,28 @@ export default iterateJsdoc(({
270270
.concat((() => {
271271
// Other methods are not in scope, but we need them, and we grab them here
272272
if (node?.type === 'MethodDefinition') {
273-
return /** @type {import('estree').ClassBody} */ (node.parent).body.map((methodOrProp) => {
273+
return /** @type {import('estree').ClassBody} */ (node.parent).body.flatMap((methodOrProp) => {
274274
if (methodOrProp.type === 'MethodDefinition') {
275275
// eslint-disable-next-line unicorn/no-lonely-if -- Pattern
276276
if (methodOrProp.key.type === 'Identifier') {
277-
return methodOrProp.key.name;
277+
return [
278+
methodOrProp.key.name,
279+
`${/** @type {import('estree').ClassDeclaration} */ (
280+
node.parent?.parent
281+
)?.id?.name}.${methodOrProp.key.name}`,
282+
];
278283
}
279284
}
280285

281286
if (methodOrProp.type === 'PropertyDefinition') {
282287
// eslint-disable-next-line unicorn/no-lonely-if -- Pattern
283288
if (methodOrProp.key.type === 'Identifier') {
284-
return methodOrProp.key.name;
289+
return [
290+
methodOrProp.key.name,
291+
`${/** @type {import('estree').ClassDeclaration} */ (
292+
node.parent?.parent
293+
)?.id?.name}.${methodOrProp.key.name}`,
294+
];
285295
}
286296
}
287297
/* c8 ignore next 2 -- Not yet built */
@@ -374,26 +384,49 @@ export default iterateJsdoc(({
374384
parsedType,
375385
tag,
376386
} of tagsWithTypes) {
377-
traverse(parsedType, (nde) => {
387+
traverse(parsedType, (nde, parentNode) => {
388+
/**
389+
* @type {import('jsdoc-type-pratt-parser').NameResult & {
390+
* _parent?: import('jsdoc-type-pratt-parser').NonRootResult
391+
* }}
392+
*/
393+
// eslint-disable-next-line canonical/id-match -- Avoid clashes
394+
(nde)._parent = parentNode;
378395
const {
379396
type,
380397
value,
381398
} = /** @type {import('jsdoc-type-pratt-parser').NameResult} */ (nde);
382399

400+
let val = value;
401+
402+
/** @type {import('jsdoc-type-pratt-parser').NonRootResult|undefined} */
403+
let currNode = nde;
404+
do {
405+
currNode =
406+
/**
407+
* @type {import('jsdoc-type-pratt-parser').NameResult & {
408+
* _parent?: import('jsdoc-type-pratt-parser').NonRootResult
409+
* }}
410+
*/ (currNode)._parent;
411+
if (currNode && 'right' in currNode && currNode.right?.type === 'JsdocTypeProperty') {
412+
val = val + '.' + currNode.right.value;
413+
}
414+
} while (currNode?.type === 'JsdocTypeNamePath');
415+
383416
if (type === 'JsdocTypeName') {
384417
const structuredTypes = structuredTags[tag.tag]?.type;
385-
if (!allDefinedTypes.has(value) &&
386-
(!Array.isArray(structuredTypes) || !structuredTypes.includes(value))
418+
if (!allDefinedTypes.has(val) &&
419+
(!Array.isArray(structuredTypes) || !structuredTypes.includes(val))
387420
) {
388421
if (!disableReporting) {
389-
report(`The type '${value}' is undefined.`, null, tag);
422+
report(`The type '${val}' is undefined.`, null, tag);
390423
}
391-
} else if (markVariablesAsUsed && !extraTypes.includes(value)) {
424+
} else if (markVariablesAsUsed && !extraTypes.includes(val)) {
392425
if (sourceCode.markVariableAsUsed) {
393-
sourceCode.markVariableAsUsed(value);
426+
sourceCode.markVariableAsUsed(val);
394427
/* c8 ignore next 3 */
395428
} else {
396-
context.markVariableAsUsed(value);
429+
context.markVariableAsUsed(val);
397430
}
398431
}
399432
}

test/rules/assertions/noUndefinedTypes.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -606,6 +606,33 @@ export default /** @type {import('../index.js').TestCases} */ ({
606606
},
607607
],
608608
},
609+
{
610+
code: `
611+
class Filler {
612+
/**
613+
* {@link Filler.methodTwo} non-existent
614+
* {@link Filler.nonStaticMethodTwo} non-existent too
615+
* {@link Filler.methodThree} existent
616+
* @returns {string} A string indicating the method's purpose.
617+
*/
618+
methodOne() {
619+
return 'Method Four';
620+
}
621+
622+
methodThree() {}
623+
}
624+
`,
625+
errors: [
626+
{
627+
line: 4,
628+
message: 'The type \'Filler.methodTwo\' is undefined.',
629+
},
630+
{
631+
line: 4,
632+
message: 'The type \'Filler.nonStaticMethodTwo\' is undefined.',
633+
},
634+
],
635+
},
609636
],
610637
valid: [
611638
{

0 commit comments

Comments
 (0)