UNPKG

@es-joy/jsdoccomment

Version:

Maintained replacement for ESLint's deprecated SourceCode#getJSDocComment along with other jsdoc utilities

695 lines (629 loc) 18.1 kB
/* eslint-disable jsdoc/reject-any-type -- Todo */ /** * Obtained originally from {@link https://github.com/eslint/eslint/blob/master/lib/util/source-code.js#L313}. * * @license MIT */ /** * @typedef {import('eslint').AST.Token | import('estree').Comment | { * type: import('eslint').AST.TokenType|"Line"|"Block"|"Shebang", * range: [number, number], * value: string * }} Token */ /** * @typedef {import('eslint').Rule.Node| * import('@typescript-eslint/types').TSESTree.Node} ESLintOrTSNode */ /** * @typedef {number} int */ /** * Checks if the given token is a comment token or not. * * @param {Token} token - The token to check. * @returns {boolean} `true` if the token is a comment token. */ const isCommentToken = (token) => { return token.type === 'Line' || token.type === 'Block' || token.type === 'Shebang'; }; /** * @typedef {( * ESLintOrTSNode| * import('estree').Comment| * import('eslint').Rule.Node & {declaration?: any, decorators?: any[]} * )} DecoratedNode */ /** * @param {DecoratedNode} node * @returns {import('@typescript-eslint/types').TSESTree.Decorator|undefined} */ const getDecorator = (node) => { // @ts-expect-error -- Loose checking for decorator presence across node kinds return node?.declaration?.decorators?.[0] || // @ts-expect-error -- Loose checking node?.decorators?.[0] || // @ts-expect-error -- Loose checking node?.parent?.decorators?.[0]; }; /** * Check to see if it is a ES6 export declaration. * * @param {ESLintOrTSNode} astNode An AST node. * @returns {boolean} whether the given node represents an export declaration. * @private */ const looksLikeExport = function (astNode) { return astNode.type === 'ExportDefaultDeclaration' || astNode.type === 'ExportNamedDeclaration' || astNode.type === 'ExportAllDeclaration' || astNode.type === 'ExportSpecifier'; }; /** * @param {ESLintOrTSNode} astNode * @returns {ESLintOrTSNode} */ const getTSFunctionComment = function (astNode) { const {parent} = astNode; /* v8 ignore next 3 */ if (!parent) { return astNode; } const grandparent = parent.parent; /* v8 ignore next 3 */ if (!grandparent) { return astNode; } const greatGrandparent = grandparent.parent; const greatGreatGrandparent = greatGrandparent && greatGrandparent.parent; if (/** @type {ESLintOrTSNode} */ (parent).type !== 'TSTypeAnnotation') { if ( parent.type === 'TSTypeAliasDeclaration' && grandparent.type === 'ExportNamedDeclaration' ) { return grandparent; /* v8 ignore next 3 */ } return astNode; } switch (/** @type {ESLintOrTSNode} */ (grandparent).type) { // @ts-expect-error -- For `ClassProperty`. case 'PropertyDefinition': case 'ClassProperty': case 'TSDeclareFunction': case 'TSMethodSignature': case 'TSPropertySignature': return grandparent; case 'ArrowFunctionExpression': /* v8 ignore next 3 */ if (!greatGrandparent) { return astNode; } if ( greatGrandparent.type === 'VariableDeclarator' // && greatGreatGrandparent.parent.type === 'VariableDeclaration' ) { /* v8 ignore next 3 */ if (!greatGreatGrandparent || !greatGreatGrandparent.parent) { return astNode; } return greatGreatGrandparent.parent; /* v8 ignore next 2 */ } return astNode; case 'FunctionExpression': /* v8 ignore next 3 */ if (!greatGreatGrandparent) { return astNode; } if (greatGrandparent.type === 'MethodDefinition') { return greatGrandparent; } // Fallthrough default: /* v8 ignore next 3 */ if (grandparent.type !== 'Identifier') { return astNode; } } /* v8 ignore next 3 */ if (!greatGreatGrandparent) { return astNode; } switch (greatGrandparent.type) { case 'ArrowFunctionExpression': if ( greatGreatGrandparent.type === 'VariableDeclarator' && greatGreatGrandparent.parent.type === 'VariableDeclaration' ) { return greatGreatGrandparent.parent; } return astNode; case 'FunctionDeclaration': return greatGrandparent; case 'VariableDeclarator': if (greatGreatGrandparent.type === 'VariableDeclaration') { return greatGreatGrandparent; } /* v8 ignore next 2 */ // Fallthrough default: /* v8 ignore next 3 */ return astNode; } }; const invokedExpression = new Set( ['CallExpression', 'OptionalCallExpression', 'NewExpression'] ); const allowableCommentNode = new Set([ 'AssignmentPattern', 'VariableDeclaration', 'ExpressionStatement', 'MethodDefinition', 'Property', 'ObjectProperty', 'ClassProperty', 'PropertyDefinition', 'ExportDefaultDeclaration', 'ReturnStatement' ]); /** * @typedef {{ * maxLines: int, * minLines: int, * skipInvokedExpressionsForCommentFinding?: boolean, * [name: string]: any * }} Settings */ /** * Reduces the provided node to the appropriate node for evaluating * JSDoc comment status. * * @param {ESLintOrTSNode} node An AST node. * @param {import('eslint').SourceCode} sourceCode The ESLint SourceCode. * @param {Settings} [settings] * @returns {ESLintOrTSNode} The AST node that * can be evaluated for appropriate JSDoc comments. */ const getReducedASTNode = function (node, sourceCode, settings) { let {parent} = node; switch (/** @type {ESLintOrTSNode} */ (node).type) { case 'TSFunctionType': return getTSFunctionComment(node); case 'TSInterfaceDeclaration': case 'TSTypeAliasDeclaration': case 'TSEnumDeclaration': case 'ClassDeclaration': case 'FunctionDeclaration': /* v8 ignore next 3 */ if (!parent) { return node; } return looksLikeExport(parent) ? parent : node; case 'TSDeclareFunction': case 'ClassExpression': case 'ObjectExpression': case 'ArrowFunctionExpression': case 'TSEmptyBodyFunctionExpression': case 'FunctionExpression': /* v8 ignore next 3 */ if (!parent) { return node; } if ( !invokedExpression.has(parent.type) || settings?.skipInvokedExpressionsForCommentFinding ) { /** * @type {ESLintOrTSNode|Token|null} */ let token = node; do { token = sourceCode.getTokenBefore( /** @type {import('eslint').Rule.Node|import('eslint').AST.Token} */ ( token ), {includeComments: true} ); } while (token && token.type === 'Punctuator' && token.value === '('); if (token && token.type === 'Block') { return node; } if (sourceCode.getCommentsBefore( /** @type {import('eslint').Rule.Node} */ (node) ).length) { return node; } while ( !sourceCode.getCommentsBefore( /** @type {import('eslint').Rule.Node} */ (parent) ).length && !(/Function/v).test(parent.type) && !allowableCommentNode.has(parent.type) ) { ({parent} = parent); if (!parent) { break; } } if (parent && parent.type !== 'FunctionDeclaration' && parent.type !== 'Program' ) { if (parent.parent && parent.parent.type === 'ExportNamedDeclaration') { return parent.parent; } return parent; } } return node; default: return node; } }; /** * Checks for the presence of a JSDoc comment for the given node and returns it. * * @param {ESLintOrTSNode} astNode The AST node to get * the comment for. * @param {import('eslint').SourceCode} sourceCode * @param {{maxLines: int, minLines: int, [name: string]: any}} settings * @param {{nonJSDoc?: boolean}} [opts] * @returns {Token|null} The Block comment token containing the JSDoc comment * for the given node or null if not found. */ const findJSDocComment = (astNode, sourceCode, settings, opts = {}) => { const {nonJSDoc} = opts; const {minLines, maxLines} = settings; /** @type {ESLintOrTSNode|import('estree').Comment} */ let currentNode = astNode; let tokenBefore = null; let parenthesisToken = null; while (currentNode) { const decorator = getDecorator(currentNode); if (decorator) { const dec = /** @type {unknown} */ (decorator); currentNode = /** @type {import('eslint').Rule.Node} */ (dec); } tokenBefore = sourceCode.getTokenBefore( /** @type {import('eslint').Rule.Node} */ (currentNode), {includeComments: true} ); if ( tokenBefore && tokenBefore.type === 'Punctuator' && tokenBefore.value === '(' ) { parenthesisToken = tokenBefore; [tokenBefore] = sourceCode.getTokensBefore( /** @type {import('eslint').Rule.Node} */ (currentNode), { count: 2, includeComments: true } ); } if (!tokenBefore || !isCommentToken(tokenBefore)) { return null; } if (!nonJSDoc && tokenBefore.type === 'Line') { currentNode = tokenBefore; continue; } break; } /* v8 ignore next 3 */ if (!tokenBefore || !currentNode.loc || !tokenBefore.loc) { return null; } if ( ( (nonJSDoc && (tokenBefore.type !== 'Block' || !(/^\*\s/v).test(tokenBefore.value))) || (!nonJSDoc && tokenBefore.type === 'Block' && (/^\*\s/v).test(tokenBefore.value)) ) && currentNode.loc.start.line - ( /** @type {import('eslint').AST.Token} */ (parenthesisToken ?? tokenBefore) ).loc.end.line >= minLines && currentNode.loc.start.line - ( /** @type {import('eslint').AST.Token} */ (parenthesisToken ?? tokenBefore) ).loc.end.line <= maxLines ) { return tokenBefore; } return null; }; const overloadMethodNode = new Set([ 'MethodDefinition', 'TSAbstractMethodDefinition' ]); const overloadStatementListNode = new Set([ 'BlockStatement', 'Program', 'StaticBlock', 'TSModuleBlock' ]); /** * @param {ESLintOrTSNode|undefined} node * @returns {ESLintOrTSNode[]|undefined} */ const getOverloadStatementSiblings = (node) => { if (node && overloadStatementListNode.has(node.type)) { return /** @type {ESLintOrTSNode[]} */ (node.body); } return undefined; }; /** * @param {ESLintOrTSNode} node * @returns {{ * bodyless: boolean, * kind: string, * name: string, * static: boolean * }|null} */ const getMethodOverloadInfo = (node) => { if (!overloadMethodNode.has(node.type)) { return null; } /** * @type {{ * computed?: boolean, * key?: {name?: string}, * kind?: string, * static?: boolean, * value?: {type?: string} * }} */ // @ts-expect-error -- Loose method-shape check after node.type guard. const method = node; if (method.computed || method.kind !== 'method' || !method.key?.name) { return null; } return { bodyless: node.type === 'TSAbstractMethodDefinition' || method.value?.type === 'TSEmptyBodyFunctionExpression', kind: method.kind, name: method.key.name, static: Boolean(method.static) }; }; /** * @param {ESLintOrTSNode} node * @param {ESLintOrTSNode} prevSibling * @returns {boolean} */ const isMatchingMethodOverloadSibling = (node, prevSibling) => { const current = getMethodOverloadInfo(node); const previous = getMethodOverloadInfo(prevSibling); return Boolean( current && previous?.bodyless && previous.name === current.name && previous.kind === current.kind && previous.static === current.static ); }; /** * @param {ESLintOrTSNode} node * @returns {string|undefined} */ const getCurrentOverloadName = (node) => { if (node.type === 'TSDeclareFunction' || node.type === 'FunctionDeclaration') { return /** @type {{id?: {name?: string}}} */ (node).id?.name; } if (node.type === 'ExportNamedDeclaration') { const {declaration} = /** @type {{declaration?: {type?: string, id?: {name?: string}}}} */ ( node ); if (declaration?.type === 'FunctionDeclaration' || declaration?.type === 'TSDeclareFunction') { return declaration.id?.name; } } if (overloadMethodNode.has(node.type)) { const method = /** @type {{computed?: boolean, key?: {name?: string}}} */ (node); if (!method.computed) { return method.key?.name; } } return undefined; }; /** * @param {ESLintOrTSNode} node * @returns {string|undefined} */ const getPreviousOverloadName = (node) => { if (node.type === 'TSDeclareFunction') { return /** @type {{id?: {name?: string}}} */ (node).id?.name; } if (node.type === 'ExportNamedDeclaration') { const {declaration} = /** @type {{declaration?: {type?: string, id?: {name?: string}}}} */ ( node ); if (declaration?.type === 'TSDeclareFunction') { return declaration.id?.name; } } if (overloadMethodNode.has(node.type)) { const method = /** @type {{computed?: boolean, key?: {name?: string}}} */ (node); if (!method.computed) { return method.key?.name; } } return undefined; }; /** * @param {ESLintOrTSNode} node * @returns {ESLintOrTSNode|null} */ const getPreviousOverloadSibling = (node) => { const {parent} = node; let childNode = node; /** @type {ESLintOrTSNode[]|undefined} */ let siblings; if ( overloadMethodNode.has(node.type) && parent?.type === 'ClassBody' ) { siblings = /** @type {ESLintOrTSNode[]} */ (parent.body); } else if (parent?.type === 'ExportNamedDeclaration') { childNode = parent; siblings = getOverloadStatementSiblings(parent.parent); } else { siblings = getOverloadStatementSiblings(parent); } if (!siblings) { return null; } const idx = siblings.indexOf(childNode); return idx > 0 ? siblings[idx - 1] : null; }; /** * Retrieves the JSDoc comment for a given node. * * @param {import('eslint').SourceCode} sourceCode The ESLint SourceCode * @param {ESLintOrTSNode} node The AST node to get * the comment for. * @param {Settings} settings The settings in context * @param {{checkOverloads?: boolean}} [opts] * @returns {Token|null} The Block comment * token containing the JSDoc comment for the given node or * null if not found. * @public */ const getJSDocComment = function (sourceCode, node, settings, opts = {}) { const reducedNode = getReducedASTNode(node, sourceCode, settings); const comment = findJSDocComment(reducedNode, sourceCode, settings); if (!comment && opts.checkOverloads) { const functionName = getCurrentOverloadName(reducedNode); const prevSibling = getPreviousOverloadSibling(reducedNode); if ( prevSibling && functionName && getPreviousOverloadName(prevSibling) === functionName && ( !overloadMethodNode.has(reducedNode.type) || isMatchingMethodOverloadSibling(reducedNode, prevSibling) ) ) { return getJSDocComment(sourceCode, prevSibling, settings, opts); } } return comment; }; /** * Retrieves the comment preceding a given node. * * @param {import('eslint').SourceCode} sourceCode The ESLint SourceCode * @param {ESLintOrTSNode} node The AST node to get * the comment for. * @param {{maxLines: int, minLines: int, [name: string]: any}} settings The * settings in context * @returns {Token|null} The Block comment * token containing the JSDoc comment for the given node or * null if not found. * @public */ const getNonJsdocComment = function (sourceCode, node, settings) { const reducedNode = getReducedASTNode(node, sourceCode, settings); return findJSDocComment(reducedNode, sourceCode, settings, { nonJSDoc: true }); }; /** * @param {ESLintOrTSNode|import('eslint').AST.Token| * import('estree').Comment * } nodeA The AST node or token to compare * @param {ESLintOrTSNode|import('eslint').AST.Token| * import('estree').Comment} nodeB The * AST node or token to compare */ const compareLocEndToStart = (nodeA, nodeB) => { /* v8 ignore next */ return (nodeA.loc?.end.line ?? 0) === (nodeB.loc?.start.line ?? 0); }; /** * Checks for the presence of a comment following the given node and * returns it. * * This method is experimental. * * @param {import('eslint').SourceCode} sourceCode * @param {ESLintOrTSNode} astNode The AST node to get * the comment for. * @returns {Token|null} The comment token containing the comment * for the given node or null if not found. */ const getFollowingComment = function (sourceCode, astNode) { /** * @param {ESLintOrTSNode} node The * AST node to get the comment for. */ const getTokensAfterIgnoringSemis = (node) => { let tokenAfter = sourceCode.getTokenAfter( /** @type {import('eslint').Rule.Node} */ (node), {includeComments: true} ); while ( tokenAfter && tokenAfter.type === 'Punctuator' && // tokenAfter.value === ')' // Don't apparently need to ignore tokenAfter.value === ';' ) { [tokenAfter] = sourceCode.getTokensAfter(tokenAfter, { includeComments: true }); } return tokenAfter; }; /** * @param {ESLintOrTSNode} node The * AST node to get the comment for. */ const tokenAfterIgnoringSemis = (node) => { const tokenAfter = getTokensAfterIgnoringSemis(node); return ( tokenAfter && isCommentToken(tokenAfter) && compareLocEndToStart(node, tokenAfter) ) ? tokenAfter : null; }; let tokenAfter = tokenAfterIgnoringSemis(astNode); if (!tokenAfter) { switch (astNode.type) { case 'FunctionDeclaration': tokenAfter = tokenAfterIgnoringSemis( /** @type {ESLintOrTSNode} */ (astNode.body) ); break; case 'ExpressionStatement': tokenAfter = tokenAfterIgnoringSemis( /** @type {ESLintOrTSNode} */ (astNode.expression) ); break; /* v8 ignore next 3 */ default: break; } } return tokenAfter; }; export { getReducedASTNode, getJSDocComment, getNonJsdocComment, getDecorator, findJSDocComment, getFollowingComment };