📄 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¶
| 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:
contextany
Returns: { CallExpression(node: any): void; 'MemberExpression[computed=true]'(node: TSESTree.MemberExpressionComputedName): void; }
Calls:
context.sourceCode.getScopegetParserServices (from ../util)services.program.getTypeCheckerskipChainExpression (from ../util)nullThrows (from ../util)node.expressions.atparseArrayFilterExpressionsisStaticMemberAccessOfValue (from ../util)getConstrainedTypeAtLocation (from ../util)isArrayishtsutils.unionConstituentstsutils.isIntrinsicNullTypetsutils.isIntrinsicUndefinedTypetsutils .intersectionConstituents(unionPart) .everychecker.isArrayTypechecker.isTupleTypegetStaticValue (from ../util)isTreatedAsZeroByArrayAtNumberisNaNMath.truncisTreatedAsZeroByMemberAccessStringcontext.sourceCode.getTokenAfterfixer.removeRangefixer.replaceTextgetObjectIfArrayAtZeroExpressioncontext.reportfilterExpressions.mapgenerateFixToReplaceFilterWithFindgenerateFixToRemoveArrayElementAccessisMemberAccessOfZero
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:
expressionTSESTree.Expression
Returns: FilterExpressionData[]
Calls:
skipChainExpression (from ../util)nullThrows (from ../util)node.expressions.atparseArrayFilterExpressionsisStaticMemberAccessOfValue (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.
Calls:
tsutils.unionConstituentstsutils.isIntrinsicNullTypetsutils.isIntrinsicUndefinedTypetsutils .intersectionConstituents(unionPart) .everychecker.isArrayTypechecker.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:
nodeTSESTree.CallExpression
Returns: TSESTree.Expression | undefined
Calls:
isStaticMemberAccessOfValue (from ../util)getStaticValue (from ../util)isTreatedAsZeroByArrayAt
Internal Comments:
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
Calls:
NumberisNaNMath.trunc
Internal Comments:
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:
nodeTSESTree.MemberExpressionComputedName
Returns: boolean
Calls:
getStaticValue (from ../util)isTreatedAsZeroByMemberAccess
Internal Comments:
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
Calls:
String
Code
generateFixToRemoveArrayElementAccess(fixer: TSESLint.RuleFixer, arrayNode: TSESTree.Expression, wholeExpressionBeingFlagged: TSESTree.Expression): RuleFix¶
Parameters:
fixerTSESLint.RuleFixerarrayNodeTSESTree.ExpressionwholeExpressionBeingFlaggedTSESTree.Expression
Returns: RuleFix
Calls:
nullThrows (from ../util)context.sourceCode.getTokenAfterfixer.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:
fixerTSESLint.RuleFixerfilterExpressionFilterExpressionData
Returns: TSESLint.RuleFix
Calls:
fixer.replaceText
Code
suggest.fix(fixer: any): TSESLint.RuleFix[]¶
Parameters:
fixerany
Returns: TSESLint.RuleFix[]
Calls:
filterExpressions.mapgenerateFixToReplaceFilterWithFindgenerateFixToRemoveArrayElementAccess
Internal Comments:
Code
suggest.fix(fixer: any): TSESLint.RuleFix[]¶
Parameters:
fixerany
Returns: TSESLint.RuleFix[]
Calls:
filterExpressions.mapgenerateFixToReplaceFilterWithFindgenerateFixToRemoveArrayElementAccess
Internal Comments:
Code
Interfaces¶
FilterExpressionData¶
Interface Code
Properties¶
| Name | Type | Optional | Description |
|---|---|---|---|
filterNode |
TSESTree.Node |
✗ | not shown |
isBracketSyntaxForFilter |
boolean |
✗ | not shown |
Generated by Syntax Scribe