diff --git a/.README/rules/check-template-names.md b/.README/rules/check-template-names.md new file mode 100644 index 00000000..f6a8832d --- /dev/null +++ b/.README/rules/check-template-names.md @@ -0,0 +1,40 @@ +# `check-template-names` + +Checks that any `@template` names are actually used in the connected +`@typedef` or type alias. + +Currently checks `TSTypeAliasDeclaration` such as: + +```ts +/** + * @template D + * @template V + */ +export type Pairs = [D, V | undefined]; +``` + +or + +```js +/** + * @template D + * @template V + * @typedef {[D, V | undefined]} Pairs + */ +``` + +||| +|---|---| +|Context|everywhere| +|Tags|`@template`| +|Recommended|false| +|Settings|| +|Options|| + +## Failing examples + + + +## Passing examples + + diff --git a/docs/rules/check-template-names.md b/docs/rules/check-template-names.md new file mode 100644 index 00000000..7626ad57 --- /dev/null +++ b/docs/rules/check-template-names.md @@ -0,0 +1,129 @@ + + +# check-template-names + +Checks that any `@template` names are actually used in the connected +`@typedef` or type alias. + +Currently checks `TSTypeAliasDeclaration` such as: + +```ts +/** + * @template D + * @template V + */ +export type Pairs = [D, V | undefined]; +``` + +or + +```js +/** + * @template D + * @template V + * @typedef {[D, V | undefined]} Pairs + */ +``` + +||| +|---|---| +|Context|everywhere| +|Tags|`@template`| +|Recommended|false| +|Settings|| +|Options|| + + + +## Failing examples + +The following patterns are considered problems: + +````js +/** + * @template D + * @template V + */ +type Pairs = [X, Y | undefined]; +// Message: @template D not in use + +/** + * @template D + * @template V + */ +export type Pairs = [X, Y | undefined]; +// Message: @template D not in use + +/** + * @template D + * @template V + * @typedef {[X, Y | undefined]} Pairs + */ +// Message: @template D not in use + +/** + * @template D + * @template V + */ +export type Pairs = [number, undefined]; +// Message: @template D not in use + +/** + * @template D + * @template V + * @typedef {[undefined]} Pairs + */ +// Settings: {"jsdoc":{"mode":"permissive"}} +// Message: @template D not in use + +/** + * @template D, U, V + */ +export type Extras = [D, U | undefined]; +// Message: @template V not in use + +/** + * @template D, U, V + * @typedef {[D, U | undefined]} Extras + */ +// Message: @template V not in use +```` + + + + + +## Passing examples + +The following patterns are not considered problems: + +````js +/** + * @template D + * @template V + */ +export type Pairs = [D, V | undefined]; + +/** + * @template D + * @template V + * @typedef {[D, V | undefined]} Pairs + */ + +/** + * @template D, U, V + */ +export type Extras = [D, U, V | undefined]; + +/** + * @template D, U, V + * @typedef {[D, U, V | undefined]} Extras + */ + +/** + * @template X + * @typedef {[D, U, V | undefined]} Extras + * @typedef {[D, U, V | undefined]} Extras + */ +```` + diff --git a/src/index.js b/src/index.js index a6a6bae6..aa750fb6 100644 --- a/src/index.js +++ b/src/index.js @@ -7,6 +7,7 @@ import checkParamNames from './rules/checkParamNames.js'; import checkPropertyNames from './rules/checkPropertyNames.js'; import checkSyntax from './rules/checkSyntax.js'; import checkTagNames from './rules/checkTagNames.js'; +import checkTemplateNames from './rules/checkTemplateNames.js'; import checkTypes from './rules/checkTypes.js'; import checkValues from './rules/checkValues.js'; import convertToJsdocComments from './rules/convertToJsdocComments.js'; @@ -81,6 +82,7 @@ const index = { 'check-property-names': checkPropertyNames, 'check-syntax': checkSyntax, 'check-tag-names': checkTagNames, + 'check-template-names': checkTemplateNames, 'check-types': checkTypes, 'check-values': checkValues, 'convert-to-jsdoc-comments': convertToJsdocComments, @@ -155,6 +157,7 @@ const createRecommendedRuleset = (warnOrError, flatName) => { 'jsdoc/check-property-names': warnOrError, 'jsdoc/check-syntax': 'off', 'jsdoc/check-tag-names': warnOrError, + 'jsdoc/check-template-names': 'off', 'jsdoc/check-types': warnOrError, 'jsdoc/check-values': warnOrError, 'jsdoc/convert-to-jsdoc-comments': 'off', diff --git a/src/rules/checkTemplateNames.js b/src/rules/checkTemplateNames.js new file mode 100644 index 00000000..439597c6 --- /dev/null +++ b/src/rules/checkTemplateNames.js @@ -0,0 +1,101 @@ +import { + parse as parseType, + traverse, + tryParse as tryParseType, +} from '@es-joy/jsdoccomment'; +import iterateJsdoc from '../iterateJsdoc.js'; + +export default iterateJsdoc(({ + context, + utils, + node, + settings, + report, +}) => { + const { + mode + } = settings; + + const templateTags = utils.getTags('template'); + + const usedNames = new Set(); + /** + * @param {import('@typescript-eslint/types').TSESTree.TSTypeAliasDeclaration} aliasDeclaration + */ + const checkParameters = (aliasDeclaration) => { + /* c8 ignore next -- Guard */ + const {params} = aliasDeclaration.typeParameters ?? {params: []}; + for (const {name: {name}} of params) { + usedNames.add(name); + } + for (const tag of templateTags) { + const {name} = tag; + const names = name.split(/,\s*/); + for (const name of names) { + if (!usedNames.has(name)) { + report(`@template ${name} not in use`, null, tag); + } + } + } + }; + + const handleTypeAliases = () => { + const nde = /** @type {import('@typescript-eslint/types').TSESTree.Node} */ ( + node + ); + if (!nde) { + return; + } + switch (nde.type) { + case 'ExportNamedDeclaration': + if (nde.declaration?.type === 'TSTypeAliasDeclaration') { + checkParameters(nde.declaration); + } + break; + case 'TSTypeAliasDeclaration': + checkParameters(nde); + break; + } + }; + + const typedefTags = utils.getTags('typedef'); + if (!typedefTags.length || typedefTags.length >= 2) { + handleTypeAliases(); + return; + } + + const potentialType = typedefTags[0].type; + const parsedType = mode === 'permissive' ? + tryParseType(/** @type {string} */ (potentialType)) : + parseType(/** @type {string} */ (potentialType), mode) + + traverse(parsedType, (nde) => { + const { + type, + value, + } = /** @type {import('jsdoc-type-pratt-parser').NameResult} */ (nde); + if (type === 'JsdocTypeName' && (/^[A-Z]$/).test(value)) { + usedNames.add(value); + } + }); + + for (const tag of templateTags) { + const {name} = tag; + const names = name.split(/,\s*/); + for (const name of names) { + if (!usedNames.has(name)) { + report(`@template ${name} not in use`, null, tag); + } + } + } +}, { + iterateAllJsdocs: true, + meta: { + docs: { + description: 'Checks that any `@template` names are actually used in the connected `@typedef` or type alias.', + url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/require-template.md#repos-sticky-header', + }, + schema: [], + type: 'suggestion', + }, +}); diff --git a/test/rules/assertions/checkTemplateNames.js b/test/rules/assertions/checkTemplateNames.js new file mode 100644 index 00000000..afe55f67 --- /dev/null +++ b/test/rules/assertions/checkTemplateNames.js @@ -0,0 +1,197 @@ +import {parser as typescriptEslintParser} from 'typescript-eslint'; + +export default { + invalid: [ + { + code: ` + /** + * @template D + * @template V + */ + type Pairs = [X, Y | undefined]; + `, + errors: [ + { + line: 3, + message: '@template D not in use', + }, + { + line: 4, + message: '@template V not in use', + }, + ], + languageOptions: { + parser: typescriptEslintParser + }, + }, + { + code: ` + /** + * @template D + * @template V + */ + export type Pairs = [X, Y | undefined]; + `, + errors: [ + { + line: 3, + message: '@template D not in use', + }, + { + line: 4, + message: '@template V not in use', + }, + ], + languageOptions: { + parser: typescriptEslintParser + }, + }, + { + code: ` + /** + * @template D + * @template V + * @typedef {[X, Y | undefined]} Pairs + */ + `, + errors: [ + { + line: 3, + message: '@template D not in use', + }, + { + line: 4, + message: '@template V not in use', + }, + ], + }, + { + code: ` + /** + * @template D + * @template V + */ + export type Pairs = [number, undefined]; + `, + errors: [ + { + line: 3, + message: '@template D not in use', + }, + { + line: 4, + message: '@template V not in use', + }, + ], + languageOptions: { + parser: typescriptEslintParser + }, + }, + { + code: ` + /** + * @template D + * @template V + * @typedef {[undefined]} Pairs + */ + `, + errors: [ + { + line: 3, + message: '@template D not in use', + }, + { + line: 4, + message: '@template V not in use', + }, + ], + settings: { + jsdoc: { + mode: 'permissive', + }, + }, + }, + { + code: ` + /** + * @template D, U, V + */ + export type Extras = [D, U | undefined]; + `, + errors: [ + { + line: 3, + message: '@template V not in use', + }, + ], + languageOptions: { + parser: typescriptEslintParser + }, + }, + { + code: ` + /** + * @template D, U, V + * @typedef {[D, U | undefined]} Extras + */ + `, + errors: [ + { + line: 3, + message: '@template V not in use', + }, + ], + }, + ], + valid: [ + { + code: ` + /** + * @template D + * @template V + */ + export type Pairs = [D, V | undefined]; + `, + languageOptions: { + parser: typescriptEslintParser + }, + }, + { + code: ` + /** + * @template D + * @template V + * @typedef {[D, V | undefined]} Pairs + */ + `, + }, + { + code: ` + /** + * @template D, U, V + */ + export type Extras = [D, U, V | undefined]; + `, + languageOptions: { + parser: typescriptEslintParser + }, + }, + { + code: ` + /** + * @template D, U, V + * @typedef {[D, U, V | undefined]} Extras + */ + `, + }, + { + code: ` + /** + * @template X + * @typedef {[D, U, V | undefined]} Extras + * @typedef {[D, U, V | undefined]} Extras + */ + `, + }, + ], +}; diff --git a/test/rules/ruleNames.json b/test/rules/ruleNames.json index c158f48e..1540d80b 100644 --- a/test/rules/ruleNames.json +++ b/test/rules/ruleNames.json @@ -8,6 +8,7 @@ "check-property-names", "check-syntax", "check-tag-names", + "check-template-names", "check-types", "check-values", "convert-to-jsdoc-comments",