UNPKG

eslint-plugin-jsdoc

Version:
2,092 lines (1,857 loc) 60.7 kB
import * as jsdocUtils from './jsdocUtils.js'; import { commentHandler, getJSDocComment, parseComment, } from '@es-joy/jsdoccomment'; import { stringify as commentStringify, util, } from 'comment-parser'; import esquery from 'esquery'; /** * @typedef {number} Integer */ /** * @typedef {import('@es-joy/jsdoccomment').JsdocBlockWithInline} JsdocBlockWithInline */ /** * @typedef {{ * disallowName?: string, * allowName?: string, * context?: string, * comment?: string, * tags?: string[], * replacement?: string, * minimum?: Integer, * message?: string, * forceRequireReturn?: boolean * }} ContextObject */ /** * @typedef {string|ContextObject} Context */ /** * @callback CheckJsdoc * @param {{ * lastIndex?: Integer, * isFunctionContext?: boolean, * selector?: string, * comment?: string * }} info * @param {null|((jsdoc: import('@es-joy/jsdoccomment').JsdocBlockWithInline) => boolean|undefined)} handler * @param {import('eslint').Rule.Node} node * @returns {void} */ /** * @callback ForEachPreferredTag * @param {string} tagName * @param {( * matchingJsdocTag: import('@es-joy/jsdoccomment').JsdocTagWithInline, * targetTagName: string * ) => void} arrayHandler * @param {boolean} [skipReportingBlockedTag] * @returns {void} */ /** * @callback ReportSettings * @param {string} message * @returns {void} */ /** * @callback ParseClosureTemplateTag * @param {import('comment-parser').Spec} tag * @returns {string[]} */ /** * @callback GetPreferredTagNameObject * @param {{ * tagName: string * }} cfg * @returns {string|false|{ * message: string; * replacement?: string|undefined * }|{ * blocked: true, * tagName: string * }} */ /** * @typedef {{ * forEachPreferredTag: ForEachPreferredTag, * reportSettings: ReportSettings, * parseClosureTemplateTag: ParseClosureTemplateTag, * getPreferredTagNameObject: GetPreferredTagNameObject, * pathDoesNotBeginWith: import('./jsdocUtils.js').PathDoesNotBeginWith * }} BasicUtils */ /** * @callback IsIteratingFunction * @returns {boolean} */ /** * @callback IsVirtualFunction * @returns {boolean} */ /** * @callback Stringify * @param {import('comment-parser').Block} tagBlock * @param {boolean} [specRewire] * @returns {string} */ /** * @callback ReportJSDoc * @param {string} msg * @param {null|import('comment-parser').Spec|{line: Integer, column?: Integer}} [tag] * @param {(() => void)|null} [handler] * @param {boolean} [specRewire] * @param {undefined|{ * [key: string]: string * }} [data] */ /** * @callback GetRegexFromString * @param {string} str * @param {string} [requiredFlags] * @returns {RegExp} */ /** * @callback GetTagDescription * @param {import('comment-parser').Spec} tg * @param {boolean} [returnArray] * @returns {string[]|string} */ /** * @callback SetTagDescription * @param {import('comment-parser').Spec} tg * @param {RegExp} matcher * @param {(description: string) => string} setter * @returns {Integer} */ /** * @callback GetDescription * @returns {{ * description: string, * descriptions: string[], * lastDescriptionLine: Integer * }} */ /** * @callback SetBlockDescription * @param {( * info: { * delimiter: string, * postDelimiter: string, * start: string * }, * seedTokens: ( * tokens?: Partial<import('comment-parser').Tokens> * ) => import('comment-parser').Tokens, * descLines: string[] * ) => import('comment-parser').Line[]} setter * @returns {void} */ /** * @callback SetDescriptionLines * @param {RegExp} matcher * @param {(description: string) => string} setter * @returns {Integer} */ /** * @callback ChangeTag * @param {import('comment-parser').Spec} tag * @param {...Partial<import('comment-parser').Tokens>} tokens * @returns {void} */ /** * @callback SetTag * @param {import('comment-parser').Spec & { * line: Integer * }} tag * @param {Partial<import('comment-parser').Tokens>} [tokens] * @returns {void} */ /** * @callback RemoveTag * @param {Integer} tagIndex * @param {{ * removeEmptyBlock?: boolean, * tagSourceOffset?: Integer * }} [cfg] * @returns {void} */ /** * @callback AddTag * @param {string} targetTagName * @param {Integer} [number] * @param {import('comment-parser').Tokens|{}} [tokens] * @returns {void} */ /** * @callback GetFirstLine * @returns {Integer|undefined} */ /** * @typedef {( * tokens?: Partial<import('comment-parser').Tokens> | undefined * ) => import('comment-parser').Tokens} SeedTokens */ /** * Sets tokens to empty string. * @callback EmptyTokens * @param {import('comment-parser').Tokens} tokens * @returns {void} */ /** * @callback AddLine * @param {Integer} sourceIndex * @param {Partial<import('comment-parser').Tokens>} tokens * @returns {void} */ /** * @callback AddLines * @param {Integer} tagIndex * @param {Integer} tagSourceOffset * @param {Integer} numLines * @returns {void} */ /** * @callback MakeMultiline * @returns {void} */ /** * @callback GetFunctionParameterNames * @param {boolean} [useDefaultObjectProperties] * @returns {import('./jsdocUtils.js').ParamNameInfo[]} */ /** * @callback HasParams * @returns {Integer} */ /** * @callback IsGenerator * @returns {boolean} */ /** * @callback IsConstructor * @returns {boolean} */ /** * @callback GetJsdocTagsDeep * @param {string} tagName * @returns {false|{ * idx: Integer, * name: string, * type: string * }[]} */ /** * @callback GetPreferredTagName * @param {{ * tagName: string, * skipReportingBlockedTag?: boolean, * allowObjectReturn?: boolean, * defaultMessage?: string * }} cfg * @returns {string|undefined|false|{ * message: string; * replacement?: string|undefined; * }|{ * blocked: true, * tagName: string * }} */ /** * @callback IsValidTag * @param {string} name * @param {string[]} definedTags * @returns {boolean} */ /** * @callback HasATag * @param {string[]} names * @returns {boolean} */ /** * @callback HasTag * @param {string} name * @returns {boolean} */ /** * @callback ComparePaths * @param {string} name * @returns {(otherPathName: string) => boolean} */ /** * @callback DropPathSegmentQuotes * @param {string} name * @returns {string} */ /** * @callback AvoidDocs * @returns {boolean} */ /** * @callback TagMightHaveNamePositionTypePosition * @param {string} tagName * @param {import('./getDefaultTagStructureForMode.js'). * TagStructure[]} [otherModeMaps] * @returns {boolean|{otherMode: true}} */ /** * @callback TagMustHave * @param {string} tagName * @param {import('./getDefaultTagStructureForMode.js'). * TagStructure[]} otherModeMaps * @returns {boolean|{ * otherMode: false * }} */ /** * @callback TagMissingRequiredTypeOrNamepath * @param {import('comment-parser').Spec} tag * @param {import('./getDefaultTagStructureForMode.js'). * TagStructure[]} otherModeMaps * @returns {boolean|{ * otherMode: false * }} */ /** * @callback IsNamepathX * @param {string} tagName * @returns {boolean} */ /** * @callback GetTagStructureForMode * @param {import('./jsdocUtils.js').ParserMode} mde * @returns {import('./getDefaultTagStructureForMode.js').TagStructure} */ /** * @callback MayBeUndefinedTypeTag * @param {import('comment-parser').Spec} tag * @returns {boolean} */ /** * @callback HasValueOrExecutorHasNonEmptyResolveValue * @param {boolean} anyPromiseAsReturn * @param {boolean} [allBranches] * @returns {boolean} */ /** * @callback HasYieldValue * @returns {boolean} */ /** * @callback HasYieldReturnValue * @returns {boolean} */ /** * @callback HasThrowValue * @returns {boolean} */ /** * @callback IsAsync * @returns {boolean|undefined} */ /** * @callback GetTags * @param {string} tagName * @returns {import('comment-parser').Spec[]} */ /** * @callback GetPresentTags * @param {string[]} tagList * @returns {import('@es-joy/jsdoccomment').JsdocTagWithInline[]} */ /** * @callback FilterTags * @param {(tag: import('@es-joy/jsdoccomment').JsdocTagWithInline) => boolean} filter * @returns {import('@es-joy/jsdoccomment').JsdocTagWithInline[]} */ /** * @callback FilterAllTags * @param {(tag: (import('comment-parser').Spec| * import('@es-joy/jsdoccomment').JsdocInlineTagNoType)) => boolean} filter * @returns {(import('comment-parser').Spec| * import('@es-joy/jsdoccomment').JsdocInlineTagNoType)[]} */ /** * @callback GetTagsByType * @param {import('comment-parser').Spec[]} tags * @returns {{ * tagsWithNames: import('comment-parser').Spec[], * tagsWithoutNames: import('comment-parser').Spec[] * }} */ /** * @callback HasOptionTag * @param {string} tagName * @returns {boolean} */ /** * @callback GetClassNode * @returns {Node|null} */ /** * @callback GetClassJsdoc * @returns {null|JsdocBlockWithInline} */ /** * @callback ClassHasTag * @param {string} tagName * @returns {boolean} */ /** * @callback FindContext * @param {Context[]} contexts * @param {string|undefined} comment * @returns {{ * foundContext: Context|undefined, * contextStr: string * }} */ /** * @typedef {BasicUtils & { * isIteratingFunction: IsIteratingFunction, * isVirtualFunction: IsVirtualFunction, * stringify: Stringify, * reportJSDoc: ReportJSDoc, * getRegexFromString: GetRegexFromString, * getTagDescription: GetTagDescription, * setTagDescription: SetTagDescription, * getDescription: GetDescription, * setBlockDescription: SetBlockDescription, * setDescriptionLines: SetDescriptionLines, * changeTag: ChangeTag, * setTag: SetTag, * removeTag: RemoveTag, * addTag: AddTag, * getFirstLine: GetFirstLine, * seedTokens: SeedTokens, * emptyTokens: EmptyTokens, * addLine: AddLine, * addLines: AddLines, * makeMultiline: MakeMultiline, * flattenRoots: import('./jsdocUtils.js').FlattenRoots, * getFunctionParameterNames: GetFunctionParameterNames, * hasParams: HasParams, * isGenerator: IsGenerator, * isConstructor: IsConstructor, * getJsdocTagsDeep: GetJsdocTagsDeep, * getPreferredTagName: GetPreferredTagName, * isValidTag: IsValidTag, * hasATag: HasATag, * hasTag: HasTag, * comparePaths: ComparePaths, * dropPathSegmentQuotes: DropPathSegmentQuotes, * avoidDocs: AvoidDocs, * tagMightHaveNamePosition: TagMightHaveNamePositionTypePosition, * tagMightHaveTypePosition: TagMightHaveNamePositionTypePosition, * tagMustHaveNamePosition: TagMustHave, * tagMustHaveTypePosition: TagMustHave, * tagMissingRequiredTypeOrNamepath: TagMissingRequiredTypeOrNamepath, * isNamepathDefiningTag: IsNamepathX, * isNamepathReferencingTag: IsNamepathX, * isNamepathOrUrlReferencingTag: IsNamepathX, * tagMightHaveNamepath: IsNamepathX, * getTagStructureForMode: GetTagStructureForMode, * mayBeUndefinedTypeTag: MayBeUndefinedTypeTag, * hasValueOrExecutorHasNonEmptyResolveValue: HasValueOrExecutorHasNonEmptyResolveValue, * hasYieldValue: HasYieldValue, * hasYieldReturnValue: HasYieldReturnValue, * hasThrowValue: HasThrowValue, * isAsync: IsAsync, * getTags: GetTags, * getPresentTags: GetPresentTags, * filterTags: FilterTags, * filterAllTags: FilterAllTags, * getTagsByType: GetTagsByType, * hasOptionTag: HasOptionTag, * getClassNode: GetClassNode, * getClassJsdoc: GetClassJsdoc, * classHasTag: ClassHasTag, * findContext: FindContext * }} Utils */ const { rewireSpecs, seedTokens, } = util; // todo: Change these `any` types once importing types properly. /** * Should use ESLint rule's typing. * @typedef {import('eslint').Rule.RuleMetaData} EslintRuleMeta */ /** * A plain object for tracking state as needed by rules across iterations. * @typedef {{ * globalTags: {}, * hasDuplicates: { * [key: string]: boolean * }, * selectorMap: { * [selector: string]: { * [comment: string]: Integer * } * }, * hasTag: { * [key: string]: boolean * }, * hasNonComment: number, * hasNonCommentBeforeTag: { * [key: string]: boolean|number * } * }} StateObject */ /** * The Node AST as supplied by the parser. * @typedef {import('eslint').Rule.Node} Node */ /* const { align as commentAlign, flow: commentFlow, indent: commentIndent, } = transforms; */ const globalState = new Map(); /** * @param {import('eslint').Rule.RuleContext} context * @param {{ * tagNamePreference?: import('./jsdocUtils.js').TagNamePreference, * mode?: import('./jsdocUtils.js').ParserMode * }} cfg * @returns {BasicUtils} */ const getBasicUtils = (context, { mode, tagNamePreference, }) => { /** @type {BasicUtils} */ const utils = {}; /** @type {ReportSettings} */ utils.reportSettings = (message) => { context.report({ loc: { end: { column: 1, line: 1, }, start: { column: 1, line: 1, }, }, message, }); }; /** @type {ParseClosureTemplateTag} */ utils.parseClosureTemplateTag = (tag) => { return jsdocUtils.parseClosureTemplateTag(tag); }; utils.pathDoesNotBeginWith = jsdocUtils.pathDoesNotBeginWith; /** @type {GetPreferredTagNameObject} */ utils.getPreferredTagNameObject = ({ tagName, }) => { const ret = jsdocUtils.getPreferredTagNameSimple( tagName, /** @type {import('./jsdocUtils.js').ParserMode} */ (mode), tagNamePreference, context, ); const isObject = ret && typeof ret === 'object'; if (ret === false || (isObject && !ret.replacement)) { return { blocked: true, tagName, }; } return ret; }; return utils; }; /** * @callback Report * @param {string} message * @param {import('eslint').Rule.ReportFixer|null} [fix] * @param {null| * {line?: Integer, column?: Integer}| * import('comment-parser').Spec & {line?: Integer} * } [jsdocLoc] * @param {undefined|{ * [key: string]: string * }} [data] * @returns {void} */ /** * @param {Node|null} node * @param {JsdocBlockWithInline} jsdoc * @param {import('eslint').AST.Token} jsdocNode * @param {Settings} settings * @param {Report} report * @param {import('eslint').Rule.RuleContext} context * @param {import('eslint').SourceCode} sc * @param {boolean|undefined} iteratingAll * @param {RuleConfig} ruleConfig * @param {string} indent * @returns {Utils} */ const getUtils = ( node, jsdoc, jsdocNode, settings, report, context, sc, iteratingAll, ruleConfig, indent, ) => { const ancestors = /** @type {import('eslint').Rule.Node[]} */ (node ? (sc.getAncestors ? ( sc.getAncestors(node) /* c8 ignore next 4 */ ) : ( context.getAncestors() )) : []); /* c8 ignore next -- Fallback to deprecated method */ const { sourceCode = context.getSourceCode(), } = context; const utils = /** @type {Utils} */ (getBasicUtils(context, settings)); const { augmentsExtendsReplacesDocs, ignoreReplacesDocs, implementsReplacesDocs, maxLines, minLines, mode, overrideReplacesDocs, tagNamePreference, } = settings; /** @type {IsIteratingFunction} */ utils.isIteratingFunction = () => { return !iteratingAll || [ 'ArrowFunctionExpression', 'FunctionDeclaration', 'FunctionExpression', 'MethodDefinition', ].includes(String(node && node.type)); }; /** @type {IsVirtualFunction} */ utils.isVirtualFunction = () => { return Boolean(iteratingAll) && utils.hasATag([ 'callback', 'function', 'func', 'method', ]); }; /** @type {Stringify} */ utils.stringify = (tagBlock, specRewire) => { let block; if (specRewire) { block = rewireSpecs(tagBlock); } return commentStringify(/** @type {import('comment-parser').Block} */ ( specRewire ? block : tagBlock)); }; /** @type {ReportJSDoc} */ utils.reportJSDoc = (msg, tag, handler, specRewire, data) => { report(msg, handler ? /** @type {import('eslint').Rule.ReportFixer} */ ( fixer, ) => { handler(); const replacement = utils.stringify(jsdoc, specRewire); if (!replacement) { const text = sourceCode.getText(); const lastLineBreakPos = text.slice( 0, jsdocNode.range[0], ).search(/\n[ \t]*$/u); if (lastLineBreakPos > -1) { return fixer.removeRange([ lastLineBreakPos, jsdocNode.range[1], ]); } return fixer.removeRange( (/\s/u).test(text.charAt(jsdocNode.range[1])) ? [ jsdocNode.range[0], jsdocNode.range[1] + 1, ] : jsdocNode.range, ); } return fixer.replaceText(jsdocNode, replacement); } : null, tag, data); }; /** @type {GetRegexFromString} */ utils.getRegexFromString = (str, requiredFlags) => { return jsdocUtils.getRegexFromString(str, requiredFlags); }; /** @type {GetTagDescription} */ utils.getTagDescription = (tg, returnArray) => { return jsdocUtils.getTagDescription(tg, returnArray); }; /** @type {SetTagDescription} */ utils.setTagDescription = (tg, matcher, setter) => { let finalIdx = 0; tg.source.some(({ tokens: { description, }, }, idx) => { if (description && matcher.test(description)) { tg.source[idx].tokens.description = setter(description); finalIdx = idx; return true; } return false; }); return finalIdx; }; /** @type {GetDescription} */ utils.getDescription = () => { /** @type {string[]} */ const descriptions = []; let lastDescriptionLine = 0; let tagsBegun = false; jsdoc.source.some(({ tokens: { description, end, tag, }, }, idx) => { if (tag) { tagsBegun = true; } if (idx && (tag || end)) { lastDescriptionLine = idx - 1; if (!tagsBegun && description) { descriptions.push(description); } return true; } if (!tagsBegun && (idx || description)) { descriptions.push(description || (descriptions.length ? '' : '\n')); } return false; }); return { description: descriptions.join('\n'), descriptions, lastDescriptionLine, }; }; /** @type {SetBlockDescription} */ utils.setBlockDescription = (setter) => { /** @type {string[]} */ const descLines = []; /** * @type {undefined|Integer} */ let startIdx; /** * @type {undefined|Integer} */ let endIdx; /** * @type {undefined|{ * delimiter: string, * postDelimiter: string, * start: string * }} */ let info; jsdoc.source.some(({ tokens: { delimiter, description, end, postDelimiter, start, tag, }, }, idx) => { if (delimiter === '/**') { return false; } if (startIdx === undefined) { startIdx = idx; info = { delimiter, postDelimiter, start, }; } if (tag || end) { endIdx = idx; return true; } descLines.push(description); return false; }); /* c8 ignore else -- Won't be called if missing */ if (descLines.length) { jsdoc.source.splice( /** @type {Integer} */ (startIdx), /** @type {Integer} */ (endIdx) - /** @type {Integer} */ (startIdx), ...setter( /** * @type {{ * delimiter: string, * postDelimiter: string, * start: string * }} */ (info), seedTokens, descLines, ), ); } }; /** @type {SetDescriptionLines} */ utils.setDescriptionLines = (matcher, setter) => { let finalIdx = 0; jsdoc.source.some(({ tokens: { description, end, tag, }, }, idx) => { /* c8 ignore next 3 -- Already checked */ if (idx && (tag || end)) { return true; } if (description && matcher.test(description)) { jsdoc.source[idx].tokens.description = setter(description); finalIdx = idx; return true; } return false; }); return finalIdx; }; /** @type {ChangeTag} */ utils.changeTag = (tag, ...tokens) => { for (const [ idx, src, ] of tag.source.entries()) { src.tokens = { ...src.tokens, ...tokens[idx], }; } }; /** @type {SetTag} */ utils.setTag = (tag, tokens) => { tag.source = [ { number: tag.line, // Or tag.source[0].number? source: '', tokens: seedTokens({ delimiter: '*', postDelimiter: ' ', start: indent + ' ', tag: '@' + tag.tag, ...tokens, }), }, ]; }; /** @type {RemoveTag} */ utils.removeTag = (tagIndex, { removeEmptyBlock = false, tagSourceOffset = 0, } = {}) => { const { source: tagSource, } = jsdoc.tags[tagIndex]; /** @type {Integer|undefined} */ let lastIndex; const firstNumber = jsdoc.source[0].number; tagSource.some(({ number, }, tagIdx) => { const sourceIndex = jsdoc.source.findIndex(({ number: srcNumber, }) => { return number === srcNumber; }); // c8 ignore else if (sourceIndex > -1) { let spliceCount = 1; tagSource.slice(tagIdx + 1).some(({ tokens: { end: ending, tag, }, }) => { if (!tag && !ending) { spliceCount++; return false; } return true; }); const spliceIdx = sourceIndex + tagSourceOffset; const { delimiter, end, } = jsdoc.source[spliceIdx].tokens; if ( spliceIdx === 0 && jsdoc.tags.length >= 2 || !removeEmptyBlock && (end || delimiter === '/**') ) { const { tokens, } = jsdoc.source[spliceIdx]; for (const item of [ 'postDelimiter', 'tag', 'postTag', 'type', 'postType', 'name', 'postName', 'description', ]) { tokens[ /** * @type {"postDelimiter"|"tag"|"type"|"postType"| * "postTag"|"name"|"postName"|"description"} */ ( item ) ] = ''; } } else { jsdoc.source.splice(spliceIdx, spliceCount - tagSourceOffset + (spliceIdx ? 0 : jsdoc.source.length)); tagSource.splice(tagIdx + tagSourceOffset, spliceCount - tagSourceOffset + (spliceIdx ? 0 : jsdoc.source.length)); } lastIndex = sourceIndex; return true; } /* c8 ignore next 2 */ // eslint-disable-next-line @stylistic/padding-line-between-statements -- c8 return false; }); for (const [ idx, src, ] of jsdoc.source.slice(lastIndex).entries()) { src.number = firstNumber + /** @type {Integer} */ (lastIndex) + idx; } // Todo: Once rewiring of tags may be fixed in comment-parser to reflect // missing tags, this step should be added here (so that, e.g., // if accessing `jsdoc.tags`, such as to add a new tag, the // correct information will be available) }; /** @type {AddTag} */ utils.addTag = ( targetTagName, number = (jsdoc.tags[jsdoc.tags.length - 1]?.source[0]?.number ?? jsdoc.source.findIndex(({ tokens: { tag, }, }) => { return tag; }) - 1) + 1, tokens = {}, ) => { jsdoc.source.splice(number, 0, { number, source: '', tokens: seedTokens({ delimiter: '*', postDelimiter: ' ', start: indent + ' ', tag: `@${targetTagName}`, ...tokens, }), }); for (const src of jsdoc.source.slice(number + 1)) { src.number++; } }; /** @type {GetFirstLine} */ utils.getFirstLine = () => { let firstLine; for (const { number, tokens: { tag, }, } of jsdoc.source) { if (tag) { firstLine = number; break; } } return firstLine; }; /** @type {SeedTokens} */ utils.seedTokens = seedTokens; /** @type {EmptyTokens} */ utils.emptyTokens = (tokens) => { for (const prop of [ 'start', 'postDelimiter', 'tag', 'type', 'postType', 'postTag', 'name', 'postName', 'description', 'end', 'lineEnd', ]) { tokens[ /** * @type {"start"|"postDelimiter"|"tag"|"type"|"postType"| * "postTag"|"name"|"postName"|"description"|"end"|"lineEnd"} */ ( prop ) ] = ''; } }; /** @type {AddLine} */ utils.addLine = (sourceIndex, tokens) => { const number = (jsdoc.source[sourceIndex - 1]?.number || 0) + 1; jsdoc.source.splice(sourceIndex, 0, { number, source: '', tokens: seedTokens(tokens), }); for (const src of jsdoc.source.slice(number + 1)) { src.number++; } // If necessary, we can rewire the tags (misnamed method) // rewireSource(jsdoc); }; /** @type {AddLines} */ utils.addLines = (tagIndex, tagSourceOffset, numLines) => { const { source: tagSource, } = jsdoc.tags[tagIndex]; /** @type {Integer|undefined} */ let lastIndex; const firstNumber = jsdoc.source[0].number; tagSource.some(({ number, }) => { const makeLine = () => { return { number, source: '', tokens: seedTokens({ delimiter: '*', start: indent + ' ', }), }; }; const makeLines = () => { return Array.from({ length: numLines, }, makeLine); }; const sourceIndex = jsdoc.source.findIndex(({ number: srcNumber, tokens: { end, }, }) => { return number === srcNumber && !end; }); // c8 ignore else if (sourceIndex > -1) { const lines = makeLines(); jsdoc.source.splice(sourceIndex + tagSourceOffset, 0, ...lines); // tagSource.splice(tagIdx + 1, 0, ...makeLines()); lastIndex = sourceIndex; return true; } /* c8 ignore next 2 */ // eslint-disable-next-line @stylistic/padding-line-between-statements -- c8 return false; }); for (const [ idx, src, ] of jsdoc.source.slice(lastIndex).entries()) { src.number = firstNumber + /** @type {Integer} */ (lastIndex) + idx; } }; /** @type {MakeMultiline} */ utils.makeMultiline = () => { const { source: [ { tokens, }, ], } = jsdoc; const { description, lineEnd, name, postDelimiter, tag, type, } = tokens; let { tokens: { postName, postTag, postType, }, } = jsdoc.source[0]; // Strip trailing leftovers from single line ending if (!description) { if (postName) { postName = ''; } else if (postType) { postType = ''; } else /* c8 ignore else -- `comment-parser` prevents empty blocks currently per https://github.com/syavorsky/comment-parser/issues/128 */ if (postTag) { postTag = ''; } } utils.emptyTokens(tokens); utils.addLine(1, { delimiter: '*', // If a description were present, it may have whitespace attached // due to being at the end of the single line description: description.trimEnd(), name, postDelimiter, postName, postTag, postType, start: indent + ' ', tag, type, }); utils.addLine(2, { end: '*/', lineEnd, start: indent + ' ', }); }; /** * @type {import('./jsdocUtils.js').FlattenRoots} */ utils.flattenRoots = jsdocUtils.flattenRoots; /** @type {GetFunctionParameterNames} */ utils.getFunctionParameterNames = (useDefaultObjectProperties) => { return jsdocUtils.getFunctionParameterNames(node, useDefaultObjectProperties); }; /** @type {HasParams} */ utils.hasParams = () => { return jsdocUtils.hasParams(/** @type {Node} */ (node)); }; /** @type {IsGenerator} */ utils.isGenerator = () => { return node !== null && Boolean( /** * @type {import('estree').FunctionDeclaration| * import('estree').FunctionExpression} */ (node).generator || node.type === 'MethodDefinition' && node.value.generator || [ 'ExportDefaultDeclaration', 'ExportNamedDeclaration', ].includes(node.type) && /** @type {import('estree').FunctionDeclaration} */ ( /** * @type {import('estree').ExportNamedDeclaration| * import('estree').ExportDefaultDeclaration} */ (node).declaration )?.generator, ); }; /** @type {IsConstructor} */ utils.isConstructor = () => { return jsdocUtils.isConstructor(/** @type {Node} */ (node)); }; /** @type {GetJsdocTagsDeep} */ utils.getJsdocTagsDeep = (tagName) => { const name = /** @type {string|false} */ (utils.getPreferredTagName({ tagName, })); if (!name) { return false; } return jsdocUtils.getJsdocTagsDeep(jsdoc, name); }; /** @type {GetPreferredTagName} */ utils.getPreferredTagName = (args) => { return jsdocUtils.getPreferredTagName( jsdoc, { ...args, context, mode, report, tagNamePreference, }, ); }; /** @type {IsValidTag} */ utils.isValidTag = (name, definedTags) => { return jsdocUtils.isValidTag(context, mode, name, definedTags); }; /** @type {HasATag} */ utils.hasATag = (names) => { return jsdocUtils.hasATag(jsdoc, names); }; /** @type {HasTag} */ utils.hasTag = (name) => { return jsdocUtils.hasTag(jsdoc, name); }; /** @type {ComparePaths} */ utils.comparePaths = (name) => { return jsdocUtils.comparePaths(name); }; /** @type {DropPathSegmentQuotes} */ utils.dropPathSegmentQuotes = (name) => { return jsdocUtils.dropPathSegmentQuotes(name); }; /** @type {AvoidDocs} */ utils.avoidDocs = () => { if ( ignoreReplacesDocs !== false && (utils.hasTag('ignore') || utils.classHasTag('ignore')) || overrideReplacesDocs !== false && (utils.hasTag('override') || utils.classHasTag('override')) || implementsReplacesDocs !== false && (utils.hasTag('implements') || utils.classHasTag('implements')) || augmentsExtendsReplacesDocs && (utils.hasATag([ 'augments', 'extends', ]) || utils.classHasTag('augments') || utils.classHasTag('extends'))) { return true; } if (jsdocUtils.exemptSpeciaMethods( jsdoc, node, context, /** @type {import('json-schema').JSONSchema4|import('json-schema').JSONSchema4[]} */ ( ruleConfig.meta.schema ), )) { return true; } const exemptedBy = context.options[0]?.exemptedBy ?? [ 'inheritDoc', ...mode === 'closure' ? [] : [ 'inheritdoc', ], ]; if (exemptedBy.length && utils.getPresentTags(exemptedBy).length) { return true; } return false; }; for (const method of [ 'tagMightHaveNamePosition', 'tagMightHaveTypePosition', ]) { /** @type {TagMightHaveNamePositionTypePosition} */ utils[ /** @type {"tagMightHaveNamePosition"|"tagMightHaveTypePosition"} */ ( method ) ] = (tagName, otherModeMaps) => { const result = jsdocUtils[ /** @type {"tagMightHaveNamePosition"|"tagMightHaveTypePosition"} */ (method) ](tagName); if (result) { return true; } if (!otherModeMaps) { return false; } const otherResult = otherModeMaps.some((otherModeMap) => { return jsdocUtils[ /** @type {"tagMightHaveNamePosition"|"tagMightHaveTypePosition"} */ (method) ](tagName, otherModeMap); }); return otherResult ? { otherMode: true, } : false; }; } /** @type {TagMissingRequiredTypeOrNamepath} */ utils.tagMissingRequiredTypeOrNamepath = (tagName, otherModeMaps) => { const result = jsdocUtils.tagMissingRequiredTypeOrNamepath(tagName); if (!result) { return false; } const otherResult = otherModeMaps.every((otherModeMap) => { return jsdocUtils.tagMissingRequiredTypeOrNamepath(tagName, otherModeMap); }); return otherResult ? true : { otherMode: false, }; }; for (const method of [ 'tagMustHaveNamePosition', 'tagMustHaveTypePosition', ]) { /** @type {TagMustHave} */ utils[ /** @type {"tagMustHaveNamePosition"|"tagMustHaveTypePosition"} */ (method) ] = (tagName, otherModeMaps) => { const result = jsdocUtils[ /** @type {"tagMustHaveNamePosition"|"tagMustHaveTypePosition"} */ (method) ](tagName); if (!result) { return false; } // if (!otherModeMaps) { return true; } const otherResult = otherModeMaps.every((otherModeMap) => { return jsdocUtils[ /** @type {"tagMustHaveNamePosition"|"tagMustHaveTypePosition"} */ (method) ](tagName, otherModeMap); }); return otherResult ? true : { otherMode: false, }; }; } for (const method of [ 'isNamepathDefiningTag', 'isNamepathReferencingTag', 'isNamepathOrUrlReferencingTag', 'tagMightHaveNamepath', ]) { /** @type {IsNamepathX} */ utils[ /** @type {"isNamepathDefiningTag"|"isNamepathReferencingTag"|"isNamepathOrUrlReferencingTag"|"tagMightHaveNamepath"} */ ( method )] = (tagName) => { return jsdocUtils[ /** @type {"isNamepathDefiningTag"|"isNamepathReferencingTag"|"isNamepathOrUrlReferencingTag"|"tagMightHaveNamepath"} */ (method) ](tagName); }; } /** @type {GetTagStructureForMode} */ utils.getTagStructureForMode = (mde) => { return jsdocUtils.getTagStructureForMode(mde, settings.structuredTags); }; /** @type {MayBeUndefinedTypeTag} */ utils.mayBeUndefinedTypeTag = (tag) => { return jsdocUtils.mayBeUndefinedTypeTag(tag, settings.mode); }; /** @type {HasValueOrExecutorHasNonEmptyResolveValue} */ utils.hasValueOrExecutorHasNonEmptyResolveValue = (anyPromiseAsReturn, allBranches) => { return jsdocUtils.hasValueOrExecutorHasNonEmptyResolveValue( /** @type {Node} */ (node), anyPromiseAsReturn, allBranches, ); }; /** @type {HasYieldValue} */ utils.hasYieldValue = () => { if ([ 'ExportDefaultDeclaration', 'ExportNamedDeclaration', ].includes(/** @type {Node} */ (node).type)) { return jsdocUtils.hasYieldValue( /** @type {import('estree').Declaration|import('estree').Expression} */ ( /** @type {import('estree').ExportNamedDeclaration|import('estree').ExportDefaultDeclaration} */ (node).declaration ), ); } return jsdocUtils.hasYieldValue(/** @type {Node} */ (node)); }; /** @type {HasYieldReturnValue} */ utils.hasYieldReturnValue = () => { return jsdocUtils.hasYieldValue(/** @type {Node} */ (node), true); }; /** @type {HasThrowValue} */ utils.hasThrowValue = () => { return jsdocUtils.hasThrowValue(node); }; /** @type {IsAsync} */ utils.isAsync = () => { return Boolean(node && 'async' in node && node.async); }; /** @type {GetTags} */ utils.getTags = (tagName) => { return jsdocUtils.getTags(jsdoc, tagName); }; /** @type {GetPresentTags} */ utils.getPresentTags = (tagList) => { return jsdocUtils.filterTags(jsdoc, (tag) => { return tagList.includes(tag.tag); }); }; /** @type {FilterTags} */ utils.filterTags = (filter) => { return jsdocUtils.filterTags(jsdoc, (tag) => { return filter(tag); }); }; /** @type {FilterAllTags} */ utils.filterAllTags = (filter) => { const tags = jsdocUtils.getAllTags(jsdoc); return tags.filter((tag) => { return filter(tag); }); }; /** @type {GetTagsByType} */ utils.getTagsByType = (tags) => { return jsdocUtils.getTagsByType(context, mode, tags); }; /** @type {HasOptionTag} */ utils.hasOptionTag = (tagName) => { const { tags, } = context.options[0] ?? {}; return Boolean(tags && tags.includes(tagName)); }; /** @type {GetClassNode} */ utils.getClassNode = () => { return [ ...ancestors, node, ].reverse().find((parent) => { return parent && [ 'ClassDeclaration', 'ClassExpression', ].includes(parent.type); }) ?? null; }; /** @type {GetClassJsdoc} */ utils.getClassJsdoc = () => { const classNode = utils.getClassNode(); if (!classNode) { return null; } const classJsdocNode = getJSDocComment(sourceCode, classNode, { maxLines, minLines, }); if (classJsdocNode) { return parseComment(classJsdocNode, ''); } return null; }; /** @type {ClassHasTag} */ utils.classHasTag = (tagName) => { const classJsdoc = utils.getClassJsdoc(); return classJsdoc !== null && jsdocUtils.hasTag(classJsdoc, tagName); }; /** @type {ForEachPreferredTag} */ utils.forEachPreferredTag = (tagName, arrayHandler, skipReportingBlockedTag) => { return jsdocUtils.forEachPreferredTag( jsdoc, tagName, arrayHandler, { context, mode, report, skipReportingBlockedTag, tagNamePreference, }, ); }; /** @type {FindContext} */ utils.findContext = (contexts, comment) => { const foundContext = contexts.find((cntxt) => { return typeof cntxt === 'string' ? esquery.matches( /** @type {Node} */ (node), esquery.parse(cntxt), undefined, { visitorKeys: sourceCode.visitorKeys, }, ) : (!cntxt.context || cntxt.context === 'any' || esquery.matches( /** @type {Node} */ (node), esquery.parse(cntxt.context), undefined, { visitorKeys: sourceCode.visitorKeys, }, )) && comment === cntxt.comment; }); const contextStr = typeof foundContext === 'object' ? foundContext.context ?? 'any' : String(foundContext); return { contextStr, foundContext, }; }; return utils; }; /** * @typedef {{ * [key: string]: false|string|{ * message: string, * replacement?: false|string * skipRootChecking?: boolean * } * }} PreferredTypes */ /** * @typedef {{ * [key: string]: { * name?: "text"|"namepath-defining"|"namepath-referencing"|false, * type?: boolean|string[], * required?: ("name"|"type"|"typeOrNameRequired")[] * } * }} StructuredTags */ /** * Settings from ESLint types. * @typedef {{ * maxLines: Integer, * minLines: Integer, * tagNamePreference: import('./jsdocUtils.js').TagNamePreference, * mode: import('./jsdocUtils.js').ParserMode, * preferredTypes: PreferredTypes, * structuredTags: StructuredTags, * [name: string]: any, * contexts?: Context[] * }} Settings */ /** * @typedef {{ * settings?: { * jsdoc?: { * ignorePrivate: boolean, * ignoreInternal: boolean, * maxLines: Integer, * minLines: Integer, * tagNamePreference: import('./jsdocUtils.js').TagNamePreference, * preferredTypes: PreferredTypes, * structuredTags: StructuredTags, * overrideReplacesDocs: boolean, * ignoreReplacesDocs: boolean, * implementsReplacesDocs: boolean, * augmentsExtendsReplacesDocs: boolean, * exemptDestructuredRootsFromChecks: boolean, * mode: import('./jsdocUtils.js').ParserMode, * contexts: Context[], * } * } * }} JSDocSettings */ /** * @param {import('eslint').Rule.RuleContext & JSDocSettings} context * @returns {Settings|false} */ const getSettings = (context) => { /* eslint-disable perfectionist/sort-objects */ const settings = { // All rules ignorePrivate: Boolean(context.settings.jsdoc?.ignorePrivate), ignoreInternal: Boolean(context.settings.jsdoc?.ignoreInternal), maxLines: Number(context.settings.jsdoc?.maxLines ?? 1), minLines: Number(context.settings.jsdoc?.minLines ?? 0), // `check-tag-names` and many returns/param rules tagNamePreference: context.settings.jsdoc?.tagNamePreference ?? {}, // `check-types` and `no-undefined-types` preferredTypes: context.settings.jsdoc?.preferredTypes ?? {}, // `check-types`, `no-undefined-types`, `valid-types` structuredTags: context.settings.jsdoc?.structuredTags ?? {}, // `require-param`, `require-description`, `require-example`, // `require-returns`, `require-throw`, `require-yields` overrideReplacesDocs: context.settings.jsdoc?.overrideReplacesDocs, ignoreReplacesDocs: context.settings.jsdoc?.ignoreReplacesDocs, implementsReplacesDocs: context.settings.jsdoc?.implementsReplacesDocs, augmentsExtendsReplacesDocs: context.settings.jsdoc?.augmentsExtendsReplacesDocs, // `require-param-type`, `require-param-description` exemptDestructuredRootsFromChecks: context.settings.jsdoc?.exemptDestructuredRootsFromChecks, // Many rules, e.g., `check-tag-names` mode: context.settings.jsdoc?.mode ?? 'typescript', // Many rules contexts: context.settings.jsdoc?.contexts, }; /* eslint-enable perfectionist/sort-objects */ jsdocUtils.setTagStructure(settings.mode); try { jsdocUtils.overrideTagStructure(settings.structuredTags); } catch (error) { context.report({ loc: { end: { column: 1, line: 1, }, start: { column: 1, line: 1, }, }, message: /** @type {Error} */ (error).message, }); return false; } return settings; }; /** * Create the report function * @callback MakeReport * @param {import('eslint').Rule.RuleContext} context * @param {import('estree').Node} commentNode * @returns {Report} */ /** @type {MakeReport} */ const makeReport = (context, commentNode) => { /** @type {Report} */ const report = (message, fix = null, jsdocLoc = null, data = undefined) => { let loc; if (jsdocLoc) { if (!('line' in jsdocLoc)) { jsdocLoc.line = /** @type {import('comment-parser').Spec & {line?: Integer}} */ ( jsdocLoc ).source[0].number; } const lineNumber = /** @type {import('eslint').AST.SourceLocation} */ ( commentNode.loc ).start.line + /** @type {Integer} */ (jsdocLoc.line); loc = { end: { column: 0, line: lineNumber, }, start: { column: 0, line: lineNumber, }, }; if ('column' in jsdocLoc && typeof jsdocLoc.column === 'number') { const colNumber = /** @type {import('eslint').AST.SourceLocation} */ ( commentNode.loc ).start.column + jsdocLoc.column; loc.end.column = colNumber; loc.start.column = colNumber; } } context.report({ data, fix, loc, message, node: commentNode, }); }; return report; }; /** * @typedef {( * arg: { * context: import('eslint').Rule.RuleContext, * sourceCode: import('eslint').SourceCode, * indent?: string, * info?: { * comment?: string|undefined, * lastIndex?: Integer|undefined * }, * state?: StateObject, * globalState?: Map<string, Map<string, string>>, * jsdoc?: JsdocBlockWithInline, * jsdocNode?: import('eslint').Rule.Node & { * range: [number, number] * }, * node?: Node, * allComments?: import('estree').Node[] * report?: Report, * makeReport?: MakeReport, * settings: Settings, * utils: BasicUtils, * } * ) => any } JsdocVisitorBasic */ /** * @typedef {( * arg: { * context: import('eslint').Rule.RuleContext, * sourceCode: import('eslint').SourceCode, * indent: string, * info: { * comment?: string|undefined, * lastIndex?: Integer|undefined * }, * state: StateObject, * globalState: Map<string, Map<string, string>>, * jsdoc: JsdocBlockWithInline, * jsdocNode: import('eslint').Rule.Node & { * range: [number, number] * }, * node: Node|null, * allComments?: import('estree').Node[] * report: Report, * makeReport?: MakeReport, * settings: Settings, * utils: Utils, * } * ) => any } JsdocVisitor */ /** * @param {{ * comment?: string, * lastIndex?: Integer, * selector?: string, * isFunctionContext?: boolean, * }} info * @param {string} indent * @param {JsdocBlockWithInline} jsdoc * @param {RuleConfig} ruleConfig * @param {import('eslint').Rule.RuleContext} context * @param {import('@es-joy/jsdoccomment').Token} jsdocNode * @param {Node|null} node * @param {Settings} settings * @param {import('eslint').SourceCode} sourceCode * @param {JsdocVisitor} iterator * @param {StateObject} state * @param {boolean} [iteratingAll] * @returns {void} */ const iterate = ( info, indent, jsdoc, ruleConfig, context, jsdocNode, node, settings, sourceCode, iterator, state, iteratingAll, ) => { const jsdocNde = /** @type {unknown} */ (jsdocNode); const report = makeReport( context, /** @type {import('estree').Node} */ (jsdocNde), ); const utils = getUtils( node, jsdoc, /** @type {import('eslint').AST.Token} */ (jsdocNode), settings, report, context, sourceCode, iteratingAll, ruleConfig, indent, ); if ( !ruleConfig.checkInternal && settings.ignoreInternal && utils.hasTag('internal') ) { return; } if ( !ruleConfig.checkPrivate && settings.ignorePrivate && ( utils.hasTag('private') || jsdocUtils .filterTags(jsdoc, ({ tag, }) => { return tag === 'access'; }) .some(({ description, }) => { return description === 'private'; }) ) ) { return; } iterator({ context, globalState, indent, info, jsdoc, jsdocNode: /** * @type {import('eslint').Rule.Node & { * range: [number, number];}} */ (jsdocNde), node, report, settings, sourceCode, state, utils, }); }; /** * @param {string[]} lines * @param {import('estree').Comment} jsdocNode * @returns {[indent: string, jsdoc: JsdocBlockWithInline]} */ const getIndentAndJSDoc = function (lines, jsdocNode) { const sourceLine = lines[ /** @type {import('estree').SourceLocation} */ (jsdocNode.loc).start.line - 1 ]; let indentChar = sourceLine.charAt(0); if (indentChar !== ' ' && indentChar !== '\t') { indentChar = ' '; } const indnt = indentChar.repeat( /** @type {import('estree').SourceLocation} */ (jsdocNode.loc).start.column, ); const jsdc = parseComment(jsdocNode, ''); return [ indnt, jsdc, ]; }; /** * * @typedef {{node: Node & { * range: [number, number] * }, state: StateObject}} NonCommentArgs */ /** * @typedef {object} RuleConfig * @property {EslintRuleMeta} meta ESLint rule meta * @property {import('./jsdocUtils.js').DefaultContexts} [contextDefaults] Any default contexts * @property {true} [contextSelected] Whether to force a `contexts` check * @property {true} [iterateAllJsdocs] Whether to iterate all JSDoc blocks by default * regardless of context * @property {true} [checkPrivate] Whether to check `@private` blocks (normally exempted) * @property {true} [checkInternal] Whether to check `@internal` blocks (normally exempted) * @property {true} [checkFile] Whether to iterates over all JSDoc blocks regardless of attachment * @property {true} [nonGlobalSettings] Whether to avoid relying on settings for global contexts * @property {true} [noTracking] Whether to disable the tracking of visited comment nodes (as * non-tracked may conduct further actions) * @property {true} [matchContext] Whether the rule expects contexts to be based on a match option * @property {(args: { * context: import('eslint').Rule.RuleContext, * state: StateObject, * settings: Settings, * utils: BasicUtils * }) => void} [exit] Handler to be executed upon exiting iteration of program AST * @property {(nca: NonCommentArgs) => void} [nonComment] Handler to be executed if rule wishes * to be supplied nodes without comments */ /** * Create an eslint rule that iterates over all JSDocs, regardless of whether * they are attached to a function-like node. * @param {JsdocVisitor} iterator * @param {RuleConfig} ruleConfig The rule's configuration * @param {ContextObject[]|null} [contexts] The `contexts` containing relevant `comment` info. * @param {boolean} [additiveCommentContexts] If true, will have a separate * iteration for each matching comment context. Otherwise, will iterate * once if there is a single matching comment context. * @returns {import('eslint').Rule.RuleModule} */ const iterateAllJsdocs = (iterator,