UNPKG

eslint-plugin-jsdoc

Version:
202 lines (182 loc) 5.04 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, }) => { 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 { name, } = tag; const names = name.split(/,\s*/u); 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.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 '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 '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]$/u).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 for each generic type parameter', url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/require-template.md#repos-sticky-header', }, schema: [ { additionalProperties: false, properties: { requireSeparateTemplates: { type: 'boolean', }, }, type: 'object', }, ], type: 'suggestion', }, });