"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.buildRejectOrPreferRuleDefinition = void 0;
var _iterateJsdoc = _interopRequireDefault(require("./iterateJsdoc.cjs"));
var _jsdoccomment = require("@es-joy/jsdoccomment");
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
/**
 * Adjusts the parent type node `meta` for generic matches (or type node
 * `type` for `JsdocTypeAny`) and sets the type node `value`.
 * @param {string} type The actual type
 * @param {string} preferred The preferred type
 * @param {boolean} isGenericMatch
 * @param {string} typeNodeName
 * @param {import('jsdoc-type-pratt-parser').NonRootResult} node
 * @param {import('jsdoc-type-pratt-parser').NonRootResult|undefined} parentNode
 * @returns {void}
 */
const adjustNames = (type, preferred, isGenericMatch, typeNodeName, node, parentNode) => {
  let ret = preferred;
  if (isGenericMatch) {
    const parentMeta = /** @type {import('jsdoc-type-pratt-parser').GenericResult} */parentNode.meta;
    if (preferred === '[]') {
      parentMeta.brackets = 'square';
      parentMeta.dot = false;
      ret = 'Array';
    } else {
      const dotBracketEnd = preferred.match(/\.(?:<>)?$/v);
      if (dotBracketEnd) {
        parentMeta.brackets = 'angle';
        parentMeta.dot = true;
        ret = preferred.slice(0, -dotBracketEnd[0].length);
      } else {
        const bracketEnd = preferred.endsWith('<>');
        if (bracketEnd) {
          parentMeta.brackets = 'angle';
          parentMeta.dot = false;
          ret = preferred.slice(0, -2);
        } else if (parentMeta?.brackets === 'square' && (typeNodeName === '[]' || typeNodeName === 'Array')) {
          parentMeta.brackets = 'angle';
          parentMeta.dot = false;
        }
      }
    }
  } else if (type === 'JsdocTypeAny') {
    node.type = 'JsdocTypeName';
  }

  /** @type {import('jsdoc-type-pratt-parser').NameResult} */
  node.value = ret.replace(/(?:\.|<>|\.<>|\[\])$/v, '');

  // For bare pseudo-types like `<>`
  if (!ret) {
    /** @type {import('jsdoc-type-pratt-parser').NameResult} */node.value = typeNodeName;
  }
};

/**
 * @param {boolean} [upperCase]
 * @returns {string}
 */
const getMessage = upperCase => {
  return 'Use object shorthand or index signatures instead of ' + '`' + (upperCase ? 'O' : 'o') + 'bject`, e.g., `{[key: string]: string}`';
};

/**
 * @type {{
 *   message: string,
 *   replacement: false
 * }}
 */
const info = {
  message: getMessage(),
  replacement: false
};

/**
 * @type {{
 *   message: string,
 *   replacement: false
 * }}
 */
const infoUC = {
  message: getMessage(true),
  replacement: false
};

/**
 * @param {{
 *   checkNativeTypes?: import('./rules/checkTypes.js').CheckNativeTypes|null
 *   overrideSettings?: import('./iterateJsdoc.js').Settings['preferredTypes']|null,
 *   description?: string,
 *   schema?: import('eslint').Rule.RuleMetaData['schema'],
 *   typeName?: string,
 *   url?: string,
 * }} cfg
 * @returns {import('eslint').Rule.RuleModule}
 */
const buildRejectOrPreferRuleDefinition = ({
  checkNativeTypes = null,
  typeName,
  description = typeName ?? 'Reports types deemed invalid (customizable and with defaults, for preventing and/or recommending replacements).',
  overrideSettings = null,
  schema = [],
  url = 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/check-types.md#repos-sticky-header'
}) => {
  return (0, _iterateJsdoc.default)(({
    context,
    jsdocNode,
    report,
    settings,
    sourceCode,
    utils
  }) => {
    const jsdocTagsWithPossibleType = utils.filterTags(tag => {
      return Boolean(utils.tagMightHaveTypePosition(tag.tag));
    });
    const
    /**
     * @type {{
     *   preferredTypes: import('./iterateJsdoc.js').PreferredTypes,
     *   structuredTags: import('./iterateJsdoc.js').StructuredTags,
     *   mode: import('./jsdocUtils.js').ParserMode
     * }}
     */
    {
      mode,
      preferredTypes: preferredTypesOriginal,
      structuredTags
    } = overrideSettings ? {
      mode: settings.mode,
      preferredTypes: overrideSettings,
      structuredTags: {}
    } : settings;
    const injectObjectPreferredTypes = !overrideSettings && !('Object' in preferredTypesOriginal || 'object' in preferredTypesOriginal || 'object.<>' in preferredTypesOriginal || 'Object.<>' in preferredTypesOriginal || 'object<>' in preferredTypesOriginal);

    /** @type {import('./iterateJsdoc.js').PreferredTypes} */
    const typeToInject = mode === 'typescript' ? {
      Object: 'object',
      'object.<>': info,
      'Object.<>': infoUC,
      'object<>': info,
      'Object<>': infoUC
    } : {
      Object: 'object',
      'object.<>': 'Object<>',
      'Object.<>': 'Object<>',
      'object<>': 'Object<>'
    };

    /** @type {import('./iterateJsdoc.js').PreferredTypes} */
    const preferredTypes = {
      ...(injectObjectPreferredTypes ? typeToInject : {}),
      ...preferredTypesOriginal
    };
    const
    /**
     * @type {{
     *   noDefaults: boolean,
     *   unifyParentAndChildTypeChecks: boolean,
     *   exemptTagContexts: ({
     *     tag: string,
     *     types: true|string[]
     *   })[]
     * }}
     */
    {
      exemptTagContexts = [],
      noDefaults,
      unifyParentAndChildTypeChecks
    } = context.options[0] || {};

    /**
     * Gets information about the preferred type: whether there is a matching
     * preferred type, what the type is, and whether it is a match to a generic.
     * @param {string} _type Not currently in use
     * @param {string} typeNodeName
     * @param {import('jsdoc-type-pratt-parser').NonRootResult|undefined} parentNode
     * @param {string|undefined} property
     * @returns {[hasMatchingPreferredType: boolean, typeName: string, isGenericMatch: boolean]}
     */
    const getPreferredTypeInfo = (_type, typeNodeName, parentNode, property) => {
      let hasMatchingPreferredType = false;
      let isGenericMatch = false;
      let typName = typeNodeName;
      const isNameOfGeneric = parentNode !== undefined && parentNode.type === 'JsdocTypeGeneric' && property === 'left';
      const brackets = /** @type {import('jsdoc-type-pratt-parser').GenericResult} */parentNode?.meta?.brackets;
      const dot = /** @type {import('jsdoc-type-pratt-parser').GenericResult} */parentNode?.meta?.dot;
      if (brackets === 'angle') {
        const checkPostFixes = dot ? ['.', '.<>'] : ['<>'];
        isGenericMatch = checkPostFixes.some(checkPostFix => {
          const preferredType = preferredTypes?.[typeNodeName + checkPostFix];

          // Does `unifyParentAndChildTypeChecks` need to be checked here?
          if ((unifyParentAndChildTypeChecks || isNameOfGeneric || (/* c8 ignore next 2 -- If checking `unifyParentAndChildTypeChecks` */
          typeof preferredType === 'object' && preferredType?.unifyParentAndChildTypeChecks)) && preferredType !== undefined) {
            typName += checkPostFix;
            return true;
          }
          return false;
        });
      }
      if (!isGenericMatch && property && /** @type {import('jsdoc-type-pratt-parser').NonRootResult} */parentNode.type === 'JsdocTypeGeneric') {
        const checkPostFixes = dot ? ['.', '.<>'] : [brackets === 'angle' ? '<>' : '[]'];
        isGenericMatch = checkPostFixes.some(checkPostFix => {
          const preferredType = preferredTypes?.[checkPostFix];
          if (
          // Does `unifyParentAndChildTypeChecks` need to be checked here?
          (unifyParentAndChildTypeChecks || isNameOfGeneric || (/* c8 ignore next 2 -- If checking `unifyParentAndChildTypeChecks` */
          typeof preferredType === 'object' && preferredType?.unifyParentAndChildTypeChecks)) && preferredType !== undefined) {
            typName = checkPostFix;
            return true;
          }
          return false;
        });
      }
      const prefType = preferredTypes?.[typeNodeName];
      const directNameMatch = prefType !== undefined && !Object.values(preferredTypes).includes(typeNodeName);
      const specificUnify = typeof prefType === 'object' && prefType?.unifyParentAndChildTypeChecks;
      const unifiedSyntaxParentMatch = property && directNameMatch && (unifyParentAndChildTypeChecks || specificUnify);
      isGenericMatch = isGenericMatch || Boolean(unifiedSyntaxParentMatch);
      hasMatchingPreferredType = isGenericMatch || directNameMatch && !property;
      return [hasMatchingPreferredType, typName, isGenericMatch];
    };

    /**
     * Collect invalid type info.
     * @param {string} type
     * @param {string} value
     * @param {string} tagName
     * @param {string} nameInTag
     * @param {number} idx
     * @param {string|undefined} property
     * @param {import('jsdoc-type-pratt-parser').NonRootResult} node
     * @param {import('jsdoc-type-pratt-parser').NonRootResult|undefined} parentNode
     * @param {(string|false|undefined)[][]} invalidTypes
     * @returns {void}
     */
    const getInvalidTypes = (type, value, tagName, nameInTag, idx, property, node, parentNode, invalidTypes) => {
      let typeNodeName = type === 'JsdocTypeAny' ? '*' : value;
      const [hasMatchingPreferredType, typName, isGenericMatch] = getPreferredTypeInfo(type, typeNodeName, parentNode, property);
      let preferred;
      let types;
      if (hasMatchingPreferredType) {
        const preferredSetting = preferredTypes[typName];
        typeNodeName = typName === '[]' ? typName : typeNodeName;
        if (!preferredSetting) {
          invalidTypes.push([typeNodeName]);
        } else if (typeof preferredSetting === 'string') {
          preferred = preferredSetting;
          invalidTypes.push([typeNodeName, preferred]);
        } else if (preferredSetting && typeof preferredSetting === 'object') {
          const nextItem = preferredSetting.skipRootChecking && jsdocTagsWithPossibleType[idx + 1];
          if (!nextItem || !nextItem.name.startsWith(`${nameInTag}.`)) {
            preferred = preferredSetting.replacement;
            invalidTypes.push([typeNodeName, preferred, preferredSetting.message]);
          }
        } else {
          utils.reportSettings('Invalid `settings.jsdoc.preferredTypes`. Values must be falsy, a string, or an object.');
          return;
        }
      } else if (Object.entries(structuredTags).some(([tag, {
        type: typs
      }]) => {
        types = typs;
        return tag === tagName && Array.isArray(types) && !types.includes(typeNodeName);
      })) {
        invalidTypes.push([typeNodeName, types]);
      } else if (checkNativeTypes && !noDefaults && type === 'JsdocTypeName') {
        preferred = checkNativeTypes(preferredTypes, typeNodeName, preferred, parentNode, invalidTypes);
      }

      // For fixer
      if (preferred) {
        adjustNames(type, preferred, isGenericMatch, typeNodeName, node, parentNode);
      }
    };
    for (const [idx, jsdocTag] of jsdocTagsWithPossibleType.entries()) {
      /** @type {(string|false|undefined)[][]} */
      const invalidTypes = [];
      let typeAst;
      try {
        typeAst = mode === 'permissive' ? (0, _jsdoccomment.tryParse)(jsdocTag.type) : (0, _jsdoccomment.parse)(jsdocTag.type, mode);
      } catch {
        continue;
      }
      const {
        name: nameInTag,
        tag: tagName
      } = jsdocTag;
      (0, _jsdoccomment.traverse)(typeAst, (node, parentNode, property) => {
        const {
          type,
          value
        } =
        /**
         * @type {import('jsdoc-type-pratt-parser').NameResult}
         */
        node;
        if (!['JsdocTypeAny', 'JsdocTypeName'].includes(type)) {
          return;
        }
        getInvalidTypes(type, value, tagName, nameInTag, idx, property, node, parentNode, invalidTypes);
      });
      if (invalidTypes.length) {
        const fixedType = (0, _jsdoccomment.stringify)(typeAst);

        /**
         * @type {import('eslint').Rule.ReportFixer}
         */
        const fix = fixer => {
          return fixer.replaceText(jsdocNode, sourceCode.getText(jsdocNode).replace(`{${jsdocTag.type}}`, `{${fixedType}}`));
        };
        for (const [badType, preferredType = '', msg] of invalidTypes) {
          const tagValue = jsdocTag.name ? ` "${jsdocTag.name}"` : '';
          if (exemptTagContexts.some(({
            tag,
            types
          }) => {
            return tag === tagName && (types === true || types.includes(jsdocTag.type));
          })) {
            continue;
          }
          report(msg || `Invalid JSDoc @${tagName}${tagValue} type "${badType}"` + (preferredType ? '; ' : '.') + (preferredType ? `prefer: ${JSON.stringify(preferredType)}.` : ''), preferredType ? fix : null, jsdocTag, msg ? {
            tagName,
            tagValue
          } : undefined);
        }
      }
    }
  }, {
    iterateAllJsdocs: true,
    meta: {
      docs: {
        description,
        url
      },
      ...(!overrideSettings || Object.values(overrideSettings).some(os => {
        return os && typeof os === 'object' ? /* c8 ignore next -- Ok */
        os.replacement : typeof os === 'string';
      }) ? {
        fixable: 'code'
      } : {}),
      schema,
      type: 'suggestion'
    }
  });
};
exports.buildRejectOrPreferRuleDefinition = buildRejectOrPreferRuleDefinition;
//# sourceMappingURL=buildRejectOrPreferRuleDefinition.cjs.map