"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 _eslint = require("eslint");
var _parseImportsExports = require("parse-imports-exports");
var _semver = _interopRequireDefault(require("semver"));
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;
  });

  /**
   * Helper to insert an import statement at the top of the file.
   * @param {string} importText - The import statement text (without newline)
   * @returns {((fixer: import('eslint').Rule.RuleFixer) => import('eslint').Rule.Fix|void)} - Fixer function for ESLint
   */
  const createImportInserter = importText => {
    return fixer => {
      const programNode = sourceCode.ast;
      const commentNodes = sourceCode.getCommentsBefore(programNode);

      /* c8 ignore start -- ESLint < 10 */
      if (commentNodes[0]) {
        return fixer.insertTextBefore(
        // @ts-expect-error - Comment is compatible with Node in practice
        commentNodes[0], `${importText}\n${indent}`);
      }

      // For ESLint < 10, use the simple approach of inserting before programNode
      if (_semver.default.lt(_eslint.Linter.version, '10.0.0')) {
        return fixer.insertTextBefore(programNode, importText);
      }

      // For ESLint 10+, handle the changed Program node range
      const sourceText = sourceCode.getText();
      const hasCode = programNode.body && programNode.body.length > 0;
      const leadingMatch = hasCode ? sourceText.match(/^(\n[ \t]*)/v) : null;
      if (leadingMatch) {
        const leading = leadingMatch[1];
        return fixer.replaceTextRange([leading.length, leading.length], `${importText}\n${leading.slice(1)}`);
      }
      const startsWithNewline = sourceText[0] === '\n';
      return fixer.replaceTextRange(startsWithNewline ? [0, 1] : [0, 0], `${importText}\n`);
      /* c8 ignore end -- ESLint < 10 */
    };
  };

  /**
   * @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, [])();
          return createImportInserter(`/** @import * as ${(0, _toValidIdentifier.default)(element.value)} from '${element.value}'; */`)(fixer);
        } : null);
        return;
      }
      const lstPathSegment = pathSegments.at(-1);
      if (lstPathSegment && lstPathSegment === 'default') {
        utils.reportJSDoc('Inline `import()` found; prefer `@import`', tag, enableFixer ? fixer => {
          getFixer(element.value, [])();
          return createImportInserter(`/** @import ${element.value} from '${element.value}'; */`)(fixer);
        } : 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 importText = outputType === 'namespaced-import' ? `/** @import * as ${(0, _toValidIdentifier.default)(element.value)} from '${element.value}'; */` : `/** @import { ${(0, _toValidIdentifier.default)(/* c8 ignore next -- TS */
        pathSegments.at(-1) ?? '')} } from '${element.value}'; */`;
        return createImportInserter(importText)(fixer);
      } : 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