| import iterateJsdoc from '../iterateJsdoc.js'; |
| import { |
| parse as parseType, |
| traverse, |
| tryParse as tryParseType, |
| } from '@es-joy/jsdoccomment'; |
| |
| export default iterateJsdoc(({ |
| context, |
| node, |
| report, |
| settings, |
| utils, |
| }) => { |
| if (utils.avoidDocs()) { |
| return; |
| } |
| |
| const { |
| requireSeparateTemplates = false, |
| } = context.options[0] || {}; |
| |
| const { |
| mode, |
| } = settings; |
| |
| const usedNames = new Set(); |
| const templateTags = utils.getTags('template'); |
| const templateNames = templateTags.flatMap((tag) => { |
| return utils.parseClosureTemplateTag(tag); |
| }); |
| |
| if (requireSeparateTemplates) { |
| for (const tag of templateTags) { |
| const names = utils.parseClosureTemplateTag(tag); |
| if (names.length > 1) { |
| report(`Missing separate @template for ${names[1]}`, null, tag); |
| } |
| } |
| } |
| |
| /** |
| * @param {import('@typescript-eslint/types').TSESTree.FunctionDeclaration| |
| * import('@typescript-eslint/types').TSESTree.ClassDeclaration| |
| * import('@typescript-eslint/types').TSESTree.TSDeclareFunction| |
| * import('@typescript-eslint/types').TSESTree.TSInterfaceDeclaration| |
| * import('@typescript-eslint/types').TSESTree.TSTypeAliasDeclaration} aliasDeclaration |
| */ |
| const checkTypeParams = (aliasDeclaration) => { |
| const { |
| params, |
| /* c8 ignore next -- Guard */ |
| } = aliasDeclaration.typeParameters ?? { |
| /* c8 ignore next -- Guard */ |
| params: [], |
| }; |
| for (const { |
| name: { |
| name, |
| }, |
| } of params) { |
| usedNames.add(name); |
| } |
| |
| for (const usedName of usedNames) { |
| if (!templateNames.includes(usedName)) { |
| report(`Missing @template ${usedName}`); |
| } |
| } |
| }; |
| |
| const handleTypes = () => { |
| const nde = /** @type {import('@typescript-eslint/types').TSESTree.Node} */ ( |
| node |
| ); |
| if (!nde) { |
| return; |
| } |
| |
| switch (nde.type) { |
| case 'ClassDeclaration': |
| case 'FunctionDeclaration': |
| case 'TSDeclareFunction': |
| case 'TSInterfaceDeclaration': |
| case 'TSTypeAliasDeclaration': |
| checkTypeParams(nde); |
| break; |
| case 'ExportDefaultDeclaration': |
| switch (nde.declaration?.type) { |
| case 'ClassDeclaration': |
| case 'FunctionDeclaration': |
| case 'TSInterfaceDeclaration': |
| checkTypeParams(nde.declaration); |
| break; |
| } |
| |
| break; |
| case 'ExportNamedDeclaration': |
| switch (nde.declaration?.type) { |
| case 'ClassDeclaration': |
| case 'FunctionDeclaration': |
| case 'TSDeclareFunction': |
| case 'TSInterfaceDeclaration': |
| case 'TSTypeAliasDeclaration': |
| checkTypeParams(nde.declaration); |
| break; |
| } |
| |
| break; |
| } |
| }; |
| |
| const usedNameToTag = new Map(); |
| |
| /** |
| * @param {import('comment-parser').Spec} potentialTag |
| */ |
| const checkForUsedTypes = (potentialTag) => { |
| let parsedType; |
| try { |
| parsedType = mode === 'permissive' ? |
| tryParseType(/** @type {string} */ (potentialTag.type)) : |
| parseType(/** @type {string} */ (potentialTag.type), mode); |
| } catch { |
| return; |
| } |
| |
| traverse(parsedType, (nde) => { |
| const { |
| type, |
| value, |
| } = /** @type {import('jsdoc-type-pratt-parser').NameResult} */ (nde); |
| if (type === 'JsdocTypeName' && (/^[A-Z]$/v).test(value)) { |
| usedNames.add(value); |
| if (!usedNameToTag.has(value)) { |
| usedNameToTag.set(value, potentialTag); |
| } |
| } |
| }); |
| }; |
| |
| /** |
| * @param {string[]} tagNames |
| */ |
| const checkTagsAndTemplates = (tagNames) => { |
| for (const tagName of tagNames) { |
| const preferredTagName = /** @type {string} */ (utils.getPreferredTagName({ |
| tagName, |
| })); |
| const matchingTags = utils.getTags(preferredTagName); |
| for (const matchingTag of matchingTags) { |
| checkForUsedTypes(matchingTag); |
| } |
| } |
| |
| // Could check against whitelist/blacklist |
| for (const usedName of usedNames) { |
| if (!templateNames.includes(usedName)) { |
| report(`Missing @template ${usedName}`, null, usedNameToTag.get(usedName)); |
| } |
| } |
| }; |
| |
| const callbackTags = utils.getTags('callback'); |
| const functionTags = utils.getTags('function'); |
| if (callbackTags.length || functionTags.length) { |
| checkTagsAndTemplates([ |
| 'param', 'returns', |
| ]); |
| return; |
| } |
| |
| const typedefTags = utils.getTags('typedef'); |
| if (!typedefTags.length || typedefTags.length >= 2) { |
| handleTypes(); |
| return; |
| } |
| |
| const potentialTypedef = typedefTags[0]; |
| checkForUsedTypes(potentialTypedef); |
| |
| checkTagsAndTemplates([ |
| 'property', |
| ]); |
| }, { |
| iterateAllJsdocs: true, |
| meta: { |
| docs: { |
| description: 'Requires `@template` tags be present when type parameters are used.', |
| url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/require-template.md#repos-sticky-header', |
| }, |
| schema: [ |
| { |
| additionalProperties: false, |
| properties: { |
| exemptedBy: { |
| description: `Array of tags (e.g., \`['type']\`) whose presence on the document |
| block avoids the need for a \`@template\`. Defaults to an array with |
| \`inheritdoc\`. If you set this array, it will overwrite the default, |
| so be sure to add back \`inheritdoc\` if you wish its presence to cause |
| exemption of the rule.`, |
| items: { |
| type: 'string', |
| }, |
| type: 'array', |
| }, |
| requireSeparateTemplates: { |
| description: `Requires that each template have its own separate line, i.e., preventing |
| templates of this format: |
| |
| \`\`\`js |
| /** |
| * @template T, U, V |
| */ |
| \`\`\` |
| |
| Defaults to \`false\`.`, |
| type: 'boolean', |
| }, |
| }, |
| type: 'object', |
| }, |
| ], |
| type: 'suggestion', |
| }, |
| }); |