From 69c3e49d52466e3dd67f24e679dddf177e744fdd Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Sun, 21 Sep 2025 19:11:51 +0800 Subject: [PATCH] feat(`no-undefined-types`): `checkUsedTypedefs` option; fixes #1165 --- .README/rules/no-undefined-types.md | 2 +- docs/rules/no-undefined-types.md | 22 +++++- src/iterateJsdoc.js | 43 ++++++----- src/rules.d.ts | 6 +- src/rules/noUndefinedTypes.js | 88 +++++++++++++++++++++-- test/rules/assertions/noUndefinedTypes.js | 31 ++++++++ 6 files changed, 163 insertions(+), 29 deletions(-) diff --git a/.README/rules/no-undefined-types.md b/.README/rules/no-undefined-types.md index 9c0828b91..9fe06e459 100644 --- a/.README/rules/no-undefined-types.md +++ b/.README/rules/no-undefined-types.md @@ -58,7 +58,7 @@ array's items will be considered as defined for the purposes of that tag. |Aliases|`constructor`, `const`, `extends`, `var`, `arg`, `argument`, `prop`, `return`, `exception`, `yield`| |Closure-only|`package`, `private`, `protected`, `public`, `static`| |Recommended|true| -|Options|`definedTypes`, `disableReporting`, `markVariablesAsUsed`| +|Options|`checkUsedTypedefs`, `definedTypes`, `disableReporting`, `markVariablesAsUsed`| |Settings|`preferredTypes`, `mode`, `structuredTags`| diff --git a/docs/rules/no-undefined-types.md b/docs/rules/no-undefined-types.md index 5c9c6734c..bf45b23ae 100644 --- a/docs/rules/no-undefined-types.md +++ b/docs/rules/no-undefined-types.md @@ -3,6 +3,7 @@ # no-undefined-types * [Options](#user-content-no-undefined-types-options) + * [`checkUsedTypedefs`](#user-content-no-undefined-types-options-checkusedtypedefs) * [`definedTypes`](#user-content-no-undefined-types-options-definedtypes) * [`disableReporting`](#user-content-no-undefined-types-options-disablereporting) * [`markVariablesAsUsed`](#user-content-no-undefined-types-options-markvariablesasused) @@ -60,6 +61,11 @@ array's items will be considered as defined for the purposes of that tag. A single options object has the following properties. + + +### checkUsedTypedefs + +Whether to check typedefs for use within the file ### definedTypes @@ -73,7 +79,7 @@ Defaults to an empty array. Whether to disable reporting of errors. Defaults to `false`. This may be set to `true` in order to take advantage of only -marking defined variables as used. +marking defined variables as used or checking used typedefs. ### markVariablesAsUsed @@ -95,7 +101,7 @@ importing types unless used in code. |Aliases|`constructor`, `const`, `extends`, `var`, `arg`, `argument`, `prop`, `return`, `exception`, `yield`| |Closure-only|`package`, `private`, `protected`, `public`, `static`| |Recommended|true| -|Options|`definedTypes`, `disableReporting`, `markVariablesAsUsed`| +|Options|`checkUsedTypedefs`, `definedTypes`, `disableReporting`, `markVariablesAsUsed`| |Settings|`preferredTypes`, `mode`, `structuredTags`| @@ -368,6 +374,13 @@ class Filler { methodThree() {} } // Message: The type 'Filler.methodTwo' is undefined. + +/** @typedef {string} SomeType */ +/** @typedef {number} AnotherType */ + +/** @type {AnotherType} */ +// "jsdoc/no-undefined-types": ["error"|"warn", {"checkUsedTypedefs":true}] +// Message: This typedef was not used within the file ```` @@ -1029,5 +1042,10 @@ function f(a) {} * @param {helperError} c */ function a (b, c) {} + +/** @typedef {string} SomeType */ + +/** @type {SomeType} */ +// "jsdoc/no-undefined-types": ["error"|"warn", {"checkUsedTypedefs":true}] ```` diff --git a/src/iterateJsdoc.js b/src/iterateJsdoc.js index 91d5b0f5d..b4c116ad7 100644 --- a/src/iterateJsdoc.js +++ b/src/iterateJsdoc.js @@ -92,6 +92,10 @@ import esquery from 'esquery'; * parseClosureTemplateTag: ParseClosureTemplateTag, * getPreferredTagNameObject: GetPreferredTagNameObject, * pathDoesNotBeginWith: import('./jsdocUtils.js').PathDoesNotBeginWith + * isNamepathDefiningTag: IsNamepathX, + * isNamepathReferencingTag: IsNamepathX, + * isNamepathOrUrlReferencingTag: IsNamepathX, + * tagMightHaveNamepath: IsNamepathX, * }} BasicUtils */ @@ -566,7 +570,8 @@ const { * hasNonComment: number, * hasNonCommentBeforeTag: { * [key: string]: boolean|number - * } + * }, + * foundTypedefValues: string[] * }} StateObject */ @@ -599,6 +604,24 @@ const getBasicUtils = (context, { /** @type {BasicUtils} */ const utils = {}; + for (const method of [ + 'isNamepathDefiningTag', + 'isNamepathReferencingTag', + 'isNamepathOrUrlReferencingTag', + 'tagMightHaveNamepath', + ]) { + /** @type {IsNamepathX} */ + utils[ + /** @type {"isNamepathDefiningTag"|"isNamepathReferencingTag"|"isNamepathOrUrlReferencingTag"|"tagMightHaveNamepath"} */ ( + method + )] = (tagName) => { + return jsdocUtils[ + /** @type {"isNamepathDefiningTag"|"isNamepathReferencingTag"|"isNamepathOrUrlReferencingTag"|"tagMightHaveNamepath"} */ + (method) + ](tagName); + }; + } + /** @type {ReportSettings} */ utils.reportSettings = (message) => { context.report({ @@ -1543,24 +1566,6 @@ const getUtils = ( }; } - for (const method of [ - 'isNamepathDefiningTag', - 'isNamepathReferencingTag', - 'isNamepathOrUrlReferencingTag', - 'tagMightHaveNamepath', - ]) { - /** @type {IsNamepathX} */ - utils[ - /** @type {"isNamepathDefiningTag"|"isNamepathReferencingTag"|"isNamepathOrUrlReferencingTag"|"tagMightHaveNamepath"} */ ( - method - )] = (tagName) => { - return jsdocUtils[ - /** @type {"isNamepathDefiningTag"|"isNamepathReferencingTag"|"isNamepathOrUrlReferencingTag"|"tagMightHaveNamepath"} */ - (method) - ](tagName); - }; - } - /** @type {GetTagStructureForMode} */ utils.getTagStructureForMode = (mde) => { return jsdocUtils.getTagStructureForMode(mde, settings.structuredTags); diff --git a/src/rules.d.ts b/src/rules.d.ts index eb187a673..728c09221 100644 --- a/src/rules.d.ts +++ b/src/rules.d.ts @@ -1250,6 +1250,10 @@ export interface Rules { | [] | [ { + /** + * Whether to check typedefs for use within the file + */ + checkUsedTypedefs?: boolean; /** * This array can be populated to indicate other types which * are automatically considered as defined (in addition to globals, etc.). @@ -1259,7 +1263,7 @@ export interface Rules { /** * Whether to disable reporting of errors. Defaults to * `false`. This may be set to `true` in order to take advantage of only - * marking defined variables as used. + * marking defined variables as used or checking used typedefs. */ disableReporting?: boolean; /** diff --git a/src/rules/noUndefinedTypes.js b/src/rules/noUndefinedTypes.js index 3ef70fb14..8ba892379 100644 --- a/src/rules/noUndefinedTypes.js +++ b/src/rules/noUndefinedTypes.js @@ -59,8 +59,12 @@ export default iterateJsdoc(({ report, settings, sourceCode, + state, utils, }) => { + /** @type {string[]} */ + const foundTypedefValues = []; + const { scopeManager, } = sourceCode; @@ -73,11 +77,13 @@ export default iterateJsdoc(({ const /** * @type {{ + * checkUsedTypedefs: boolean * definedTypes: string[], * disableReporting: boolean, - * markVariablesAsUsed: boolean + * markVariablesAsUsed: boolean, * }} */ { + checkUsedTypedefs = false, definedTypes = [], disableReporting = false, markVariablesAsUsed = true, @@ -128,14 +134,16 @@ export default iterateJsdoc(({ return commentNode.value.replace(/^\s*globals/v, '').trim().split(/,\s*/v); }).concat(Object.keys(context.languageOptions.globals ?? [])); - const typedefDeclarations = comments + const typedefs = comments .flatMap((doc) => { return doc.tags.filter(({ tag, }) => { return utils.isNamepathDefiningTag(tag); }); - }) + }); + + const typedefDeclarations = typedefs .map((tag) => { return tag.name; }); @@ -204,9 +212,9 @@ export default iterateJsdoc(({ return []; } - const jsdoc = parseComment(commentNode, ''); + const jsdc = parseComment(commentNode, ''); - return jsdoc.tags.filter((tag) => { + return jsdc.tags.filter((tag) => { return tag.tag === 'template'; }); }; @@ -510,10 +518,74 @@ export default iterateJsdoc(({ context.markVariableAsUsed(val); } } + + if (checkUsedTypedefs && typedefDeclarations.includes(val)) { + foundTypedefValues.push(val); + } } }); } + + state.foundTypedefValues = foundTypedefValues; }, { + // We use this method rather than checking at end of handler above because + // in that case, it is invoked too many times and would thus report errors + // too many times. + exit ({ + context, + state, + utils, + }) { + const { + checkUsedTypedefs = false, + } = context.options[0] || {}; + + if (!checkUsedTypedefs) { + return; + } + + const allComments = context.sourceCode.getAllComments(); + const comments = allComments + .filter((comment) => { + return (/^\*(?!\*)/v).test(comment.value); + }) + .map((commentNode) => { + return { + doc: parseComment(commentNode, ''), + loc: commentNode.loc, + }; + }); + const typedefs = comments + .flatMap(({ + doc, + loc, + }) => { + const tags = doc.tags.filter(({ + tag, + }) => { + return utils.isNamepathDefiningTag(tag); + }); + if (!tags.length) { + return []; + } + + return { + loc, + tags, + }; + }); + + for (const typedef of typedefs) { + if ( + !state.foundTypedefValues.includes(typedef.tags[0].name) + ) { + context.report({ + loc: /** @type {import('@eslint/core').SourceLocation} */ (typedef.loc), + message: 'This typedef was not used within the file', + }); + } + } + }, iterateAllJsdocs: true, meta: { docs: { @@ -524,6 +596,10 @@ export default iterateJsdoc(({ { additionalProperties: false, properties: { + checkUsedTypedefs: { + description: 'Whether to check typedefs for use within the file', + type: 'boolean', + }, definedTypes: { description: `This array can be populated to indicate other types which are automatically considered as defined (in addition to globals, etc.). @@ -536,7 +612,7 @@ Defaults to an empty array.`, disableReporting: { description: `Whether to disable reporting of errors. Defaults to \`false\`. This may be set to \`true\` in order to take advantage of only -marking defined variables as used.`, +marking defined variables as used or checking used typedefs.`, type: 'boolean', }, markVariablesAsUsed: { diff --git a/test/rules/assertions/noUndefinedTypes.js b/test/rules/assertions/noUndefinedTypes.js index e8f381004..2c9517cea 100644 --- a/test/rules/assertions/noUndefinedTypes.js +++ b/test/rules/assertions/noUndefinedTypes.js @@ -651,6 +651,25 @@ export default /** @type {import('../index.js').TestCases} */ ({ ], ignoreReadme: true, }, + { + code: ` + /** @typedef {string} SomeType */ + /** @typedef {number} AnotherType */ + + /** @type {AnotherType} */ + `, + errors: [ + { + line: 2, + message: 'This typedef was not used within the file', + }, + ], + options: [ + { + checkUsedTypedefs: true, + }, + ], + }, ], valid: [ { @@ -1770,5 +1789,17 @@ export default /** @type {import('../index.js').TestCases} */ ({ function a (b, c) {} `, }, + { + code: ` + /** @typedef {string} SomeType */ + + /** @type {SomeType} */ + `, + options: [ + { + checkUsedTypedefs: true, + }, + ], + }, ], });