Skip to content

⬅️ Back to Table of Contents

📄 prefer-find

📊 Analysis Summary

Metric Count
🔧 Functions 11
📦 Imports 12
📐 Interfaces 1

📚 Table of Contents

🛠️ File Location:

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

📤 Default Export

export default createRule({ ... })
Property Value
name 'prefer-find'
meta.type 'suggestion'
meta.docs.description 'Enforce the use of Array.prototype.find() over Array.prototype.filter() followed by [0] when looking for a single re...
meta.docs.recommended 'stylistic'
meta.docs.requiresTypeChecking true
meta.hasSuggestions true
meta.messages.preferFind 'Prefer .find(...) instead of .filter(...)[0].'
meta.messages.preferFindSuggestion 'Use .find(...) instead of .filter(...)[0].'
meta.schema []
defaultOptions []

Entry point: create — documented under Functions.


📦 Imports

Name Source
TSESLint @typescript-eslint/utils
TSESTree @typescript-eslint/utils
RuleFix @typescript-eslint/utils/ts-eslint
Type typescript
AST_NODE_TYPES @typescript-eslint/utils
createRule ../util
getConstrainedTypeAtLocation ../util
getParserServices ../util
getStaticValue ../util
isStaticMemberAccessOfValue ../util
nullThrows ../util
skipChainExpression ../util

Functions

create(context: any): { CallExpression(node: any): void; 'MemberExpression[comput…

Parameters:

  • context any

Returns: { CallExpression(node: any): void; 'MemberExpression[computed=true]'(node: TSESTree.MemberExpressionComputedName): void; }

Calls:

  • context.sourceCode.getScope
  • getParserServices (from ../util)
  • services.program.getTypeChecker
  • skipChainExpression (from ../util)
  • nullThrows (from ../util)
  • node.expressions.at
  • parseArrayFilterExpressions
  • isStaticMemberAccessOfValue (from ../util)
  • getConstrainedTypeAtLocation (from ../util)
  • isArrayish
  • tsutils.unionConstituents
  • tsutils.isIntrinsicNullType
  • tsutils.isIntrinsicUndefinedType
  • tsutils .intersectionConstituents(unionPart) .every
  • checker.isArrayType
  • checker.isTupleType
  • getStaticValue (from ../util)
  • isTreatedAsZeroByArrayAt
  • Number
  • isNaN
  • Math.trunc
  • isTreatedAsZeroByMemberAccess
  • String
  • context.sourceCode.getTokenAfter
  • fixer.removeRange
  • fixer.replaceText
  • getObjectIfArrayAtZeroExpression
  • context.report
  • filterExpressions.map
  • generateFixToReplaceFilterWithFind
  • generateFixToRemoveArrayElementAccess
  • isMemberAccessOfZero

Internal Comments:

// Only the last expression in (a, b, [1, 2, 3].filter(condition))[0] matters (x2)
// This is the only reason we're returning a list rather than a single value.
// Both branches of the ternary _must_ return results. (x2)
// Accumulate the results from both sides and pass up the chain.
// Check if it looks like <<stuff>>(...), but not <<stuff>>?.(...)
// Check if it looks like <<stuff>>.filter(...) or <<stuff>>['filter'](...),
// or the optional chaining variants.
// As long as the object is a (possibly nullable) array,
// this is an Array.prototype.filter expression.
// not a filter expression.
/**
     * Tells whether the type is a possibly nullable array/tuple or union thereof.
     */
// apparently checker.isArrayType(T[] & S[]) => false. (x2)
// so we need to check the intersection parts individually. (x2)
// There is a non-array, non-nullish type component,
// so it's not an array.
// .at() should take exactly one argument.
/**
     * Implements the algorithm for array indexing by `.at()` method.
     * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/at#parameters
     */
// This would cause the number constructor coercion to throw. Other static
// values are safe.
// Check if it looks like <<stuff>>[0] or <<stuff>>['0'], but not <<stuff>>?.[0]
/**
     * Implements the algorithm for array indexing by member operator.
     * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array#array_indices
     */
// The next `.` or `[` is what we're looking for. (x4)
// think of (...).at(0) or (...)[0] or even (...)["at"](0). (x4)
// This query will be used to find things like `filteredResults.at(0)`. (x2)
// Get rid of the .at(0) or ['at'](0). (x2)
// This query will be used to find things like `filteredResults[0]`. (x2)
// (x2)
// Note: we're always looking for array member access to be "computed", (x2)
// i.e. `filteredResults[0]`, since `filteredResults.0` isn't a thing. (x2)
// Get rid of the [0]. (x2)

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

    interface FilterExpressionData {
      filterNode: TSESTree.Node;
      isBracketSyntaxForFilter: boolean;
    }

    function parseArrayFilterExpressions(
      expression: TSESTree.Expression,
    ): FilterExpressionData[] {
      const node = skipChainExpression(expression);

      if (node.type === AST_NODE_TYPES.SequenceExpression) {
        // Only the last expression in (a, b, [1, 2, 3].filter(condition))[0] matters
        const lastExpression = nullThrows(
          node.expressions.at(-1),
          'Expected to have more than zero expressions in a sequence expression',
        );
        return parseArrayFilterExpressions(lastExpression);
      }

      // This is the only reason we're returning a list rather than a single value.
      if (node.type === AST_NODE_TYPES.ConditionalExpression) {
        // Both branches of the ternary _must_ return results.
        const consequentResult = parseArrayFilterExpressions(node.consequent);
        if (consequentResult.length === 0) {
          return [];
        }

        const alternateResult = parseArrayFilterExpressions(node.alternate);
        if (alternateResult.length === 0) {
          return [];
        }

        // Accumulate the results from both sides and pass up the chain.
        return [...consequentResult, ...alternateResult];
      }

      // Check if it looks like <<stuff>>(...), but not <<stuff>>?.(...)
      if (node.type === AST_NODE_TYPES.CallExpression && !node.optional) {
        const callee = node.callee;
        // Check if it looks like <<stuff>>.filter(...) or <<stuff>>['filter'](...),
        // or the optional chaining variants.
        if (callee.type === AST_NODE_TYPES.MemberExpression) {
          const isBracketSyntaxForFilter = callee.computed;
          if (isStaticMemberAccessOfValue(callee, context, 'filter')) {
            const filterNode = callee.property;

            const filteredObjectType = getConstrainedTypeAtLocation(
              services,
              callee.object,
            );

            // As long as the object is a (possibly nullable) array,
            // this is an Array.prototype.filter expression.
            if (isArrayish(filteredObjectType)) {
              return [
                {
                  filterNode,
                  isBracketSyntaxForFilter,
                },
              ];
            }
          }
        }
      }

      // not a filter expression.
      return [];
    }

    /**
     * Tells whether the type is a possibly nullable array/tuple or union thereof.
     */
    function isArrayish(type: Type): boolean {
      let isAtLeastOneArrayishComponent = false;
      for (const unionPart of tsutils.unionConstituents(type)) {
        if (
          tsutils.isIntrinsicNullType(unionPart) ||
          tsutils.isIntrinsicUndefinedType(unionPart)
        ) {
          continue;
        }

        // apparently checker.isArrayType(T[] & S[]) => false.
        // so we need to check the intersection parts individually.
        const isArrayOrIntersectionThereof = tsutils
          .intersectionConstituents(unionPart)
          .every(
            intersectionPart =>
              checker.isArrayType(intersectionPart) ||
              checker.isTupleType(intersectionPart),
          );

        if (!isArrayOrIntersectionThereof) {
          // There is a non-array, non-nullish type component,
          // so it's not an array.
          return false;
        }

        isAtLeastOneArrayishComponent = true;
      }

      return isAtLeastOneArrayishComponent;
    }

    function getObjectIfArrayAtZeroExpression(
      node: TSESTree.CallExpression,
    ): TSESTree.Expression | undefined {
      // .at() should take exactly one argument.
      if (node.arguments.length !== 1) {
        return undefined;
      }

      const callee = node.callee;
      if (
        callee.type === AST_NODE_TYPES.MemberExpression &&
        !callee.optional &&
        isStaticMemberAccessOfValue(callee, context, 'at')
      ) {
        const atArgument = getStaticValue(node.arguments[0], globalScope);
        if (atArgument != null && isTreatedAsZeroByArrayAt(atArgument.value)) {
          return callee.object;
        }
      }

      return undefined;
    }

    /**
     * Implements the algorithm for array indexing by `.at()` method.
     * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/at#parameters
     */
    function isTreatedAsZeroByArrayAt(value: unknown): boolean {
      // This would cause the number constructor coercion to throw. Other static
      // values are safe.
      if (typeof value === 'symbol') {
        return false;
      }

      const asNumber = Number(value);

      if (isNaN(asNumber)) {
        return true;
      }

      return Math.trunc(asNumber) === 0;
    }

    function isMemberAccessOfZero(
      node: TSESTree.MemberExpressionComputedName,
    ): boolean {
      const property = getStaticValue(node.property, globalScope);
      // Check if it looks like <<stuff>>[0] or <<stuff>>['0'], but not <<stuff>>?.[0]
      return (
        !node.optional &&
        property != null &&
        isTreatedAsZeroByMemberAccess(property.value)
      );
    }

    /**
     * Implements the algorithm for array indexing by member operator.
     * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array#array_indices
     */
    function isTreatedAsZeroByMemberAccess(value: unknown): boolean {
      return String(value) === '0';
    }

    function generateFixToRemoveArrayElementAccess(
      fixer: TSESLint.RuleFixer,
      arrayNode: TSESTree.Expression,
      wholeExpressionBeingFlagged: TSESTree.Expression,
    ): RuleFix {
      const tokenToStartDeletingFrom = nullThrows(
        // The next `.` or `[` is what we're looking for.
        // think of (...).at(0) or (...)[0] or even (...)["at"](0).
        context.sourceCode.getTokenAfter(
          arrayNode,
          token => token.value === '.' || token.value === '[',
        ),
        'Expected to find a member access token!',
      );
      return fixer.removeRange([
        tokenToStartDeletingFrom.range[0],
        wholeExpressionBeingFlagged.range[1],
      ]);
    }

    function generateFixToReplaceFilterWithFind(
      fixer: TSESLint.RuleFixer,
      filterExpression: FilterExpressionData,
    ): TSESLint.RuleFix {
      return fixer.replaceText(
        filterExpression.filterNode,
        filterExpression.isBracketSyntaxForFilter ? '"find"' : 'find',
      );
    }

    return {
      // This query will be used to find things like `filteredResults.at(0)`.
      CallExpression(node): void {
        const object = getObjectIfArrayAtZeroExpression(node);
        if (object) {
          const filterExpressions = parseArrayFilterExpressions(object);
          if (filterExpressions.length !== 0) {
            context.report({
              node,
              messageId: 'preferFind',
              suggest: [
                {
                  messageId: 'preferFindSuggestion',
                  fix: (fixer): TSESLint.RuleFix[] => {
                    return [
                      ...filterExpressions.map(filterExpression =>
                        generateFixToReplaceFilterWithFind(
                          fixer,
                          filterExpression,
                        ),
                      ),
                      // Get rid of the .at(0) or ['at'](0).
                      generateFixToRemoveArrayElementAccess(
                        fixer,
                        object,
                        node,
                      ),
                    ];
                  },
                },
              ],
            });
          }
        }
      },

      // This query will be used to find things like `filteredResults[0]`.
      //
      // Note: we're always looking for array member access to be "computed",
      // i.e. `filteredResults[0]`, since `filteredResults.0` isn't a thing.
      'MemberExpression[computed=true]'(
        node: TSESTree.MemberExpressionComputedName,
      ): void {
        if (isMemberAccessOfZero(node)) {
          const object = node.object;
          const filterExpressions = parseArrayFilterExpressions(object);
          if (filterExpressions.length !== 0) {
            context.report({
              node,
              messageId: 'preferFind',
              suggest: [
                {
                  messageId: 'preferFindSuggestion',
                  fix: (fixer): TSESLint.RuleFix[] => {
                    return [
                      ...filterExpressions.map(filterExpression =>
                        generateFixToReplaceFilterWithFind(
                          fixer,
                          filterExpression,
                        ),
                      ),
                      // Get rid of the [0].
                      generateFixToRemoveArrayElementAccess(
                        fixer,
                        object,
                        node,
                      ),
                    ];
                  },
                },
              ],
            });
          }
        }
      },
    };
  }

Internal helpers

Declared inside another function in this file.

parseArrayFilterExpressions(expression: TSESTree.Expression): FilterExpressionData[]

Parameters:

  • expression TSESTree.Expression

Returns: FilterExpressionData[]

Calls:

  • skipChainExpression (from ../util)
  • nullThrows (from ../util)
  • node.expressions.at
  • parseArrayFilterExpressions
  • isStaticMemberAccessOfValue (from ../util)
  • getConstrainedTypeAtLocation (from ../util)
  • isArrayish

Internal Comments:

// Only the last expression in (a, b, [1, 2, 3].filter(condition))[0] matters (x2)
// This is the only reason we're returning a list rather than a single value.
// Both branches of the ternary _must_ return results. (x2)
// Accumulate the results from both sides and pass up the chain.
// Check if it looks like <<stuff>>(...), but not <<stuff>>?.(...)
// Check if it looks like <<stuff>>.filter(...) or <<stuff>>['filter'](...),
// or the optional chaining variants.
// As long as the object is a (possibly nullable) array,
// this is an Array.prototype.filter expression.
// not a filter expression.

Code
function parseArrayFilterExpressions(
      expression: TSESTree.Expression,
    ): FilterExpressionData[] {
      const node = skipChainExpression(expression);

      if (node.type === AST_NODE_TYPES.SequenceExpression) {
        // Only the last expression in (a, b, [1, 2, 3].filter(condition))[0] matters
        const lastExpression = nullThrows(
          node.expressions.at(-1),
          'Expected to have more than zero expressions in a sequence expression',
        );
        return parseArrayFilterExpressions(lastExpression);
      }

      // This is the only reason we're returning a list rather than a single value.
      if (node.type === AST_NODE_TYPES.ConditionalExpression) {
        // Both branches of the ternary _must_ return results.
        const consequentResult = parseArrayFilterExpressions(node.consequent);
        if (consequentResult.length === 0) {
          return [];
        }

        const alternateResult = parseArrayFilterExpressions(node.alternate);
        if (alternateResult.length === 0) {
          return [];
        }

        // Accumulate the results from both sides and pass up the chain.
        return [...consequentResult, ...alternateResult];
      }

      // Check if it looks like <<stuff>>(...), but not <<stuff>>?.(...)
      if (node.type === AST_NODE_TYPES.CallExpression && !node.optional) {
        const callee = node.callee;
        // Check if it looks like <<stuff>>.filter(...) or <<stuff>>['filter'](...),
        // or the optional chaining variants.
        if (callee.type === AST_NODE_TYPES.MemberExpression) {
          const isBracketSyntaxForFilter = callee.computed;
          if (isStaticMemberAccessOfValue(callee, context, 'filter')) {
            const filterNode = callee.property;

            const filteredObjectType = getConstrainedTypeAtLocation(
              services,
              callee.object,
            );

            // As long as the object is a (possibly nullable) array,
            // this is an Array.prototype.filter expression.
            if (isArrayish(filteredObjectType)) {
              return [
                {
                  filterNode,
                  isBracketSyntaxForFilter,
                },
              ];
            }
          }
        }
      }

      // not a filter expression.
      return [];
    }

isArrayish(type: Type): boolean

Tells whether the type is a possibly nullable array/tuple or union thereof.

Raw JSDoc
/**
     * Tells whether the type is a possibly nullable array/tuple or union thereof.
     */

Calls:

  • tsutils.unionConstituents
  • tsutils.isIntrinsicNullType
  • tsutils.isIntrinsicUndefinedType
  • tsutils .intersectionConstituents(unionPart) .every
  • checker.isArrayType
  • checker.isTupleType

Internal Comments:

// apparently checker.isArrayType(T[] & S[]) => false. (x2)
// so we need to check the intersection parts individually. (x2)
// There is a non-array, non-nullish type component,
// so it's not an array.

Code
function isArrayish(type: Type): boolean {
      let isAtLeastOneArrayishComponent = false;
      for (const unionPart of tsutils.unionConstituents(type)) {
        if (
          tsutils.isIntrinsicNullType(unionPart) ||
          tsutils.isIntrinsicUndefinedType(unionPart)
        ) {
          continue;
        }

        // apparently checker.isArrayType(T[] & S[]) => false.
        // so we need to check the intersection parts individually.
        const isArrayOrIntersectionThereof = tsutils
          .intersectionConstituents(unionPart)
          .every(
            intersectionPart =>
              checker.isArrayType(intersectionPart) ||
              checker.isTupleType(intersectionPart),
          );

        if (!isArrayOrIntersectionThereof) {
          // There is a non-array, non-nullish type component,
          // so it's not an array.
          return false;
        }

        isAtLeastOneArrayishComponent = true;
      }

      return isAtLeastOneArrayishComponent;
    }

getObjectIfArrayAtZeroExpression(node: TSESTree.CallExpression): TSESTree.Expression | undefined

Parameters:

  • node TSESTree.CallExpression

Returns: TSESTree.Expression | undefined

Calls:

  • isStaticMemberAccessOfValue (from ../util)
  • getStaticValue (from ../util)
  • isTreatedAsZeroByArrayAt

Internal Comments:

// .at() should take exactly one argument.

Code
function getObjectIfArrayAtZeroExpression(
      node: TSESTree.CallExpression,
    ): TSESTree.Expression | undefined {
      // .at() should take exactly one argument.
      if (node.arguments.length !== 1) {
        return undefined;
      }

      const callee = node.callee;
      if (
        callee.type === AST_NODE_TYPES.MemberExpression &&
        !callee.optional &&
        isStaticMemberAccessOfValue(callee, context, 'at')
      ) {
        const atArgument = getStaticValue(node.arguments[0], globalScope);
        if (atArgument != null && isTreatedAsZeroByArrayAt(atArgument.value)) {
          return callee.object;
        }
      }

      return undefined;
    }

isTreatedAsZeroByArrayAt(value: unknown): boolean

Implements the algorithm for array indexing by .at() method.

See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/at#parameters

Raw JSDoc
/**
     * Implements the algorithm for array indexing by `.at()` method.
     * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/at#parameters
     */

Calls:

  • Number
  • isNaN
  • Math.trunc

Internal Comments:

// This would cause the number constructor coercion to throw. Other static
// values are safe.

Code
function isTreatedAsZeroByArrayAt(value: unknown): boolean {
      // This would cause the number constructor coercion to throw. Other static
      // values are safe.
      if (typeof value === 'symbol') {
        return false;
      }

      const asNumber = Number(value);

      if (isNaN(asNumber)) {
        return true;
      }

      return Math.trunc(asNumber) === 0;
    }

isMemberAccessOfZero(node: TSESTree.MemberExpressionComputedName): boolean

Parameters:

  • node TSESTree.MemberExpressionComputedName

Returns: boolean

Calls:

  • getStaticValue (from ../util)
  • isTreatedAsZeroByMemberAccess

Internal Comments:

// Check if it looks like <<stuff>>[0] or <<stuff>>['0'], but not <<stuff>>?.[0]

Code
function isMemberAccessOfZero(
      node: TSESTree.MemberExpressionComputedName,
    ): boolean {
      const property = getStaticValue(node.property, globalScope);
      // Check if it looks like <<stuff>>[0] or <<stuff>>['0'], but not <<stuff>>?.[0]
      return (
        !node.optional &&
        property != null &&
        isTreatedAsZeroByMemberAccess(property.value)
      );
    }

isTreatedAsZeroByMemberAccess(value: unknown): boolean

Implements the algorithm for array indexing by member operator.

See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array#array_indices

Raw JSDoc
/**
     * Implements the algorithm for array indexing by member operator.
     * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array#array_indices
     */

Calls:

  • String
Code
function isTreatedAsZeroByMemberAccess(value: unknown): boolean {
      return String(value) === '0';
    }

generateFixToRemoveArrayElementAccess(fixer: TSESLint.RuleFixer, arrayNode: TSESTree.Expression, wholeExpressionBeingFlagged: TSESTree.Expression): RuleFix

Parameters:

  • fixer TSESLint.RuleFixer
  • arrayNode TSESTree.Expression
  • wholeExpressionBeingFlagged TSESTree.Expression

Returns: RuleFix

Calls:

  • nullThrows (from ../util)
  • context.sourceCode.getTokenAfter
  • fixer.removeRange

Internal Comments:

// The next `.` or `[` is what we're looking for. (x4)
// think of (...).at(0) or (...)[0] or even (...)["at"](0). (x4)

Code
function generateFixToRemoveArrayElementAccess(
      fixer: TSESLint.RuleFixer,
      arrayNode: TSESTree.Expression,
      wholeExpressionBeingFlagged: TSESTree.Expression,
    ): RuleFix {
      const tokenToStartDeletingFrom = nullThrows(
        // The next `.` or `[` is what we're looking for.
        // think of (...).at(0) or (...)[0] or even (...)["at"](0).
        context.sourceCode.getTokenAfter(
          arrayNode,
          token => token.value === '.' || token.value === '[',
        ),
        'Expected to find a member access token!',
      );
      return fixer.removeRange([
        tokenToStartDeletingFrom.range[0],
        wholeExpressionBeingFlagged.range[1],
      ]);
    }

generateFixToReplaceFilterWithFind(fixer: TSESLint.RuleFixer, filterExpression: FilterExpressionData): TSESLint.RuleFix

Parameters:

  • fixer TSESLint.RuleFixer
  • filterExpression FilterExpressionData

Returns: TSESLint.RuleFix

Calls:

  • fixer.replaceText
Code
function generateFixToReplaceFilterWithFind(
      fixer: TSESLint.RuleFixer,
      filterExpression: FilterExpressionData,
    ): TSESLint.RuleFix {
      return fixer.replaceText(
        filterExpression.filterNode,
        filterExpression.isBracketSyntaxForFilter ? '"find"' : 'find',
      );
    }

suggest.fix(fixer: any): TSESLint.RuleFix[]

Parameters:

  • fixer any

Returns: TSESLint.RuleFix[]

Calls:

  • filterExpressions.map
  • generateFixToReplaceFilterWithFind
  • generateFixToRemoveArrayElementAccess

Internal Comments:

// Get rid of the .at(0) or ['at'](0). (x2)

Code
(fixer): TSESLint.RuleFix[] => {
                    return [
                      ...filterExpressions.map(filterExpression =>
                        generateFixToReplaceFilterWithFind(
                          fixer,
                          filterExpression,
                        ),
                      ),
                      // Get rid of the .at(0) or ['at'](0).
                      generateFixToRemoveArrayElementAccess(
                        fixer,
                        object,
                        node,
                      ),
                    ];
                  }

suggest.fix(fixer: any): TSESLint.RuleFix[]

Parameters:

  • fixer any

Returns: TSESLint.RuleFix[]

Calls:

  • filterExpressions.map
  • generateFixToReplaceFilterWithFind
  • generateFixToRemoveArrayElementAccess

Internal Comments:

// Get rid of the [0]. (x2)

Code
(fixer): TSESLint.RuleFix[] => {
                    return [
                      ...filterExpressions.map(filterExpression =>
                        generateFixToReplaceFilterWithFind(
                          fixer,
                          filterExpression,
                        ),
                      ),
                      // Get rid of the [0].
                      generateFixToRemoveArrayElementAccess(
                        fixer,
                        object,
                        node,
                      ),
                    ];
                  }

Interfaces

FilterExpressionData

Interface Code
interface FilterExpressionData {
      filterNode: TSESTree.Node;
      isBracketSyntaxForFilter: boolean;
    }

Properties

Name Type Optional Description
filterNode TSESTree.Node not shown
isBracketSyntaxForFilter boolean not shown

Generated by Syntax Scribe