"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.default = void 0;

var _eslint = require("eslint");

var _iterateJsdoc = _interopRequireDefault(require("../iterateJsdoc"));

var _warnRemovedSettings = _interopRequireDefault(require("../warnRemovedSettings"));

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; }

function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }

function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }

function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _nonIterableRest(); }

function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance"); }

function _iterableToArrayLimit(arr, i) { if (!(Symbol.iterator in Object(arr) || Object.prototype.toString.call(arr) === "[object Arguments]")) { return; } var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; }

function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }

const zeroBasedLineIndexAdjust = -1;
const likelyNestedJSDocIndentSpace = 1;
const preTagSpaceLength = 1; // If a space is present, we should ignore it

const firstLinePrefixLength = preTagSpaceLength;
const hasCaptionRegex = /^\s*<caption>(.*?)<\/caption>/u;

const escapeStringRegexp = str => {
  return str.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
};

const countChars = (str, ch) => {
  return (str.match(new RegExp(escapeStringRegexp(ch), 'gu')) || []).length;
};

const getRegexFromString = regexString => {
  const match = regexString.match(/^\/(.*)\/([gimyus]*)$/u);
  let flags = 'u';
  let regex = regexString;

  if (match) {
    var _match = _slicedToArray(match, 3);

    regex = _match[1];
    flags = _match[2];

    if (!flags) {
      flags = 'u';
    }

    const uniqueFlags = [...new Set(flags)];
    flags = uniqueFlags.join('');
  }

  return new RegExp(regex, flags);
};

var _default = (0, _iterateJsdoc.default)(({
  report,
  utils,
  context,
  globalState
}) => {
  (0, _warnRemovedSettings.default)(context, 'check-examples');
  const tagName = utils.getPreferredTagName({
    tagName: 'example'
  });

  if (!utils.hasTag(tagName)) {
    return;
  }

  if (!globalState.has('checkExamples-matchingFileName')) {
    globalState.set('checkExamples-matchingFileName', new Map());
  }

  const matchingFileNameMap = globalState.get('checkExamples-matchingFileName');
  const options = context.options[0] || {};
  let _options$exampleCodeR = options.exampleCodeRegex,
      exampleCodeRegex = _options$exampleCodeR === void 0 ? null : _options$exampleCodeR,
      _options$rejectExampl = options.rejectExampleCodeRegex,
      rejectExampleCodeRegex = _options$rejectExampl === void 0 ? null : _options$rejectExampl;
  const _options$noDefaultExa = options.noDefaultExampleRules,
        noDefaultExampleRules = _options$noDefaultExa === void 0 ? false : _options$noDefaultExa,
        _options$checkEslintr = options.checkEslintrc,
        checkEslintrc = _options$checkEslintr === void 0 ? true : _options$checkEslintr,
        _options$matchingFile = options.matchingFileName,
        filename = _options$matchingFile === void 0 ? null : _options$matchingFile,
        _options$paddedIndent = options.paddedIndent,
        paddedIndent = _options$paddedIndent === void 0 ? 0 : _options$paddedIndent,
        _options$baseConfig = options.baseConfig,
        baseConfig = _options$baseConfig === void 0 ? {} : _options$baseConfig,
        configFile = options.configFile,
        _options$allowInlineC = options.allowInlineConfig,
        allowInlineConfig = _options$allowInlineC === void 0 ? true : _options$allowInlineC,
        _options$reportUnused = options.reportUnusedDisableDirectives,
        reportUnusedDisableDirectives = _options$reportUnused === void 0 ? true : _options$reportUnused,
        _options$captionRequi = options.captionRequired,
        captionRequired = _options$captionRequi === void 0 ? false : _options$captionRequi;
  let defaultFileName;

  if (!filename) {
    const jsFileName = context.getFilename();

    if (typeof jsFileName === 'string' && jsFileName.includes('.')) {
      defaultFileName = jsFileName.replace(/\..*?$/, '.md');
    } else {
      defaultFileName = 'dummy.md';
    }
  } // Make this configurable?


  const rulePaths = [];
  const rules = noDefaultExampleRules ? undefined : {
    // "always" newline rule at end unlikely in sample code
    'eol-last': 0,
    // Wouldn't generally expect example paths to resolve relative to JS file
    'import/no-unresolved': 0,
    // Snippets likely too short to always include import/export info
    'import/unambiguous': 0,
    // Unlikely to have inadvertent debugging within examples
    'no-console': 0,
    // Often wish to start `@example` code after newline; also may use
    //   empty lines for spacing
    'no-multiple-empty-lines': 0,
    // Many variables in examples will be `undefined`
    'no-undef': 0,
    // Common to define variables for clarity without always using them
    'no-unused-vars': 0,
    // See import/no-unresolved
    'node/no-missing-import': 0,
    'node/no-missing-require': 0,
    // Can generally look nicer to pad a little even if code imposes more stringency
    'padded-blocks': 0
  };

  if (exampleCodeRegex) {
    exampleCodeRegex = getRegexFromString(exampleCodeRegex);
  }

  if (rejectExampleCodeRegex) {
    rejectExampleCodeRegex = getRegexFromString(rejectExampleCodeRegex);
  }

  utils.forEachPreferredTag('example', (tag, targetTagName) => {
    let source = tag.description;
    const match = source.match(hasCaptionRegex);

    if (captionRequired && (!match || !match[1].trim())) {
      report('Caption is expected for examples.', null, tag);
    } // If we allow newlines in hasCaptionRegex, we should add to line count


    source = source.replace(hasCaptionRegex, '');

    if (exampleCodeRegex && !exampleCodeRegex.test(source) || rejectExampleCodeRegex && rejectExampleCodeRegex.test(source)) {
      return;
    }

    const sources = [];

    if (exampleCodeRegex) {
      let nonJSPrefacingCols = 0;
      let nonJSPrefacingLines = 0;
      let startingIndex = 0;
      let lastStringCount = 0;
      let exampleCode;
      exampleCodeRegex.lastIndex = 0;

      while ((exampleCode = exampleCodeRegex.exec(source)) !== null) {
        const _exampleCode = exampleCode,
              index = _exampleCode.index,
              n0 = _exampleCode[0],
              n1 = _exampleCode[1]; // Count anything preceding user regex match (can affect line numbering)

        const preMatch = source.slice(startingIndex, index);
        const preMatchLines = countChars(preMatch, '\n');
        const colDelta = preMatchLines ? preMatch.slice(preMatch.lastIndexOf('\n') + 1).length : preMatch.length;
        let nonJSPreface;
        let nonJSPrefaceLineCount;

        if (n1) {
          const idx = n0.indexOf(n1);
          nonJSPreface = n0.slice(0, idx);
          nonJSPrefaceLineCount = countChars(nonJSPreface, '\n');
        } else {
          nonJSPreface = '';
          nonJSPrefaceLineCount = 0;
        }

        nonJSPrefacingLines += lastStringCount + preMatchLines + nonJSPrefaceLineCount; // Ignore `preMatch` delta if newlines here

        if (nonJSPrefaceLineCount) {
          const charsInLastLine = nonJSPreface.slice(nonJSPreface.lastIndexOf('\n') + 1).length;
          nonJSPrefacingCols += charsInLastLine;
        } else {
          nonJSPrefacingCols += colDelta + nonJSPreface.length;
        }

        const string = n1 || n0;
        sources.push({
          nonJSPrefacingCols,
          nonJSPrefacingLines,
          string
        });
        startingIndex = exampleCodeRegex.lastIndex;
        lastStringCount = countChars(string, '\n');

        if (!exampleCodeRegex.global) {
          break;
        }
      }
    } else {
      sources.push({
        nonJSPrefacingCols: 0,
        nonJSPrefacingLines: 0,
        string: source
      });
    } // Todo: Make fixable
    // Todo: Fix whitespace indent


    const checkRules = function checkRules({
      nonJSPrefacingCols,
      nonJSPrefacingLines,
      string
    }) {
      const cliConfig = {
        allowInlineConfig,
        baseConfig,
        configFile,
        reportUnusedDisableDirectives,
        rulePaths,
        rules,
        useEslintrc: checkEslintrc
      };
      const cliConfigStr = JSON.stringify(cliConfig);
      const src = paddedIndent ? string.replace(new RegExp(`(^|\n) {${paddedIndent}}(?!$)`, 'gu'), '\n') : string; // Programmatic ESLint API: https://eslint.org/docs/developer-guide/nodejs-api

      const fileNameMapKey = filename ? 'a' + cliConfigStr + filename : 'b' + cliConfigStr + defaultFileName;
      const file = filename || defaultFileName;
      let cliFile;

      if (matchingFileNameMap.has(fileNameMapKey)) {
        cliFile = matchingFileNameMap.get(fileNameMapKey);
      } else {
        const cli = new _eslint.CLIEngine(cliConfig);
        let config;

        if (filename || checkEslintrc) {
          config = cli.getConfigForFile(file);
        } // We need a new instance to ensure that the rules that may only
        //  be available to `file` (if it has its own `.eslintrc`),
        //  will be defined.


        cliFile = new _eslint.CLIEngine({
          allowInlineConfig,
          baseConfig: _objectSpread({}, baseConfig, {}, config),
          configFile,
          reportUnusedDisableDirectives,
          rulePaths,
          rules,
          useEslintrc: false
        });
        matchingFileNameMap.set(fileNameMapKey, cliFile);
      }

      const _cliFile$executeOnTex = cliFile.executeOnText(src),
            _cliFile$executeOnTex2 = _slicedToArray(_cliFile$executeOnTex.results, 1),
            messages = _cliFile$executeOnTex2[0].messages; // NOTE: `tag.line` can be 0 if of form `/** @tag ... */`


      const codeStartLine = tag.line + nonJSPrefacingLines;
      const codeStartCol = likelyNestedJSDocIndentSpace;
      messages.forEach(({
        message,
        line,
        column,
        severity,
        ruleId
      }) => {
        const startLine = codeStartLine + line + zeroBasedLineIndexAdjust;
        const startCol = codeStartCol + ( // This might not work for line 0, but line 0 is unlikely for examples
        line <= 1 ? nonJSPrefacingCols + firstLinePrefixLength : preTagSpaceLength) + column;
        report('@' + targetTagName + ' ' + (severity === 2 ? 'error' : 'warning') + (ruleId ? ' (' + ruleId + ')' : '') + ': ' + message, null, {
          column: startCol,
          line: startLine
        });
      });
    };

    sources.forEach(checkRules);
  });
}, {
  iterateAllJsdocs: true,
  meta: {
    schema: [{
      additionalProperties: false,
      properties: {
        allowInlineConfig: {
          default: true,
          type: 'boolean'
        },
        baseConfig: {
          type: 'object'
        },
        captionRequired: {
          default: false,
          type: 'boolean'
        },
        checkEslintrc: {
          default: true,
          type: 'boolean'
        },
        configFile: {
          type: 'string'
        },
        exampleCodeRegex: {
          type: 'string'
        },
        matchingFileName: {
          type: 'string'
        },
        noDefaultExampleRules: {
          default: false,
          type: 'boolean'
        },
        paddedIndent: {
          default: 0,
          type: 'integer'
        },
        rejectExampleCodeRegex: {
          type: 'string'
        },
        reportUnusedDisableDirectives: {
          default: true,
          type: 'boolean'
        }
      },
      type: 'object'
    }],
    type: 'suggestion'
  },
  noTrim: true
});

exports.default = _default;
module.exports = exports.default;
//# sourceMappingURL=checkExamples.js.map