diff --git a/.README/README.md b/.README/README.md index 5a241d1e5..2c94aaf34 100644 --- a/.README/README.md +++ b/.README/README.md @@ -57,6 +57,7 @@ Finally, enable all of the rules that you would like to use. "jsdoc/empty-tags": 1, // Recommended "jsdoc/implements-on-classes": 1, // Recommended "jsdoc/informative-docs": 1, + "jsdoc/used-types": 1, "jsdoc/match-description": 1, "jsdoc/multiline-blocks": 1, // Recommended "jsdoc/no-bad-blocks": 1, @@ -240,4 +241,5 @@ Problems reported by rules which have a wrench :wrench: below can be fixed autom |||[sort-tags](./docs/rules/sort-tags.md#readme)|Sorts tags by a specified sequence according to tag name, optionally adding line breaks between tag groups| |:heavy_check_mark:|:wrench:|[tag-lines](./docs/rules/tag-lines.md#readme)|Enforces lines (or no lines) between tags| ||:wrench:|[text-escaping](./docs/rules/text-escaping.md#readme)|This rule can auto-escape certain characters that are input within block and tag descriptions| +|||[used-types](./docs/rules/used-types.md#readme)|Marks all types referenced from JSDoc tags as used.| |:heavy_check_mark:||[valid-types](./docs/rules/valid-types.md#readme)|Requires all types/namepaths to be valid JSDoc, Closure compiler, or TypeScript types (configurable in settings)| diff --git a/.README/rules/used-types.md b/.README/rules/used-types.md new file mode 100644 index 000000000..2b148751b --- /dev/null +++ b/.README/rules/used-types.md @@ -0,0 +1,27 @@ +# `used-types` + +{"gitdown": "contents", "rootId": "used-types"} + +Marks all types referenced from JSDoc tags as used. + +## Fixer + +Not applicable. + +#### Options + +||| +|---|---| +|Context|everywhere| +|Tags|N/A| +|Recommended|false| +|Settings|| +|Options|| + +## Failing examples + + + +## Passing examples + + diff --git a/README.md b/README.md index 19ed07970..1ee5dd5ec 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,7 @@ Finally, enable all of the rules that you would like to use. "jsdoc/empty-tags": 1, // Recommended "jsdoc/implements-on-classes": 1, // Recommended "jsdoc/informative-docs": 1, + "jsdoc/used-types": 1, "jsdoc/match-description": 1, "jsdoc/multiline-blocks": 1, // Recommended "jsdoc/no-bad-blocks": 1, @@ -261,4 +262,5 @@ Problems reported by rules which have a wrench :wrench: below can be fixed autom |||[sort-tags](./docs/rules/sort-tags.md#readme)|Sorts tags by a specified sequence according to tag name, optionally adding line breaks between tag groups| |:heavy_check_mark:|:wrench:|[tag-lines](./docs/rules/tag-lines.md#readme)|Enforces lines (or no lines) between tags| ||:wrench:|[text-escaping](./docs/rules/text-escaping.md#readme)|This rule can auto-escape certain characters that are input within block and tag descriptions| +|||[used-types](./docs/rules/used-types.md#readme)|Marks all types referenced from JSDoc tags as used.| |:heavy_check_mark:||[valid-types](./docs/rules/valid-types.md#readme)|Requires all types/namepaths to be valid JSDoc, Closure compiler, or TypeScript types (configurable in settings)| diff --git a/docs/rules/used-types.md b/docs/rules/used-types.md new file mode 100644 index 000000000..e150e15a3 --- /dev/null +++ b/docs/rules/used-types.md @@ -0,0 +1,59 @@ + + +# used-types + +* [Fixer](#user-content-used-types-fixer) +* [Failing examples](#user-content-used-types-failing-examples) +* [Passing examples](#user-content-used-types-passing-examples) + + +Marks all types referenced from JSDoc tags as used. + + + +## Fixer + +Not applicable. + + + +#### Options + +||| +|---|---| +|Context|everywhere| +|Tags|N/A| +|Recommended|false| +|Settings|| +|Options|| + + + +## Failing examples + +The following patterns are considered problems: + +````js + +```` + + + + + +## Passing examples + +The following patterns are not considered problems: + +````js +class Foo {} +/** @param {Foo} */ +function foo() {} +foo(); + +class Foo {} +/** @returns {Foo} */ +function foo() {} +foo(); +```` + diff --git a/package-lock.json b/package-lock.json index 46f9ad585..4f820669c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "debug": "^4.3.4", "escape-string-regexp": "^4.0.0", "esquery": "^1.5.0", + "jsdoc-type-pratt-parser": "^4.0.0", "semver": "^7.5.0", "spdx-expression-parse": "^3.0.1" }, @@ -44,7 +45,6 @@ "gitdown": "^3.1.5", "glob": "^10.2.2", "husky": "^8.0.3", - "jsdoc-type-pratt-parser": "^4.0.0", "lint-staged": "^13.2.2", "lodash.defaultsdeep": "^4.6.1", "mocha": "^10.2.0", diff --git a/package.json b/package.json index 530b0b44c..a91e72550 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "debug": "^4.3.4", "escape-string-regexp": "^4.0.0", "esquery": "^1.5.0", + "jsdoc-type-pratt-parser": "^4.0.0", "semver": "^7.5.0", "spdx-expression-parse": "^3.0.1" }, @@ -41,7 +42,6 @@ "gitdown": "^3.1.5", "glob": "^10.2.2", "husky": "^8.0.3", - "jsdoc-type-pratt-parser": "^4.0.0", "lint-staged": "^13.2.2", "lodash.defaultsdeep": "^4.6.1", "mocha": "^10.2.0", diff --git a/src/index.js b/src/index.js index 257f0245c..8b5c54694 100644 --- a/src/index.js +++ b/src/index.js @@ -49,6 +49,7 @@ import requireYieldsCheck from './rules/requireYieldsCheck'; import sortTags from './rules/sortTags'; import tagLines from './rules/tagLines'; import textEscaping from './rules/textEscaping'; +import usedTypes from './rules/usedTypes'; import validTypes from './rules/validTypes'; const index = { @@ -105,6 +106,7 @@ const index = { 'sort-tags': sortTags, 'tag-lines': tagLines, 'text-escaping': textEscaping, + 'used-types': usedTypes, 'valid-types': validTypes, }, }; diff --git a/src/rules/usedTypes.js b/src/rules/usedTypes.js new file mode 100644 index 000000000..42d60c664 --- /dev/null +++ b/src/rules/usedTypes.js @@ -0,0 +1,100 @@ +import { + parse, +} from 'jsdoc-type-pratt-parser'; +import iterateJsdoc from '../iterateJsdoc'; + +/** + * Extracts the type names from parsed type declaration. + */ +const extractTypeNames = (parsed) => { // eslint-disable-line complexity + if (typeof parsed !== 'object') { + return []; + } + + switch (parsed.type) { + case 'JsdocTypeName': + return [ + parsed.value, + ]; + case 'JsdocTypeOptional': + case 'JsdocTypeNullable': + case 'JsdocTypeNotNullable': + case 'JsdocTypeTypeof': + case 'JsdocTypeKeyof': + case 'JsdocTypeParenthesis': + case 'JsdocTypeVariadic': + return extractTypeNames(parsed.element); + case 'JsdocTypeUnion': + case 'JsdocTypeObject': + case 'JsdocTypeTuple': + case 'JsdocTypeIntersection': + return parsed.elements.flatMap(extractTypeNames); + case 'JsdocTypeGeneric': + return [ + ...extractTypeNames(parsed.left), + ...parsed.elements.flatMap(extractTypeNames), + ]; + case 'JsdocTypeFunction': + return [ + ...parsed.parameters.flatMap(extractTypeNames), + ...extractTypeNames(parsed.returnType), + ]; + case 'JsdocTypeNamePath': + return extractTypeNames(parsed.left); + case 'JsdocTypePredicate': + // We purposefully don't consider the left (subject of the predicate) used + return extractTypeNames(parsed.right); + case 'JsdocTypeObjectField': + return [ + ...extractTypeNames(parsed.key), + ...extractTypeNames(parsed.right), + ]; + case 'JsdocTypeJsdocObjectField': + return [ + ...extractTypeNames(parsed.left), + ...extractTypeNames(parsed.right), + ]; + case 'JsdocTypeKeyValue': + case 'JsdocTypeIndexSignature': + case 'JsdocTypeMappedType': + return extractTypeNames(parsed.right); + default: + return []; + } +}; + +export default iterateJsdoc(({ + jsdoc, + jsdocNode, + context, + settings, +}) => { + const { + mode, + } = settings; + + const sourceCode = context.getSourceCode(); + for (const tag of jsdoc.tags) { + const parsedType = parse(tag.type, mode); + const typeNames = extractTypeNames(parsedType); + for (const typeName of typeNames) { + sourceCode.markVariableAsUsed(typeName, jsdocNode); + } + } +}, { + iterateAllJsdocs: true, + meta: { + docs: { + description: 'Marks all types referenced from JSDoc tags as used.', + url: 'https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-used-types', + }, + fixable: 'code', + schema: [ + { + additionalProperties: false, + properties: {}, + }, + ], + type: 'suggestion', + }, +}); diff --git a/test/rules/assertions/usedTypes.js b/test/rules/assertions/usedTypes.js new file mode 100644 index 000000000..23b0ceb4b --- /dev/null +++ b/test/rules/assertions/usedTypes.js @@ -0,0 +1,68 @@ +export default { + invalid: [], + valid: [ + // { + // code: ` + // const foo = "bar"; + // /** This thing uses {@link foo} for something */ + // `, + // /* + // rules: { + // 'no-unused-vars': 'error', + // }, + // */ + // }, + { + code: ` + class Foo {} + /** @param {Foo} */ + function foo() {} + foo(); + `, + rules: { + 'no-unused-vars': 'error', + }, + }, + { + code: ` + class Foo {} + /** @returns {Foo} */ + function foo() {} + foo(); + `, + rules: { + 'no-unused-vars': 'error', + }, + }, + { + code: ` + class Foo {} + class Bar {} + class Baz {} + class Qux {} + /** @type {(!Foo|?Bar|...Baz|Qux[]|foo=|obj["level1"]|{Foo?: Foo}|function(this:Foo))|external:something} */ + let foo = null; + `, + ignoreReadme: true, + rules: { + 'no-unused-vars': 'error', + }, + }, + { + code: ` + class Foo {} + /** @type {typeof foo|import("some-package")|new(number, string): Foo|foo is Foo|{foo: Foo}} */ + let foo = null; + `, + ignoreReadme: true, + rules: { + 'no-unused-vars': 'error', + }, + settings: { + jsdoc: { + mode: 'typescript', + }, + }, + }, + ], +}; diff --git a/test/rules/ruleNames.json b/test/rules/ruleNames.json index 7cff81eab..6ce1a4aef 100644 --- a/test/rules/ruleNames.json +++ b/test/rules/ruleNames.json @@ -13,6 +13,7 @@ "empty-tags", "implements-on-classes", "informative-docs", + "used-types", "match-description", "match-name", "multiline-blocks",