UNPKG

eslint-plugin-jsdoc

Version:
227 lines (204 loc) 5.87 kB
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', }, });