Skip to content

⬅️ Back to Table of Contents

📄 prefer-includes

📊 Analysis Summary

Metric Count
🔧 Functions 8
📦 Imports 9

📚 Table of Contents

🛠️ File Location:

📂 packages/eslint-plugin/src/rules/prefer-includes.ts

📤 Default Export

export default createRule({ ... })
Property Value
name 'prefer-includes'
meta.type 'suggestion'
meta.docs.description 'Enforce includes method over indexOf method'
meta.docs.recommended 'stylistic'
meta.docs.requiresTypeChecking true
meta.fixable 'code'
meta.messages.preferIncludes "Use 'includes()' method instead."
meta.messages.preferStringIncludes 'Use String#includes() method with a string instead.'
meta.schema []
defaultOptions []

Entry point: create — documented under Functions.


📦 Imports

Name Source
TSESLint @typescript-eslint/utils
TSESTree @typescript-eslint/utils
parseRegExpLiteral @eslint-community/regexpp
AST_NODE_TYPES @typescript-eslint/utils
createRule ../util
getConstrainedTypeAtLocation ../util
getParserServices ../util
getStaticValue ../util
isStaticMemberAccessOfValue ../util

Functions

create(context: any): { 'BinaryExpression > CallExpression.left > MemberExpressio…

Parameters:

  • context any

Returns: { 'BinaryExpression > CallExpression.left > MemberExpression'(node: TSESTree.MemberExpression): void; 'BinaryExpression > ChainExpression.left > CallExpression > MemberExpression'(node: TSESTree.MemberExpression): void; 'CallExpression[arguments.length=1] > MemberExpression.callee[property.name="test"][computed=false]'(node: { parent: TSESTree.CallExpression; } & TSESTree.MemberExpression): void; }

Calls:

  • context.sourceCode.getScope
  • getParserServices (from ../util)
  • services.program.getTypeChecker
  • getStaticValue (from ../util)
  • isNumber
  • ts.isFunctionLike
  • paramA.getText
  • paramB.getText
  • parseRegExpLiteral (from @eslint-community/regexpp)
  • chars.every
  • String.fromCodePoint
  • chars.map
  • Object.values(EscapeMap).join
  • str.replaceAll
  • isStaticMemberAccessOfValue (from ../util)
  • isNegativeCheck
  • isPositiveCheck
  • services .getSymbolAtLocation(node.property) ?.getDeclarations
  • checker.getTypeAtLocation
  • type .getProperty('includes') ?.getDeclarations
  • includesMethodDecl?.some
  • hasSameParameters
  • context.report
  • fixer.insertTextBefore
  • fixer.replaceText
  • fixer.removeRange
  • checkArrayIndexOf
  • parseRegExp
  • getConstrainedTypeAtLocation (from ../util)
  • fixer.insertTextAfter
  • escapeString

Internal Comments:

// Check name, type, and question token once.
/**
     * Parse a given node if it's a `RegExp` instance.
     * @param node The node to parse.
     */
// Check if it can determine a unique string. (x2)
// To string.
// Check if the comparison is equivalent to `includes()`. (x2)
// Get the symbol of `indexOf` method. (x2)
// Check if every declaration of `indexOf` method has `includes` method
// and the two methods have the same parameters.
// Report it. (x4)
// a.indexOf(b) !== 1 (x2)
// a?.indexOf(b) !== 1 (x2)
// /bar/.test(foo) (x2)
//check the argument type of test methods (x2)

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

    function isNumber(node: TSESTree.Node, value: number): boolean {
      const evaluated = getStaticValue(node, globalScope);
      return evaluated?.value === value;
    }

    function isPositiveCheck(node: TSESTree.BinaryExpression): boolean {
      switch (node.operator) {
        case '!==':
        case '!=':
        case '>':
          return isNumber(node.right, -1);
        case '>=':
          return isNumber(node.right, 0);
        default:
          return false;
      }
    }
    function isNegativeCheck(node: TSESTree.BinaryExpression): boolean {
      switch (node.operator) {
        case '===':
        case '==':
        case '<=':
          return isNumber(node.right, -1);
        case '<':
          return isNumber(node.right, 0);
        default:
          return false;
      }
    }

    function hasSameParameters(
      nodeA: ts.Declaration,
      nodeB: ts.Declaration,
    ): boolean {
      if (!ts.isFunctionLike(nodeA) || !ts.isFunctionLike(nodeB)) {
        return false;
      }

      const paramsA = nodeA.parameters;
      const paramsB = nodeB.parameters;
      if (paramsA.length !== paramsB.length) {
        return false;
      }

      for (let i = 0; i < paramsA.length; ++i) {
        const paramA = paramsA[i];
        const paramB = paramsB[i];

        // Check name, type, and question token once.
        if (paramA.getText() !== paramB.getText()) {
          return false;
        }
      }

      return true;
    }

    /**
     * Parse a given node if it's a `RegExp` instance.
     * @param node The node to parse.
     */
    function parseRegExp(node: TSESTree.Node): string | null {
      const evaluated = getStaticValue(node, globalScope);
      if (evaluated == null || !(evaluated.value instanceof RegExp)) {
        return null;
      }

      const { flags, pattern } = parseRegExpLiteral(evaluated.value);
      if (
        pattern.alternatives.length !== 1 ||
        flags.ignoreCase ||
        flags.global
      ) {
        return null;
      }

      // Check if it can determine a unique string.
      const chars = pattern.alternatives[0].elements;
      if (!chars.every(c => c.type === 'Character')) {
        return null;
      }

      // To string.
      return String.fromCodePoint(...chars.map(c => c.value));
    }

    function escapeString(str: string): string {
      const EscapeMap = {
        '\0': '\\0',
        '\t': '\\t',
        '\n': '\\n',
        '\v': '\\v',
        '\f': '\\f',
        '\r': '\\r',
        "'": "\\'",
        '\\': '\\\\',
        // "\b" cause unexpected replacements
        // '\b': '\\b',
      };
      const replaceRegex = new RegExp(Object.values(EscapeMap).join('|'), 'g');

      return str.replaceAll(
        replaceRegex,
        char => EscapeMap[char as keyof typeof EscapeMap],
      );
    }

    function checkArrayIndexOf(
      node: TSESTree.MemberExpression,
      allowFixing: boolean,
    ): void {
      if (!isStaticMemberAccessOfValue(node, context, 'indexOf')) {
        return;
      }
      // Check if the comparison is equivalent to `includes()`.
      const callNode = node.parent as TSESTree.CallExpression;
      const compareNode = (
        callNode.parent.type === AST_NODE_TYPES.ChainExpression
          ? callNode.parent.parent
          : callNode.parent
      ) as TSESTree.BinaryExpression;
      const negative = isNegativeCheck(compareNode);
      if (!negative && !isPositiveCheck(compareNode)) {
        return;
      }

      // Get the symbol of `indexOf` method.
      const indexofMethodDeclarations = services
        .getSymbolAtLocation(node.property)
        ?.getDeclarations();
      if (
        indexofMethodDeclarations == null ||
        indexofMethodDeclarations.length === 0
      ) {
        return;
      }

      // Check if every declaration of `indexOf` method has `includes` method
      // and the two methods have the same parameters.
      for (const instanceofMethodDecl of indexofMethodDeclarations) {
        const typeDecl = instanceofMethodDecl.parent;
        const type = checker.getTypeAtLocation(typeDecl);
        const includesMethodDecl = type
          .getProperty('includes')
          ?.getDeclarations();
        if (
          !includesMethodDecl?.some(includesMethodDecl =>
            hasSameParameters(includesMethodDecl, instanceofMethodDecl),
          )
        ) {
          return;
        }
      }

      // Report it.
      context.report({
        node: compareNode,
        messageId: 'preferIncludes',
        ...(allowFixing && {
          *fix(fixer): Generator<TSESLint.RuleFix> {
            if (negative) {
              yield fixer.insertTextBefore(callNode, '!');
            }
            yield fixer.replaceText(node.property, 'includes');
            yield fixer.removeRange([callNode.range[1], compareNode.range[1]]);
          },
        }),
      });
    }

    return {
      // a.indexOf(b) !== 1
      'BinaryExpression > CallExpression.left > MemberExpression'(
        node: TSESTree.MemberExpression,
      ): void {
        checkArrayIndexOf(node, /* allowFixing */ true);
      },

      // a?.indexOf(b) !== 1
      'BinaryExpression > ChainExpression.left > CallExpression > MemberExpression'(
        node: TSESTree.MemberExpression,
      ): void {
        checkArrayIndexOf(node, /* allowFixing */ false);
      },

      // /bar/.test(foo)
      'CallExpression[arguments.length=1] > MemberExpression.callee[property.name="test"][computed=false]'(
        node: { parent: TSESTree.CallExpression } & TSESTree.MemberExpression,
      ): void {
        const callNode = node.parent;
        const text = parseRegExp(node.object);
        if (text == null) {
          return;
        }

        //check the argument type of test methods
        const argument = callNode.arguments[0];
        const type = getConstrainedTypeAtLocation(services, argument);

        const includesMethodDecl = type
          .getProperty('includes')
          ?.getDeclarations();
        if (includesMethodDecl == null) {
          return;
        }

        context.report({
          node: callNode,
          messageId: 'preferStringIncludes',
          *fix(fixer) {
            const argNode = callNode.arguments[0];
            const needsParen =
              argNode.type !== AST_NODE_TYPES.Literal &&
              argNode.type !== AST_NODE_TYPES.TemplateLiteral &&
              argNode.type !== AST_NODE_TYPES.Identifier &&
              argNode.type !== AST_NODE_TYPES.MemberExpression &&
              argNode.type !== AST_NODE_TYPES.CallExpression;

            yield fixer.removeRange([callNode.range[0], argNode.range[0]]);
            yield fixer.removeRange([argNode.range[1], callNode.range[1]]);
            if (needsParen) {
              yield fixer.insertTextBefore(argNode, '(');
              yield fixer.insertTextAfter(argNode, ')');
            }
            yield fixer.insertTextAfter(
              argNode,
              `${node.optional ? '?.' : '.'}includes('${escapeString(text)}')`,
            );
          },
        });
      },
    };
  }

Internal helpers

Declared inside another function in this file.

isNumber(node: TSESTree.Node, value: number): boolean

Parameters:

  • node TSESTree.Node
  • value number

Returns: boolean

Calls:

  • getStaticValue (from ../util)
Code
function isNumber(node: TSESTree.Node, value: number): boolean {
      const evaluated = getStaticValue(node, globalScope);
      return evaluated?.value === value;
    }

isPositiveCheck(node: TSESTree.BinaryExpression): boolean

Parameters:

  • node TSESTree.BinaryExpression

Returns: boolean

Calls:

  • isNumber
Code
function isPositiveCheck(node: TSESTree.BinaryExpression): boolean {
      switch (node.operator) {
        case '!==':
        case '!=':
        case '>':
          return isNumber(node.right, -1);
        case '>=':
          return isNumber(node.right, 0);
        default:
          return false;
      }
    }

isNegativeCheck(node: TSESTree.BinaryExpression): boolean

Parameters:

  • node TSESTree.BinaryExpression

Returns: boolean

Calls:

  • isNumber
Code
function isNegativeCheck(node: TSESTree.BinaryExpression): boolean {
      switch (node.operator) {
        case '===':
        case '==':
        case '<=':
          return isNumber(node.right, -1);
        case '<':
          return isNumber(node.right, 0);
        default:
          return false;
      }
    }

hasSameParameters(nodeA: ts.Declaration, nodeB: ts.Declaration): boolean

Parameters:

  • nodeA ts.Declaration
  • nodeB ts.Declaration

Returns: boolean

Calls:

  • ts.isFunctionLike
  • paramA.getText
  • paramB.getText

Internal Comments:

// Check name, type, and question token once.

Code
function hasSameParameters(
      nodeA: ts.Declaration,
      nodeB: ts.Declaration,
    ): boolean {
      if (!ts.isFunctionLike(nodeA) || !ts.isFunctionLike(nodeB)) {
        return false;
      }

      const paramsA = nodeA.parameters;
      const paramsB = nodeB.parameters;
      if (paramsA.length !== paramsB.length) {
        return false;
      }

      for (let i = 0; i < paramsA.length; ++i) {
        const paramA = paramsA[i];
        const paramB = paramsB[i];

        // Check name, type, and question token once.
        if (paramA.getText() !== paramB.getText()) {
          return false;
        }
      }

      return true;
    }

parseRegExp(node: TSESTree.Node): string | null

Parse a given node if it's a RegExp instance.

Parameters:

  • node any: The node to parse.
Raw JSDoc
/**
     * Parse a given node if it's a `RegExp` instance.
     * @param node The node to parse.
     */

Calls:

  • getStaticValue (from ../util)
  • parseRegExpLiteral (from @eslint-community/regexpp)
  • chars.every
  • String.fromCodePoint
  • chars.map

Internal Comments:

// Check if it can determine a unique string. (x2)
// To string.

Code
function parseRegExp(node: TSESTree.Node): string | null {
      const evaluated = getStaticValue(node, globalScope);
      if (evaluated == null || !(evaluated.value instanceof RegExp)) {
        return null;
      }

      const { flags, pattern } = parseRegExpLiteral(evaluated.value);
      if (
        pattern.alternatives.length !== 1 ||
        flags.ignoreCase ||
        flags.global
      ) {
        return null;
      }

      // Check if it can determine a unique string.
      const chars = pattern.alternatives[0].elements;
      if (!chars.every(c => c.type === 'Character')) {
        return null;
      }

      // To string.
      return String.fromCodePoint(...chars.map(c => c.value));
    }

escapeString(str: string): string

Parameters:

  • str string

Returns: string

Calls:

  • Object.values(EscapeMap).join
  • str.replaceAll
Code
function escapeString(str: string): string {
      const EscapeMap = {
        '\0': '\\0',
        '\t': '\\t',
        '\n': '\\n',
        '\v': '\\v',
        '\f': '\\f',
        '\r': '\\r',
        "'": "\\'",
        '\\': '\\\\',
        // "\b" cause unexpected replacements
        // '\b': '\\b',
      };
      const replaceRegex = new RegExp(Object.values(EscapeMap).join('|'), 'g');

      return str.replaceAll(
        replaceRegex,
        char => EscapeMap[char as keyof typeof EscapeMap],
      );
    }

checkArrayIndexOf(node: TSESTree.MemberExpression, allowFixing: boolean): void

Parameters:

  • node TSESTree.MemberExpression
  • allowFixing boolean

Returns: void

Calls:

  • isStaticMemberAccessOfValue (from ../util)
  • isNegativeCheck
  • isPositiveCheck
  • services .getSymbolAtLocation(node.property) ?.getDeclarations
  • checker.getTypeAtLocation
  • type .getProperty('includes') ?.getDeclarations
  • includesMethodDecl?.some
  • hasSameParameters
  • context.report
  • fixer.insertTextBefore
  • fixer.replaceText
  • fixer.removeRange

Internal Comments:

// Check if the comparison is equivalent to `includes()`. (x2)
// Get the symbol of `indexOf` method. (x2)
// Check if every declaration of `indexOf` method has `includes` method
// and the two methods have the same parameters.
// Report it. (x4)

Code
function checkArrayIndexOf(
      node: TSESTree.MemberExpression,
      allowFixing: boolean,
    ): void {
      if (!isStaticMemberAccessOfValue(node, context, 'indexOf')) {
        return;
      }
      // Check if the comparison is equivalent to `includes()`.
      const callNode = node.parent as TSESTree.CallExpression;
      const compareNode = (
        callNode.parent.type === AST_NODE_TYPES.ChainExpression
          ? callNode.parent.parent
          : callNode.parent
      ) as TSESTree.BinaryExpression;
      const negative = isNegativeCheck(compareNode);
      if (!negative && !isPositiveCheck(compareNode)) {
        return;
      }

      // Get the symbol of `indexOf` method.
      const indexofMethodDeclarations = services
        .getSymbolAtLocation(node.property)
        ?.getDeclarations();
      if (
        indexofMethodDeclarations == null ||
        indexofMethodDeclarations.length === 0
      ) {
        return;
      }

      // Check if every declaration of `indexOf` method has `includes` method
      // and the two methods have the same parameters.
      for (const instanceofMethodDecl of indexofMethodDeclarations) {
        const typeDecl = instanceofMethodDecl.parent;
        const type = checker.getTypeAtLocation(typeDecl);
        const includesMethodDecl = type
          .getProperty('includes')
          ?.getDeclarations();
        if (
          !includesMethodDecl?.some(includesMethodDecl =>
            hasSameParameters(includesMethodDecl, instanceofMethodDecl),
          )
        ) {
          return;
        }
      }

      // Report it.
      context.report({
        node: compareNode,
        messageId: 'preferIncludes',
        ...(allowFixing && {
          *fix(fixer): Generator<TSESLint.RuleFix> {
            if (negative) {
              yield fixer.insertTextBefore(callNode, '!');
            }
            yield fixer.replaceText(node.property, 'includes');
            yield fixer.removeRange([callNode.range[1], compareNode.range[1]]);
          },
        }),
      });
    }

Generated by Syntax Scribe