/**
 * @fileoverview Rule to enforce spacing before and after keywords.
 * @author Toru Nagashima
 * @deprecated in ESLint v8.53.0
 */

'use strict';

//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------

const astUtils = require('./utils/ast-utils'),
  keywords = require('./utils/keywords');

//------------------------------------------------------------------------------
// Constants
//------------------------------------------------------------------------------

const PREV_TOKEN = /^[)\]}>]$/u;
const NEXT_TOKEN = /^(?:[([{<~!]|\+\+?|--?)$/u;
const PREV_TOKEN_M = /^[)\]}>*]$/u;
const NEXT_TOKEN_M = /^[{*]$/u;
const TEMPLATE_OPEN_PAREN = /\$\{$/u;
const TEMPLATE_CLOSE_PAREN = /^\}/u;
const CHECK_TYPE =
  /^(?:JSXElement|RegularExpression|String|Template|PrivateIdentifier)$/u;
const KEYS = keywords.concat([
  'as',
  'async',
  'await',
  'from',
  'get',
  'let',
  'of',
  'set',
  'yield',
]);

// check duplications.
(function () {
  KEYS.sort();
  for (let i = 1; i < KEYS.length; ++i) {
    if (KEYS[i] === KEYS[i - 1]) {
      throw new Error(`Duplication was found in the keyword list: ${KEYS[i]}`);
    }
  }
})();

//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------

/**
 * Checks whether or not a given token is a "Template" token ends with "${".
 * @param {Token} token A token to check.
 * @returns {boolean} `true` if the token is a "Template" token ends with "${".
 */
function isOpenParenOfTemplate(token) {
  return token.type === 'Template' && TEMPLATE_OPEN_PAREN.test(token.value);
}

/**
 * Checks whether or not a given token is a "Template" token starts with "}".
 * @param {Token} token A token to check.
 * @returns {boolean} `true` if the token is a "Template" token starts with "}".
 */
function isCloseParenOfTemplate(token) {
  return token.type === 'Template' && TEMPLATE_CLOSE_PAREN.test(token.value);
}

//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------

/** @type {import('../types').Rule.RuleModule} */
module.exports = {
  meta: {
    deprecated: {
      message: 'Formatting rules are being moved out of ESLint core.',
      url: 'https://eslint.org/blog/2023/10/deprecating-formatting-rules/',
      deprecatedSince: '8.53.0',
      availableUntil: '10.0.0',
      replacedBy: [
        {
          message:
            'ESLint Stylistic now maintains deprecated stylistic core rules.',
          url: 'https://eslint.style/guide/migration',
          plugin: {
            name: '@stylistic/eslint-plugin-js',
            url: 'https://eslint.style/packages/js',
          },
          rule: {
            name: 'keyword-spacing',
            url: 'https://eslint.style/rules/js/keyword-spacing',
          },
        },
      ],
    },
    type: 'layout',

    docs: {
      description: 'Enforce consistent spacing before and after keywords',
      recommended: false,
      url: 'https://eslint.org/docs/latest/rules/keyword-spacing',
    },

    fixable: 'whitespace',

    schema: [
      {
        type: 'object',
        properties: {
          before: { type: 'boolean', default: true },
          after: { type: 'boolean', default: true },
          overrides: {
            type: 'object',
            properties: KEYS.reduce((retv, key) => {
              retv[key] = {
                type: 'object',
                properties: {
                  before: { type: 'boolean' },
                  after: { type: 'boolean' },
                },
                additionalProperties: false,
              };
              return retv;
            }, {}),
            additionalProperties: false,
          },
        },
        additionalProperties: false,
      },
    ],
    messages: {
      expectedBefore: 'Expected space(s) before "{{value}}".',
      expectedAfter: 'Expected space(s) after "{{value}}".',
      unexpectedBefore: 'Unexpected space(s) before "{{value}}".',
      unexpectedAfter: 'Unexpected space(s) after "{{value}}".',
    },
  },

  create(context) {
    const sourceCode = context.sourceCode;

    const tokensToIgnore = new WeakSet();

    /**
     * Reports a given token if there are not space(s) before the token.
     * @param {Token} token A token to report.
     * @param {RegExp} pattern A pattern of the previous token to check.
     * @returns {void}
     */
    function expectSpaceBefore(token, pattern) {
      const prevToken = sourceCode.getTokenBefore(token);

      if (
        prevToken &&
        (CHECK_TYPE.test(prevToken.type) || pattern.test(prevToken.value)) &&
        !isOpenParenOfTemplate(prevToken) &&
        !tokensToIgnore.has(prevToken) &&
        astUtils.isTokenOnSameLine(prevToken, token) &&
        !sourceCode.isSpaceBetweenTokens(prevToken, token)
      ) {
        context.report({
          loc: token.loc,
          messageId: 'expectedBefore',
          data: token,
          fix(fixer) {
            return fixer.insertTextBefore(token, ' ');
          },
        });
      }
    }

    /**
     * Reports a given token if there are space(s) before the token.
     * @param {Token} token A token to report.
     * @param {RegExp} pattern A pattern of the previous token to check.
     * @returns {void}
     */
    function unexpectSpaceBefore(token, pattern) {
      const prevToken = sourceCode.getTokenBefore(token);

      if (
        prevToken &&
        (CHECK_TYPE.test(prevToken.type) || pattern.test(prevToken.value)) &&
        !isOpenParenOfTemplate(prevToken) &&
        !tokensToIgnore.has(prevToken) &&
        astUtils.isTokenOnSameLine(prevToken, token) &&
        sourceCode.isSpaceBetweenTokens(prevToken, token)
      ) {
        context.report({
          loc: { start: prevToken.loc.end, end: token.loc.start },
          messageId: 'unexpectedBefore',
          data: token,
          fix(fixer) {
            return fixer.removeRange([prevToken.range[1], token.range[0]]);
          },
        });
      }
    }

    /**
     * Reports a given token if there are not space(s) after the token.
     * @param {Token} token A token to report.
     * @param {RegExp} pattern A pattern of the next token to check.
     * @returns {void}
     */
    function expectSpaceAfter(token, pattern) {
      const nextToken = sourceCode.getTokenAfter(token);

      if (
        nextToken &&
        (CHECK_TYPE.test(nextToken.type) || pattern.test(nextToken.value)) &&
        !isCloseParenOfTemplate(nextToken) &&
        !tokensToIgnore.has(nextToken) &&
        astUtils.isTokenOnSameLine(token, nextToken) &&
        !sourceCode.isSpaceBetweenTokens(token, nextToken)
      ) {
        context.report({
          loc: token.loc,
          messageId: 'expectedAfter',
          data: token,
          fix(fixer) {
            return fixer.insertTextAfter(token, ' ');
          },
        });
      }
    }

    /**
     * Reports a given token if there are space(s) after the token.
     * @param {Token} token A token to report.
     * @param {RegExp} pattern A pattern of the next token to check.
     * @returns {void}
     */
    function unexpectSpaceAfter(token, pattern) {
      const nextToken = sourceCode.getTokenAfter(token);

      if (
        nextToken &&
        (CHECK_TYPE.test(nextToken.type) || pattern.test(nextToken.value)) &&
        !isCloseParenOfTemplate(nextToken) &&
        !tokensToIgnore.has(nextToken) &&
        astUtils.isTokenOnSameLine(token, nextToken) &&
        sourceCode.isSpaceBetweenTokens(token, nextToken)
      ) {
        context.report({
          loc: { start: token.loc.end, end: nextToken.loc.start },
          messageId: 'unexpectedAfter',
          data: token,
          fix(fixer) {
            return fixer.removeRange([token.range[1], nextToken.range[0]]);
          },
        });
      }
    }

    /**
     * Parses the option object and determines check methods for each keyword.
     * @param {Object|undefined} options The option object to parse.
     * @returns {Object} - Normalized option object.
     *      Keys are keywords (there are for every keyword).
     *      Values are instances of `{"before": function, "after": function}`.
     */
    function parseOptions(options = {}) {
      const before = options.before !== false;
      const after = options.after !== false;
      const defaultValue = {
        before: before ? expectSpaceBefore : unexpectSpaceBefore,
        after: after ? expectSpaceAfter : unexpectSpaceAfter,
      };
      const overrides = (options && options.overrides) || {};
      const retv = Object.create(null);

      for (let i = 0; i < KEYS.length; ++i) {
        const key = KEYS[i];
        const override = overrides[key];

        if (override) {
          const thisBefore = 'before' in override ? override.before : before;
          const thisAfter = 'after' in override ? override.after : after;

          retv[key] = {
            before: thisBefore ? expectSpaceBefore : unexpectSpaceBefore,
            after: thisAfter ? expectSpaceAfter : unexpectSpaceAfter,
          };
        } else {
          retv[key] = defaultValue;
        }
      }

      return retv;
    }

    const checkMethodMap = parseOptions(context.options[0]);

    /**
     * Reports a given token if usage of spacing followed by the token is
     * invalid.
     * @param {Token} token A token to report.
     * @param {RegExp} [pattern] Optional. A pattern of the previous
     *      token to check.
     * @returns {void}
     */
    function checkSpacingBefore(token, pattern) {
      checkMethodMap[token.value].before(token, pattern || PREV_TOKEN);
    }

    /**
     * Reports a given token if usage of spacing preceded by the token is
     * invalid.
     * @param {Token} token A token to report.
     * @param {RegExp} [pattern] Optional. A pattern of the next
     *      token to check.
     * @returns {void}
     */
    function checkSpacingAfter(token, pattern) {
      checkMethodMap[token.value].after(token, pattern || NEXT_TOKEN);
    }

    /**
     * Reports a given token if usage of spacing around the token is invalid.
     * @param {Token} token A token to report.
     * @returns {void}
     */
    function checkSpacingAround(token) {
      checkSpacingBefore(token);
      checkSpacingAfter(token);
    }

    /**
     * Reports the first token of a given node if the first token is a keyword
     * and usage of spacing around the token is invalid.
     * @param {ASTNode|null} node A node to report.
     * @returns {void}
     */
    function checkSpacingAroundFirstToken(node) {
      const firstToken = node && sourceCode.getFirstToken(node);

      if (firstToken && firstToken.type === 'Keyword') {
        checkSpacingAround(firstToken);
      }
    }

    /**
     * Reports the first token of a given node if the first token is a keyword
     * and usage of spacing followed by the token is invalid.
     *
     * This is used for unary operators (e.g. `typeof`), `function`, and `super`.
     * Other rules are handling usage of spacing preceded by those keywords.
     * @param {ASTNode|null} node A node to report.
     * @returns {void}
     */
    function checkSpacingBeforeFirstToken(node) {
      const firstToken = node && sourceCode.getFirstToken(node);

      if (firstToken && firstToken.type === 'Keyword') {
        checkSpacingBefore(firstToken);
      }
    }

    /**
     * Reports the previous token of a given node if the token is a keyword and
     * usage of spacing around the token is invalid.
     * @param {ASTNode|null} node A node to report.
     * @returns {void}
     */
    function checkSpacingAroundTokenBefore(node) {
      if (node) {
        const token = sourceCode.getTokenBefore(node, astUtils.isKeywordToken);

        checkSpacingAround(token);
      }
    }

    /**
     * Reports `async` or `function` keywords of a given node if usage of
     * spacing around those keywords is invalid.
     * @param {ASTNode} node A node to report.
     * @returns {void}
     */
    function checkSpacingForFunction(node) {
      const firstToken = node && sourceCode.getFirstToken(node);

      if (
        firstToken &&
        ((firstToken.type === 'Keyword' && firstToken.value === 'function') ||
          firstToken.value === 'async')
      ) {
        checkSpacingBefore(firstToken);
      }
    }

    /**
     * Reports `class` and `extends` keywords of a given node if usage of
     * spacing around those keywords is invalid.
     * @param {ASTNode} node A node to report.
     * @returns {void}
     */
    function checkSpacingForClass(node) {
      checkSpacingAroundFirstToken(node);
      checkSpacingAroundTokenBefore(node.superClass);
    }

    /**
     * Reports `if` and `else` keywords of a given node if usage of spacing
     * around those keywords is invalid.
     * @param {ASTNode} node A node to report.
     * @returns {void}
     */
    function checkSpacingForIfStatement(node) {
      checkSpacingAroundFirstToken(node);
      checkSpacingAroundTokenBefore(node.alternate);
    }

    /**
     * Reports `try`, `catch`, and `finally` keywords of a given node if usage
     * of spacing around those keywords is invalid.
     * @param {ASTNode} node A node to report.
     * @returns {void}
     */
    function checkSpacingForTryStatement(node) {
      checkSpacingAroundFirstToken(node);
      checkSpacingAroundFirstToken(node.handler);
      checkSpacingAroundTokenBefore(node.finalizer);
    }

    /**
     * Reports `do` and `while` keywords of a given node if usage of spacing
     * around those keywords is invalid.
     * @param {ASTNode} node A node to report.
     * @returns {void}
     */
    function checkSpacingForDoWhileStatement(node) {
      checkSpacingAroundFirstToken(node);
      checkSpacingAroundTokenBefore(node.test);
    }

    /**
     * Reports `for` and `in` keywords of a given node if usage of spacing
     * around those keywords is invalid.
     * @param {ASTNode} node A node to report.
     * @returns {void}
     */
    function checkSpacingForForInStatement(node) {
      checkSpacingAroundFirstToken(node);

      const inToken = sourceCode.getTokenBefore(
        node.right,
        astUtils.isNotOpeningParenToken
      );
      const previousToken = sourceCode.getTokenBefore(inToken);

      if (previousToken.type !== 'PrivateIdentifier') {
        checkSpacingBefore(inToken);
      }

      checkSpacingAfter(inToken);
    }

    /**
     * Reports `for` and `of` keywords of a given node if usage of spacing
     * around those keywords is invalid.
     * @param {ASTNode} node A node to report.
     * @returns {void}
     */
    function checkSpacingForForOfStatement(node) {
      if (node.await) {
        checkSpacingBefore(sourceCode.getFirstToken(node, 0));
        checkSpacingAfter(sourceCode.getFirstToken(node, 1));
      } else {
        checkSpacingAroundFirstToken(node);
      }

      const ofToken = sourceCode.getTokenBefore(
        node.right,
        astUtils.isNotOpeningParenToken
      );
      const previousToken = sourceCode.getTokenBefore(ofToken);

      if (previousToken.type !== 'PrivateIdentifier') {
        checkSpacingBefore(ofToken);
      }

      checkSpacingAfter(ofToken);
    }

    /**
     * Reports `import`, `export`, `as`, and `from` keywords of a given node if
     * usage of spacing around those keywords is invalid.
     *
     * This rule handles the `*` token in module declarations.
     *
     *     import*as A from "./a"; /*error Expected space(s) after "import".
     *                               error Expected space(s) before "as".
     * @param {ASTNode} node A node to report.
     * @returns {void}
     */
    function checkSpacingForModuleDeclaration(node) {
      const firstToken = sourceCode.getFirstToken(node);

      checkSpacingBefore(firstToken, PREV_TOKEN_M);
      checkSpacingAfter(firstToken, NEXT_TOKEN_M);

      if (node.type === 'ExportDefaultDeclaration') {
        checkSpacingAround(sourceCode.getTokenAfter(firstToken));
      }

      if (node.type === 'ExportAllDeclaration' && node.exported) {
        const asToken = sourceCode.getTokenBefore(node.exported);

        checkSpacingBefore(asToken, PREV_TOKEN_M);
        checkSpacingAfter(asToken, NEXT_TOKEN_M);
      }

      if (node.source) {
        const fromToken = sourceCode.getTokenBefore(node.source);

        checkSpacingBefore(fromToken, PREV_TOKEN_M);
        checkSpacingAfter(fromToken, NEXT_TOKEN_M);
      }
    }

    /**
     * Reports `as` keyword of a given node if usage of spacing around this
     * keyword is invalid.
     * @param {ASTNode} node An `ImportSpecifier` node to check.
     * @returns {void}
     */
    function checkSpacingForImportSpecifier(node) {
      if (node.imported.range[0] !== node.local.range[0]) {
        const asToken = sourceCode.getTokenBefore(node.local);

        checkSpacingBefore(asToken, PREV_TOKEN_M);
      }
    }

    /**
     * Reports `as` keyword of a given node if usage of spacing around this
     * keyword is invalid.
     * @param {ASTNode} node An `ExportSpecifier` node to check.
     * @returns {void}
     */
    function checkSpacingForExportSpecifier(node) {
      if (node.local.range[0] !== node.exported.range[0]) {
        const asToken = sourceCode.getTokenBefore(node.exported);

        checkSpacingBefore(asToken, PREV_TOKEN_M);
        checkSpacingAfter(asToken, NEXT_TOKEN_M);
      }
    }

    /**
     * Reports `as` keyword of a given node if usage of spacing around this
     * keyword is invalid.
     * @param {ASTNode} node A node to report.
     * @returns {void}
     */
    function checkSpacingForImportNamespaceSpecifier(node) {
      const asToken = sourceCode.getFirstToken(node, 1);

      checkSpacingBefore(asToken, PREV_TOKEN_M);
    }

    /**
     * Reports `static`, `get`, and `set` keywords of a given node if usage of
     * spacing around those keywords is invalid.
     * @param {ASTNode} node A node to report.
     * @throws {Error} If unable to find token get, set, or async beside method name.
     * @returns {void}
     */
    function checkSpacingForProperty(node) {
      if (node.static) {
        checkSpacingAroundFirstToken(node);
      }
      if (
        node.kind === 'get' ||
        node.kind === 'set' ||
        ((node.method || node.type === 'MethodDefinition') && node.value.async)
      ) {
        const token = sourceCode.getTokenBefore(node.key, (tok) => {
          switch (tok.value) {
            case 'get':
            case 'set':
            case 'async':
              return true;
            default:
              return false;
          }
        });

        if (!token) {
          throw new Error(
            'Failed to find token get, set, or async beside method name'
          );
        }

        checkSpacingAround(token);
      }
    }

    /**
     * Reports `await` keyword of a given node if usage of spacing before
     * this keyword is invalid.
     * @param {ASTNode} node A node to report.
     * @returns {void}
     */
    function checkSpacingForAwaitExpression(node) {
      checkSpacingBefore(sourceCode.getFirstToken(node));
    }

    return {
      // Statements
      DebuggerStatement: checkSpacingAroundFirstToken,
      WithStatement: checkSpacingAroundFirstToken,

      // Statements - Control flow
      BreakStatement: checkSpacingAroundFirstToken,
      ContinueStatement: checkSpacingAroundFirstToken,
      ReturnStatement: checkSpacingAroundFirstToken,
      ThrowStatement: checkSpacingAroundFirstToken,
      TryStatement: checkSpacingForTryStatement,

      // Statements - Choice
      IfStatement: checkSpacingForIfStatement,
      SwitchStatement: checkSpacingAroundFirstToken,
      SwitchCase: checkSpacingAroundFirstToken,

      // Statements - Loops
      DoWhileStatement: checkSpacingForDoWhileStatement,
      ForInStatement: checkSpacingForForInStatement,
      ForOfStatement: checkSpacingForForOfStatement,
      ForStatement: checkSpacingAroundFirstToken,
      WhileStatement: checkSpacingAroundFirstToken,

      // Statements - Declarations
      ClassDeclaration: checkSpacingForClass,
      ExportNamedDeclaration: checkSpacingForModuleDeclaration,
      ExportDefaultDeclaration: checkSpacingForModuleDeclaration,
      ExportAllDeclaration: checkSpacingForModuleDeclaration,
      FunctionDeclaration: checkSpacingForFunction,
      ImportDeclaration: checkSpacingForModuleDeclaration,
      VariableDeclaration: checkSpacingAroundFirstToken,

      // Expressions
      ArrowFunctionExpression: checkSpacingForFunction,
      AwaitExpression: checkSpacingForAwaitExpression,
      ClassExpression: checkSpacingForClass,
      FunctionExpression: checkSpacingForFunction,
      NewExpression: checkSpacingBeforeFirstToken,
      Super: checkSpacingBeforeFirstToken,
      ThisExpression: checkSpacingBeforeFirstToken,
      UnaryExpression: checkSpacingBeforeFirstToken,
      YieldExpression: checkSpacingBeforeFirstToken,

      // Others
      ImportSpecifier: checkSpacingForImportSpecifier,
      ExportSpecifier: checkSpacingForExportSpecifier,
      ImportNamespaceSpecifier: checkSpacingForImportNamespaceSpecifier,
      MethodDefinition: checkSpacingForProperty,
      PropertyDefinition: checkSpacingForProperty,
      StaticBlock: checkSpacingAroundFirstToken,
      Property: checkSpacingForProperty,

      // To avoid conflicts with `space-infix-ops`, e.g. `a > this.b`
      "BinaryExpression[operator='>']"(node) {
        const operatorToken = sourceCode.getTokenBefore(
          node.right,
          astUtils.isNotOpeningParenToken
        );

        tokensToIgnore.add(operatorToken);
      },
    };
  },
};