| 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, ruleConfig, contexts, additiveCommentContexts) => { |
| const trackedJsdocs = new Set(); |
| |
| /** @type {import('@es-joy/jsdoccomment').CommentHandler} */ |
| let handler; |
| |
| /** @type {Settings|false} */ |
| let settings; |
| |
| /** |
| * @param {import('eslint').Rule.RuleContext} context |
| * @param {Node|null} node |
| * @param {import('estree').Comment[]} jsdocNodes |
| * @param {StateObject} state |
| * @param {boolean} [lastCall] |
| * @returns {void} |
| */ |
| const callIterator = (context, node, jsdocNodes, state, lastCall) => { |
| /* c8 ignore next -- Fallback to deprecated method */ |
| const { |
| sourceCode = context.getSourceCode(), |
| } = context; |
| const { |
| lines, |
| } = sourceCode; |
| |
| const utils = getBasicUtils(context, /** @type {Settings} */ (settings)); |
| for (const jsdocNode of jsdocNodes) { |
| const jsdocNde = /** @type {unknown} */ (jsdocNode); |
| if (!(/^\/\*\*\s/u).test(sourceCode.getText( |
| /** @type {import('estree').Node} */ |
| (jsdocNde), |
| ))) { |
| continue; |
| } |
| |
| const [ |
| indent, |
| jsdoc, |
| ] = getIndentAndJSDoc( |
| lines, jsdocNode, |
| ); |
| |
| if (additiveCommentContexts) { |
| for (const [ |
| idx, |
| { |
| comment, |
| }, |
| ] of /** @type {ContextObject[]} */ (contexts).entries()) { |
| if (comment && handler(comment, jsdoc) === false) { |
| continue; |
| } |
| |
| iterate( |
| { |
| comment, |
| lastIndex: idx, |
| selector: node?.type, |
| }, |
| indent, |
| jsdoc, |
| ruleConfig, |
| context, |
| jsdocNode, |
| /** @type {Node} */ |
| (node), |
| /** @type {Settings} */ |
| (settings), |
| sourceCode, |
| iterator, |
| state, |
| true, |
| ); |
| } |
| |
| continue; |
| } |
| |
| let lastComment; |
| let lastIndex; |
| // eslint-disable-next-line no-loop-func |
| if (contexts && contexts.every(({ |
| comment, |
| }, idx) => { |
| lastComment = comment; |
| lastIndex = idx; |
| |
| return comment && handler(comment, jsdoc) === false; |
| })) { |
| continue; |
| } |
| |
| iterate( |
| lastComment ? { |
| comment: lastComment, |
| lastIndex, |
| selector: node?.type, |
| } : { |
| lastIndex, |
| selector: node?.type, |
| }, |
| indent, |
| jsdoc, |
| ruleConfig, |
| context, |
| jsdocNode, |
| node, |
| /** @type {Settings} */ |
| (settings), |
| sourceCode, |
| iterator, |
| state, |
| true, |
| ); |
| } |
| |
| const settngs = /** @type {Settings} */ (settings); |
| |
| if (lastCall && ruleConfig.exit) { |
| ruleConfig.exit({ |
| context, |
| settings: settngs, |
| state, |
| utils, |
| }); |
| } |
| }; |
| |
| return { |
| create (context) { |
| /* c8 ignore next -- Fallback to deprecated method */ |
| const { |
| sourceCode = context.getSourceCode(), |
| } = context; |
| settings = getSettings(context); |
| if (!settings) { |
| return {}; |
| } |
| |
| if (contexts) { |
| handler = commentHandler(settings); |
| } |
| |
| const state = {}; |
| |
| return { |
| /** |
| * @param {import('eslint').Rule.Node & { |
| * range: [Integer, Integer]; |
| * }} node |
| * @returns {void} |
| */ |
| '*:not(Program)' (node) { |
| const commentNode = getJSDocComment( |
| sourceCode, node, /** @type {Settings} */ (settings), |
| ); |
| if (!ruleConfig.noTracking && trackedJsdocs.has(commentNode)) { |
| return; |
| } |
| |
| if (!commentNode) { |
| if (ruleConfig.nonComment) { |
| const ste = /** @type {StateObject} */ (state); |
| ruleConfig.nonComment({ |
| node, |
| state: ste, |
| }); |
| } |
| |
| return; |
| } |
| |
| trackedJsdocs.add(commentNode); |
| callIterator(context, node, [ |
| /** @type {import('estree').Comment} */ |
| (commentNode), |
| ], /** @type {StateObject} */ (state)); |
| }, |
| 'Program:exit' () { |
| const allComments = sourceCode.getAllComments(); |
| const untrackedJSdoc = allComments.filter((node) => { |
| return !trackedJsdocs.has(node); |
| }); |
| |
| callIterator( |
| context, |
| null, |
| untrackedJSdoc, |
| /** @type {StateObject} */ |
| (state), |
| true, |
| ); |
| }, |
| }; |
| }, |
| meta: ruleConfig.meta, |
| }; |
| }; |
| |
| /** |
| * Create an eslint rule that iterates over all JSDocs, regardless of whether |
| * they are attached to a function-like node. |
| * @param {JsdocVisitorBasic} iterator |
| * @param {RuleConfig} ruleConfig |
| * @returns {import('eslint').Rule.RuleModule} |
| */ |
| const checkFile = (iterator, ruleConfig) => { |
| return { |
| create (context) { |
| /* c8 ignore next -- Fallback to deprecated method */ |
| const { |
| sourceCode = context.getSourceCode(), |
| } = context; |
| const settings = getSettings(context); |
| if (!settings) { |
| return {}; |
| } |
| |
| return { |
| 'Program:exit' () { |
| const allComms = /** @type {unknown} */ (sourceCode.getAllComments()); |
| const utils = getBasicUtils(context, settings); |
| |
| iterator({ |
| allComments: /** @type {import('estree').Node[]} */ (allComms), |
| context, |
| makeReport, |
| settings, |
| sourceCode, |
| utils, |
| }); |
| }, |
| }; |
| }, |
| meta: ruleConfig.meta, |
| }; |
| }; |
| |
| export { |
| getSettings, |
| // dslint-disable-next-line unicorn/prefer-export-from -- Avoid experimental parser |
| |
| }; |
| |
| /** |
| * @param {JsdocVisitor} iterator |
| * @param {RuleConfig} ruleConfig |
| * @returns {import('eslint').Rule.RuleModule} |
| */ |
| export default function iterateJsdoc (iterator, ruleConfig) { |
| const metaType = ruleConfig?.meta?.type; |
| if (!metaType || ![ |
| 'layout', 'problem', 'suggestion', |
| ].includes(metaType)) { |
| throw new TypeError('Rule must include `meta.type` option (with value "problem", "suggestion", or "layout")'); |
| } |
| |
| if (typeof iterator !== 'function') { |
| throw new TypeError('The iterator argument must be a function.'); |
| } |
| |
| if (ruleConfig.checkFile) { |
| return checkFile( |
| /** @type {JsdocVisitorBasic} */ (iterator), |
| ruleConfig, |
| ); |
| } |
| |
| if (ruleConfig.iterateAllJsdocs) { |
| return iterateAllJsdocs(iterator, ruleConfig); |
| } |
| |
| /** @type {import('eslint').Rule.RuleModule} */ |
| return { |
| /** |
| * The entrypoint for the JSDoc rule. |
| * @param {import('eslint').Rule.RuleContext} context |
| * a reference to the context which hold all important information |
| * like settings and the sourcecode to check. |
| * @returns {import('eslint').Rule.RuleListener} |
| * a listener with parser callback function. |
| */ |
| create (context) { |
| const settings = getSettings(context); |
| if (!settings) { |
| return {}; |
| } |
| |
| /** |
| * @type {Context[]|undefined} |
| */ |
| let contexts; |
| if (ruleConfig.contextDefaults || ruleConfig.contextSelected || ruleConfig.matchContext) { |
| contexts = ruleConfig.matchContext && context.options[0]?.match ? |
| context.options[0].match : |
| jsdocUtils.enforcedContexts(context, ruleConfig.contextDefaults, ruleConfig.nonGlobalSettings ? {} : settings); |
| |
| if (contexts) { |
| contexts = contexts.map((obj) => { |
| if (typeof obj === 'object' && !obj.context) { |
| return { |
| ...obj, |
| context: 'any', |
| }; |
| } |
| |
| return obj; |
| }); |
| } |
| |
| const hasPlainAny = contexts?.includes('any'); |
| const hasObjectAny = !hasPlainAny && contexts?.find((ctxt) => { |
| if (typeof ctxt === 'string') { |
| return false; |
| } |
| |
| return ctxt?.context === 'any'; |
| }); |
| if (hasPlainAny || hasObjectAny) { |
| return iterateAllJsdocs( |
| iterator, |
| ruleConfig, |
| hasObjectAny ? /** @type {ContextObject[]} */ (contexts) : null, |
| ruleConfig.matchContext, |
| ).create(context); |
| } |
| } |
| |
| /* c8 ignore next -- Fallback to deprecated method */ |
| const { |
| sourceCode = context.getSourceCode(), |
| } = context; |
| const { |
| lines, |
| } = sourceCode; |
| |
| /** @type {Partial<StateObject>} */ |
| const state = {}; |
| |
| /** @type {CheckJsdoc} */ |
| const checkJsdoc = (info, handler, node) => { |
| const jsdocNode = getJSDocComment(sourceCode, node, settings); |
| if (!jsdocNode) { |
| return; |
| } |
| |
| const [ |
| indent, |
| jsdoc, |
| ] = getIndentAndJSDoc( |
| lines, |
| /** @type {import('estree').Comment} */ |
| (jsdocNode), |
| ); |
| |
| if ( |
| // Note, `handler` should already be bound in its first argument |
| // with these only to be called after the value of |
| // `comment` |
| handler && handler(jsdoc) === false |
| ) { |
| return; |
| } |
| |
| iterate( |
| info, |
| indent, |
| jsdoc, |
| ruleConfig, |
| context, |
| jsdocNode, |
| node, |
| settings, |
| sourceCode, |
| iterator, |
| /** @type {StateObject} */ |
| (state), |
| ); |
| }; |
| |
| /** @type {import('eslint').Rule.RuleListener} */ |
| let contextObject = {}; |
| |
| if (contexts && ( |
| ruleConfig.contextDefaults || ruleConfig.contextSelected || ruleConfig.matchContext |
| )) { |
| contextObject = jsdocUtils.getContextObject( |
| contexts, |
| checkJsdoc, |
| commentHandler(settings), |
| ); |
| } else { |
| for (const prop of [ |
| 'ArrowFunctionExpression', |
| 'FunctionDeclaration', |
| 'FunctionExpression', |
| 'TSDeclareFunction', |
| ]) { |
| contextObject[prop] = checkJsdoc.bind(null, { |
| selector: prop, |
| }, null); |
| } |
| } |
| |
| if (typeof ruleConfig.exit === 'function') { |
| contextObject['Program:exit'] = () => { |
| const ste = /** @type {StateObject} */ (state); |
| |
| // @ts-expect-error `utils` not needed at this point |
| /** @type {Required<RuleConfig>} */ (ruleConfig).exit({ |
| context, |
| settings, |
| state: ste, |
| }); |
| }; |
| } |
| |
| return contextObject; |
| }, |
| meta: ruleConfig.meta, |
| }; |
| } |
| |
| export { |
| parseComment, |
| } from '@es-joy/jsdoccomment'; |