UNPKG

eslint-plugin-jsdoc

Version:
335 lines (325 loc) 14.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _iterateJsdoc = _interopRequireWildcard(require("../iterateJsdoc.cjs")); var _jsdoccomment = require("@es-joy/jsdoccomment"); var _parseImportsExports = require("parse-imports-exports"); var _toValidIdentifier = _interopRequireDefault(require("../to-valid-identifier.cjs")); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); } var _default = exports.default = (0, _iterateJsdoc.default)(({ context, indent, jsdoc, settings, sourceCode, utils }) => { const { mode } = settings; const { enableFixer = true, exemptTypedefs = true, outputType = 'namespaced-import' } = context.options[0] || {}; const allComments = sourceCode.getAllComments(); const comments = allComments.filter(comment => { return /^\*(?!\*)/v.test(comment.value); }).map(commentNode => { return (0, _jsdoccomment.commentParserToESTree)((0, _iterateJsdoc.parseComment)(commentNode, ''), mode === 'permissive' ? 'typescript' : mode); }); const typedefs = comments.flatMap(doc => { return doc.tags.filter(({ tag }) => { return utils.isNameOrNamepathDefiningTag(tag); }); }); const imports = comments.flatMap(doc => { return doc.tags.filter(({ tag }) => { return tag === 'import'; }); }).map(tag => { // Causes problems with stringification otherwise tag.delimiter = ''; return tag; }); /** * @param {import('@es-joy/jsdoccomment').JsdocTagWithInline} tag */ const iterateInlineImports = tag => { const potentialType = tag.type; let parsedType; try { parsedType = mode === 'permissive' ? (0, _jsdoccomment.tryParse)(/** @type {string} */potentialType) : (0, _jsdoccomment.parse)(/** @type {string} */potentialType, mode); } catch { return; } (0, _jsdoccomment.traverse)(parsedType, (nde, parentNode) => { // @ts-expect-error Adding our own property for use below nde.parentNode = parentNode; }); (0, _jsdoccomment.traverse)(parsedType, nde => { const { element, type } = /** @type {import('jsdoc-type-pratt-parser').ImportResult} */nde; if (type !== 'JsdocTypeImport') { return; } let currentNode = nde; /** @type {string[]} */ const pathSegments = []; /** @type {import('jsdoc-type-pratt-parser').NamePathResult[]} */ const nodes = []; /** @type {string[]} */ const extraPathSegments = []; /** @type {(import('jsdoc-type-pratt-parser').QuoteStyle|undefined)[]} */ const quotes = []; const propertyOrBrackets = /** @type {import('jsdoc-type-pratt-parser').NamePathResult['pathType'][]} */[]; // @ts-expect-error Referencing our own property added above while (currentNode && currentNode.parentNode) { // @ts-expect-error Referencing our own property added above currentNode = currentNode.parentNode; /* c8 ignore next 3 -- Guard */ if (currentNode.type !== 'JsdocTypeNamePath') { break; } pathSegments.unshift(currentNode.right.type === 'JsdocTypeIndexedAccessIndex' ? (0, _jsdoccomment.stringify)(currentNode.right.right) : currentNode.right.value); nodes.unshift(currentNode); propertyOrBrackets.unshift(currentNode.pathType); quotes.unshift(currentNode.right.type === 'JsdocTypeIndexedAccessIndex' ? undefined : currentNode.right.meta.quote); } /** * @param {string} name * @param {string[]} extrPathSegments */ const getFixer = (name, extrPathSegments) => { const matchingName = (0, _toValidIdentifier.default)(name); return () => { /** @type {import('jsdoc-type-pratt-parser').NamePathResult|undefined} */ let node = nodes.at(0); if (!node) { // Not really a NamePathResult, but will be converted later anyways node = /** @type {import('jsdoc-type-pratt-parser').NamePathResult} */ /** @type {unknown} */ nde; } const keys = /** @type {(keyof import('jsdoc-type-pratt-parser').NamePathResult)[]} */ Object.keys(node); for (const key of keys) { delete node[key]; } if (extrPathSegments.length) { let newNode = /** @type {import('jsdoc-type-pratt-parser').NamePathResult} */ /** @type {unknown} */ node; while (extrPathSegments.length && newNode) { newNode.type = 'JsdocTypeNamePath'; newNode.right = { meta: { quote: quotes.shift() }, type: 'JsdocTypeProperty', value: (/** @type {string} */extrPathSegments.shift()) }; newNode.pathType = /** @type {import('jsdoc-type-pratt-parser').NamePathResult['pathType']} */ propertyOrBrackets.shift(); // @ts-expect-error Temporary newNode.left = {}; newNode = /** @type {import('jsdoc-type-pratt-parser').NamePathResult} */ newNode.left; } const nameNode = /** @type {import('jsdoc-type-pratt-parser').NameResult} */ /** @type {unknown} */ newNode; nameNode.type = 'JsdocTypeName'; nameNode.value = matchingName; } else { const newNode = /** @type {import('jsdoc-type-pratt-parser').NameResult} */ /** @type {unknown} */ node; newNode.type = 'JsdocTypeName'; newNode.value = matchingName; } for (const src of tag.source) { if (src.tokens.type) { src.tokens.type = `{${(0, _jsdoccomment.stringify)(parsedType)}}`; break; } } }; }; /** @type {string[]} */ let unusedPathSegments = []; const findMatchingTypedef = () => { // Don't want typedefs to find themselves if (!exemptTypedefs) { return undefined; } const pthSegments = [...pathSegments]; return typedefs.find(typedef => { let typedefNode = typedef.parsedType; let namepathMatch; while (typedefNode && typedefNode.type === 'JsdocTypeNamePath') { const pathSegment = pthSegments.shift(); if (!pathSegment) { namepathMatch = false; break; } if (typedefNode.right.type === 'JsdocTypeIndexedAccessIndex' && (0, _jsdoccomment.stringify)(typedefNode.right.right) !== pathSegment || typedefNode.right.type !== 'JsdocTypeIndexedAccessIndex' && typedefNode.right.value !== pathSegment) { if (namepathMatch === true) { // It stopped matching, so stop break; } extraPathSegments.push(pathSegment); namepathMatch = false; continue; } namepathMatch = true; unusedPathSegments = pthSegments; typedefNode = typedefNode.left; } return namepathMatch && // `import('eslint')` matches typedefNode && typedefNode.type === 'JsdocTypeImport' && typedefNode.element.value === element.value; }); }; // Check @typedef's first as should be longest match, allowing // for shorter abbreviations const matchingTypedef = findMatchingTypedef(); if (matchingTypedef) { utils.reportJSDoc('Inline `import()` found; using `@typedef`', tag, enableFixer ? getFixer(matchingTypedef.name, [...extraPathSegments, ...unusedPathSegments.slice(-1), ...unusedPathSegments.slice(0, -1)]) : null); return; } const findMatchingImport = () => { for (const imprt of imports) { const parsedImport = (0, _parseImportsExports.parseImportsExports)((0, _jsdoccomment.estreeToString)(imprt).replace(/^\s*@/v, '').trim()); const namedImportsModuleSpecifier = Object.keys(parsedImport.namedImports || {})[0]; const namedImports = Object.values(parsedImport.namedImports || {})[0]?.[0]; const namedImportNames = (namedImports && namedImports.names && Object.keys(namedImports.names)) ?? []; const namespaceImports = Object.values(parsedImport.namespaceImports || {})[0]?.[0]; const namespaceImportsDefault = namespaceImports && namespaceImports.default; const namespaceImportsNamespace = namespaceImports && namespaceImports.namespace; const namespaceImportsModuleSpecifier = Object.keys(parsedImport.namespaceImports || {})[0]; const lastPathSegment = pathSegments.at(-1); if (namespaceImportsDefault && namespaceImportsModuleSpecifier === element.value || element.value === namedImportsModuleSpecifier && (lastPathSegment && namedImportNames.includes(lastPathSegment) || lastPathSegment === 'default') || namespaceImportsNamespace && namespaceImportsModuleSpecifier === element.value) { return { namedImportNames, namedImports, namedImportsModuleSpecifier, namespaceImports, namespaceImportsDefault, namespaceImportsModuleSpecifier, namespaceImportsNamespace }; } } return undefined; }; const matchingImport = findMatchingImport(); if (matchingImport) { const { namedImportNames, namedImports, namedImportsModuleSpecifier, namespaceImportsNamespace } = matchingImport; if (!namedImportNames.length && namedImportsModuleSpecifier && namedImports.default) { utils.reportJSDoc('Inline `import()` found; prefer `@import`', tag, enableFixer ? getFixer(namedImports.default, []) : null); return; } const lastPthSegment = pathSegments.at(-1); if (lastPthSegment && namedImportNames.includes(lastPthSegment)) { utils.reportJSDoc('Inline `import()` found; prefer `@import`', tag, enableFixer ? getFixer(lastPthSegment, pathSegments.slice(0, -1)) : null); return; } if (namespaceImportsNamespace) { utils.reportJSDoc('Inline `import()` found; prefer `@import`', tag, enableFixer ? getFixer(namespaceImportsNamespace, [...pathSegments]) : null); return; } } if (!pathSegments.length) { utils.reportJSDoc('Inline `import()` found; prefer `@import`', tag, enableFixer ? fixer => { getFixer(element.value, [])(); const programNode = sourceCode.ast; const commentNodes = sourceCode.getCommentsBefore(programNode); return fixer.insertTextBefore( // @ts-expect-error Ok commentNodes[0] ?? programNode, `/** @import * as ${(0, _toValidIdentifier.default)(element.value)} from '${element.value}'; */${commentNodes[0] ? '\n' + indent : ''}`); } : null); return; } const lstPathSegment = pathSegments.at(-1); if (lstPathSegment && lstPathSegment === 'default') { utils.reportJSDoc('Inline `import()` found; prefer `@import`', tag, enableFixer ? fixer => { getFixer(element.value, [])(); const programNode = sourceCode.ast; const commentNodes = sourceCode.getCommentsBefore(programNode); return fixer.insertTextBefore( // @ts-expect-error Ok commentNodes[0] ?? programNode, `/** @import ${element.value} from '${element.value}'; */${commentNodes[0] ? '\n' + indent : ''}`); } : null); return; } utils.reportJSDoc('Inline `import()` found; prefer `@import`', tag, enableFixer ? fixer => { if (outputType === 'namespaced-import') { getFixer(element.value, [...pathSegments])(); } else { getFixer(/** @type {string} */pathSegments.at(-1), pathSegments.slice(0, -1))(); } const programNode = sourceCode.ast; const commentNodes = sourceCode.getCommentsBefore(programNode); return fixer.insertTextBefore( // @ts-expect-error Ok commentNodes[0] ?? programNode, outputType === 'namespaced-import' ? `/** @import * as ${(0, _toValidIdentifier.default)(element.value)} from '${element.value}'; */${commentNodes[0] ? '\n' + indent : ''}` : `/** @import { ${(0, _toValidIdentifier.default)(/* c8 ignore next -- TS */ pathSegments.at(-1) ?? '')} } from '${element.value}'; */${commentNodes[0] ? '\n' + indent : ''}`); } : null); }); }; for (const tag of jsdoc.tags) { const mightHaveTypePosition = utils.tagMightHaveTypePosition(tag.tag); const hasTypePosition = mightHaveTypePosition === true && Boolean(tag.type); if (hasTypePosition && (!exemptTypedefs || !utils.isNameOrNamepathDefiningTag(tag.tag))) { iterateInlineImports(tag); } } }, { iterateAllJsdocs: true, meta: { docs: { description: 'Prefer `@import` tags to inline `import()` statements.', url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/prefer-import-tag.md#repos-sticky-header' }, fixable: 'code', schema: [{ additionalProperties: false, properties: { enableFixer: { description: 'Whether or not to enable the fixer to add `@import` tags.', type: 'boolean' }, exemptTypedefs: { description: 'Whether to allow `import()` statements within `@typedef`', type: 'boolean' }, // We might add `typedef` and `typedef-local-only`, but also raises // question of how deep the generated typedef should be outputType: { description: 'What kind of `@import` to generate when no matching `@typedef` or `@import` is found', enum: ['named-import', 'namespaced-import'], type: 'string' } }, type: 'object' }], type: 'suggestion' } }); module.exports = exports.default; //# sourceMappingURL=preferImportTag.cjs.map