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",