Skip to content

⬅️ Back to Table of Contents

📄 prefer-regexp-exec

📊 Analysis Summary

Metric Count
🔧 Functions 5
📦 Imports 8
🎯 Enums 1

📚 Table of Contents

🛠️ File Location:

📂 packages/eslint-plugin/src/rules/prefer-regexp-exec.ts

📤 Default Export

export default createRule({ ... })
Property Value
name 'prefer-regexp-exec'
meta.type 'suggestion'
meta.docs.description 'Enforce RegExp#exec over String#match if no global flag is provided'
meta.docs.recommended 'stylistic'
meta.docs.requiresTypeChecking true
meta.fixable 'code'
meta.messages.regExpExecOverStringMatch 'Use the RegExp#exec() method instead.'
meta.schema []
defaultOptions []

Entry point: create — documented under Functions.


📦 Imports

Name Source
TSESTree @typescript-eslint/utils
AST_NODE_TYPES @typescript-eslint/utils
createRule ../util
getParserServices ../util
getStaticValue ../util
getTypeName ../util
getWrappingFixer ../util
isStaticMemberAccessOfValue ../util

Functions

create(context: any): { 'CallExpression[arguments.length=1] > MemberExpression'(m…

Parameters:

  • context any

Returns: { 'CallExpression[arguments.length=1] > MemberExpression'(memberNode: TSESTree.MemberExpression): void; }

Calls:

  • context.sourceCode.getScope
  • getParserServices (from ../util)
  • services.program.getTypeChecker
  • getTypeName (from ../util)
  • isRegExpType
  • isStringType
  • node.arguments.at
  • getStaticValue (from ../util)
  • flagsValue.value.includes
  • isStaticMemberAccessOfValue (from ../util)
  • services.getTypeAtLocation
  • definitelyDoesNotContainGlobalFlag
  • argumentValue.value.flags.includes
  • RegExp
  • context.report
  • getWrappingFixer (from ../util)
  • regExp.toString
  • collectArgumentTypes
  • tsutils.unionConstituents

Internal Comments:

/**
     * Check if a given node type is a string.
     * @param type The node type to check.
     */
/**
     * Check if a given node type is a RegExp.
     * @param type The node type to check.
     */
/**
     * Returns true if and only if we have syntactic proof that the /g flag is
     * absent. Returns false in all other cases (i.e. it still might or might
     * not contain the global flag).
     */
// Don't report regular expressions with global flag.

Code
create(context) {
    const globalScope = context.sourceCode.getScope(context.sourceCode.ast);
    const services = getParserServices(context);
    const checker = services.program.getTypeChecker();

    /**
     * Check if a given node type is a string.
     * @param type The node type to check.
     */
    function isStringType(type: ts.Type): boolean {
      return getTypeName(checker, type) === 'string';
    }

    /**
     * Check if a given node type is a RegExp.
     * @param type The node type to check.
     */
    function isRegExpType(type: ts.Type): boolean {
      return getTypeName(checker, type) === 'RegExp';
    }

    function collectArgumentTypes(types: ts.Type[]): ArgumentType {
      let result = ArgumentType.Other;

      for (const type of types) {
        if (isRegExpType(type)) {
          result |= ArgumentType.RegExp;
        } else if (isStringType(type)) {
          result |= ArgumentType.String;
        }
      }

      return result;
    }

    /**
     * Returns true if and only if we have syntactic proof that the /g flag is
     * absent. Returns false in all other cases (i.e. it still might or might
     * not contain the global flag).
     */
    function definitelyDoesNotContainGlobalFlag(
      node: TSESTree.CallExpressionArgument,
    ): boolean {
      if (
        (node.type === AST_NODE_TYPES.CallExpression ||
          node.type === AST_NODE_TYPES.NewExpression) &&
        node.callee.type === AST_NODE_TYPES.Identifier &&
        node.callee.name === 'RegExp'
      ) {
        const flags = node.arguments.at(1);

        if (!flags) {
          return true;
        }

        const flagsValue = getStaticValue(flags, globalScope);

        return (
          !!flagsValue &&
          (typeof flagsValue.value !== 'string' ||
            !flagsValue.value.includes('g'))
        );
      }

      return false;
    }

    return {
      'CallExpression[arguments.length=1] > MemberExpression'(
        memberNode: TSESTree.MemberExpression,
      ): void {
        if (!isStaticMemberAccessOfValue(memberNode, context, 'match')) {
          return;
        }
        const objectNode = memberNode.object;
        const callNode = memberNode.parent as TSESTree.CallExpression;
        const [argumentNode] = callNode.arguments;
        const argumentValue = getStaticValue(argumentNode, globalScope);

        if (!isStringType(services.getTypeAtLocation(objectNode))) {
          return;
        }

        // Don't report regular expressions with global flag.
        if (
          (!argumentValue &&
            !definitelyDoesNotContainGlobalFlag(argumentNode)) ||
          (argumentValue &&
            argumentValue.value instanceof RegExp &&
            argumentValue.value.flags.includes('g'))
        ) {
          return;
        }

        if (
          argumentNode.type === AST_NODE_TYPES.Literal &&
          typeof argumentNode.value === 'string'
        ) {
          let regExp: RegExp;
          try {
            regExp = RegExp(argumentNode.value);
          } catch {
            return;
          }
          return context.report({
            node: memberNode.property,
            messageId: 'regExpExecOverStringMatch',
            fix: getWrappingFixer({
              node: callNode,
              innerNode: [objectNode],
              sourceCode: context.sourceCode,
              wrap: objectCode => `${regExp.toString()}.exec(${objectCode})`,
            }),
          });
        }

        const argumentType = services.getTypeAtLocation(argumentNode);
        const argumentTypes = collectArgumentTypes(
          tsutils.unionConstituents(argumentType),
        );
        switch (argumentTypes) {
          case ArgumentType.RegExp:
            return context.report({
              node: memberNode.property,
              messageId: 'regExpExecOverStringMatch',
              fix: getWrappingFixer({
                node: callNode,
                innerNode: [objectNode, argumentNode],
                sourceCode: context.sourceCode,
                wrap: (objectCode, argumentCode) =>
                  `${argumentCode}.exec(${objectCode})`,
              }),
            });

          case ArgumentType.String:
            return context.report({
              node: memberNode.property,
              messageId: 'regExpExecOverStringMatch',
              fix: getWrappingFixer({
                node: callNode,
                innerNode: [objectNode, argumentNode],
                sourceCode: context.sourceCode,
                wrap: (objectCode, argumentCode) =>
                  `RegExp(${argumentCode}).exec(${objectCode})`,
              }),
            });
        }
      },
    };
  }

Internal helpers

Declared inside another function in this file.

isStringType(type: ts.Type): boolean

Check if a given node type is a string.

Parameters:

  • type any: The node type to check.
Raw JSDoc
/**
     * Check if a given node type is a string.
     * @param type The node type to check.
     */

Calls:

  • getTypeName (from ../util)
Code
function isStringType(type: ts.Type): boolean {
      return getTypeName(checker, type) === 'string';
    }

isRegExpType(type: ts.Type): boolean

Check if a given node type is a RegExp.

Parameters:

  • type any: The node type to check.
Raw JSDoc
/**
     * Check if a given node type is a RegExp.
     * @param type The node type to check.
     */

Calls:

  • getTypeName (from ../util)
Code
function isRegExpType(type: ts.Type): boolean {
      return getTypeName(checker, type) === 'RegExp';
    }

collectArgumentTypes(types: ts.Type[]): ArgumentType

Parameters:

  • types ts.Type[]

Returns: ArgumentType

Calls:

  • isRegExpType
  • isStringType
Code
function collectArgumentTypes(types: ts.Type[]): ArgumentType {
      let result = ArgumentType.Other;

      for (const type of types) {
        if (isRegExpType(type)) {
          result |= ArgumentType.RegExp;
        } else if (isStringType(type)) {
          result |= ArgumentType.String;
        }
      }

      return result;
    }

definitelyDoesNotContainGlobalFlag(node: TSESTree.CallExpressionArgument): boolean

Returns true if and only if we have syntactic proof that the /g flag is absent. Returns false in all other cases (i.e. it still might or might not contain the global flag).

Raw JSDoc
/**
     * Returns true if and only if we have syntactic proof that the /g flag is
     * absent. Returns false in all other cases (i.e. it still might or might
     * not contain the global flag).
     */

Calls:

  • node.arguments.at
  • getStaticValue (from ../util)
  • flagsValue.value.includes
Code
function definitelyDoesNotContainGlobalFlag(
      node: TSESTree.CallExpressionArgument,
    ): boolean {
      if (
        (node.type === AST_NODE_TYPES.CallExpression ||
          node.type === AST_NODE_TYPES.NewExpression) &&
        node.callee.type === AST_NODE_TYPES.Identifier &&
        node.callee.name === 'RegExp'
      ) {
        const flags = node.arguments.at(1);

        if (!flags) {
          return true;
        }

        const flagsValue = getStaticValue(flags, globalScope);

        return (
          !!flagsValue &&
          (typeof flagsValue.value !== 'string' ||
            !flagsValue.value.includes('g'))
        );
      }

      return false;
    }

Enums

enum ArgumentType

Enum Code
enum ArgumentType {
  Other = 0,
  String = 1 << 0,
  RegExp = 1 << 1,
  Both = String | RegExp,
}

Members

Name Value Description
Other 0 not shown
String 1 << 0 not shown
RegExp 1 << 1 not shown
Both String \| RegExp not shown

Generated by Syntax Scribe