diff --git a/packages/eslint-plugin/docs/rules/prefer-find.md b/packages/eslint-plugin/docs/rules/prefer-find.md new file mode 100644 index 000000000000..62de96826809 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/prefer-find.md @@ -0,0 +1,39 @@ +--- +description: 'Enforce the use of Array.prototype.find() over Array.prototype.filter() followed by [0] when looking for a single result.' +--- + +> 🛑 This file is source code, not the primary documentation location! 🛑 +> +> See **https://typescript-eslint.io/rules/prefer-find** for documentation. + +When searching for the first item in an array matching a condition, it may be tempting to use code like `arr.filter(x => x > 0)[0]`. +However, it is simpler to use [Array.prototype.find()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find) instead, `arr.find(x => x > 0)`, which also returns the first entry matching a condition. +Because the `.find()` only needs to execute the callback until it finds a match, it's also more efficient. + +:::info + +Beware the difference in short-circuiting behavior between the approaches. +`.find()` will only execute the callback on array elements until it finds a match, whereas `.filter()` executes the callback for all array elements. +Therefore, when fixing errors from this rule, be sure that your `.filter()` callbacks do not have side effects. + +::: + + + +### ❌ Incorrect + +```ts +[1, 2, 3].filter(x => x > 1)[0]; + +[1, 2, 3].filter(x => x > 1).at(0); +``` + +### ✅ Correct + +```ts +[1, 2, 3].find(x => x > 1); +``` + +## When Not To Use It + +If you intentionally use patterns like `.filter(callback)[0]` to execute side effects in `callback` on all array elements, you will want to avoid this rule. diff --git a/packages/eslint-plugin/src/configs/all.ts b/packages/eslint-plugin/src/configs/all.ts index 9881e3397de2..99770f7c03b2 100644 --- a/packages/eslint-plugin/src/configs/all.ts +++ b/packages/eslint-plugin/src/configs/all.ts @@ -118,6 +118,7 @@ export = { 'prefer-destructuring': 'off', '@typescript-eslint/prefer-destructuring': 'error', '@typescript-eslint/prefer-enum-initializers': 'error', + '@typescript-eslint/prefer-find': 'error', '@typescript-eslint/prefer-for-of': 'error', '@typescript-eslint/prefer-function-type': 'error', '@typescript-eslint/prefer-includes': 'error', diff --git a/packages/eslint-plugin/src/configs/disable-type-checked.ts b/packages/eslint-plugin/src/configs/disable-type-checked.ts index 2fe413146c7b..20a9481ea733 100644 --- a/packages/eslint-plugin/src/configs/disable-type-checked.ts +++ b/packages/eslint-plugin/src/configs/disable-type-checked.ts @@ -38,6 +38,7 @@ export = { '@typescript-eslint/no-useless-template-literals': 'off', '@typescript-eslint/non-nullable-type-assertion-style': 'off', '@typescript-eslint/prefer-destructuring': 'off', + '@typescript-eslint/prefer-find': 'off', '@typescript-eslint/prefer-includes': 'off', '@typescript-eslint/prefer-nullish-coalescing': 'off', '@typescript-eslint/prefer-optional-chain': 'off', diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index 14c171af990e..ed426770097c 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -102,6 +102,7 @@ import parameterProperties from './parameter-properties'; import preferAsConst from './prefer-as-const'; import preferDestructuring from './prefer-destructuring'; import preferEnumInitializers from './prefer-enum-initializers'; +import preferFind from './prefer-find'; import preferForOf from './prefer-for-of'; import preferFunctionType from './prefer-function-type'; import preferIncludes from './prefer-includes'; @@ -241,6 +242,7 @@ export default { 'prefer-as-const': preferAsConst, 'prefer-destructuring': preferDestructuring, 'prefer-enum-initializers': preferEnumInitializers, + 'prefer-find': preferFind, 'prefer-for-of': preferForOf, 'prefer-function-type': preferFunctionType, 'prefer-includes': preferIncludes, diff --git a/packages/eslint-plugin/src/rules/prefer-find.ts b/packages/eslint-plugin/src/rules/prefer-find.ts new file mode 100644 index 000000000000..e94f384ecd33 --- /dev/null +++ b/packages/eslint-plugin/src/rules/prefer-find.ts @@ -0,0 +1,320 @@ +import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; +import { AST_NODE_TYPES } from '@typescript-eslint/utils'; +import { getScope, getSourceCode } from '@typescript-eslint/utils/eslint-utils'; +import type { + RuleFix, + Scope, + SourceCode, +} from '@typescript-eslint/utils/ts-eslint'; +import * as tsutils from 'ts-api-utils'; +import type { Type } from 'typescript'; + +import { + createRule, + getConstrainedTypeAtLocation, + getParserServices, + getStaticValue, + nullThrows, +} from '../util'; + +export default createRule({ + name: 'prefer-find', + meta: { + docs: { + description: + 'Enforce the use of Array.prototype.find() over Array.prototype.filter() followed by [0] when looking for a single result', + requiresTypeChecking: true, + }, + messages: { + preferFind: 'Prefer .find(...) instead of .filter(...)[0].', + preferFindSuggestion: 'Use .find(...) instead of .filter(...)[0].', + }, + schema: [], + type: 'suggestion', + hasSuggestions: true, + }, + + defaultOptions: [], + + create(context) { + const globalScope = getScope(context); + const services = getParserServices(context); + const checker = services.program.getTypeChecker(); + + interface FilterExpressionData { + isBracketSyntaxForFilter: boolean; + filterNode: TSESTree.Node; + } + + function parseIfArrayFilterExpression( + expression: TSESTree.Expression, + ): FilterExpressionData | undefined { + if (expression.type === AST_NODE_TYPES.SequenceExpression) { + // Only the last expression in (a, b, [1, 2, 3].filter(condition))[0] matters + const lastExpression = nullThrows( + expression.expressions.at(-1), + 'Expected to have more than zero expressions in a sequence expression', + ); + return parseIfArrayFilterExpression(lastExpression); + } + + if (expression.type === AST_NODE_TYPES.ChainExpression) { + return parseIfArrayFilterExpression(expression.expression); + } + + // Check if it looks like <>(...), but not <>?.(...) + if ( + expression.type === AST_NODE_TYPES.CallExpression && + !expression.optional + ) { + const callee = expression.callee; + // Check if it looks like <>.filter(...) or <>['filter'](...), + // or the optional chaining variants. + if (callee.type === AST_NODE_TYPES.MemberExpression) { + const isBracketSyntaxForFilter = callee.computed; + if (isStaticMemberAccessOfValue(callee, 'filter', globalScope)) { + 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 { + isBracketSyntaxForFilter, + filterNode, + }; + } + } + } + } + + return undefined; + } + + /** + * 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.unionTypeParts(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 + .intersectionTypeParts(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 getObjectIfArrayAtExpression( + node: TSESTree.CallExpression, + ): TSESTree.Expression | undefined { + // .at() should take exactly one argument. + if (node.arguments.length !== 1) { + return undefined; + } + + const atArgument = getStaticValue(node.arguments[0], globalScope); + if (atArgument != null && isTreatedAsZeroByArrayAt(atArgument.value)) { + const callee = node.callee; + if ( + callee.type === AST_NODE_TYPES.MemberExpression && + !callee.optional && + isStaticMemberAccessOfValue(callee, 'at', globalScope) + ) { + 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 { + 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 <>[0] or <>['0'], but not <>?.[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, + sourceCode: SourceCode, + ): RuleFix { + const tokenToStartDeletingFrom = nullThrows( + // The next `.` or `[` is what we're looking for. + // think of (...).at(0) or (...)[0] or even (...)["at"](0). + 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 = getObjectIfArrayAtExpression(node); + if (object) { + const filterExpression = parseIfArrayFilterExpression(object); + if (filterExpression) { + context.report({ + node, + messageId: 'preferFind', + suggest: [ + { + messageId: 'preferFindSuggestion', + fix: (fixer): TSESLint.RuleFix[] => { + const sourceCode = getSourceCode(context); + return [ + generateFixToReplaceFilterWithFind( + fixer, + filterExpression, + ), + // Get rid of the .at(0) or ['at'](0). + generateFixToRemoveArrayElementAccess( + fixer, + object, + node, + sourceCode, + ), + ]; + }, + }, + ], + }); + } + } + }, + + // 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 filterExpression = parseIfArrayFilterExpression(object); + if (filterExpression) { + context.report({ + node, + messageId: 'preferFind', + suggest: [ + { + messageId: 'preferFindSuggestion', + fix: (fixer): TSESLint.RuleFix[] => { + const sourceCode = context.sourceCode; + return [ + generateFixToReplaceFilterWithFind( + fixer, + filterExpression, + ), + // Get rid of the [0]. + generateFixToRemoveArrayElementAccess( + fixer, + object, + node, + sourceCode, + ), + ]; + }, + }, + ], + }); + } + } + }, + }; + }, +}); + +/** + * Answers whether the member expression looks like + * `x.memberName`, `x['memberName']`, + * or even `const mn = 'memberName'; x[mn]` (or optional variants thereof). + */ +function isStaticMemberAccessOfValue( + memberExpression: + | TSESTree.MemberExpressionComputedName + | TSESTree.MemberExpressionNonComputedName, + value: string, + scope?: Scope.Scope | undefined, +): boolean { + if (!memberExpression.computed) { + // x.memberName case. + return memberExpression.property.name === value; + } + + // x['memberName'] cases. + const staticValueResult = getStaticValue(memberExpression.property, scope); + return staticValueResult != null && value === staticValueResult.value; +} diff --git a/packages/eslint-plugin/tests/rules/prefer-find.test.ts b/packages/eslint-plugin/tests/rules/prefer-find.test.ts new file mode 100644 index 000000000000..6b303ea9aeda --- /dev/null +++ b/packages/eslint-plugin/tests/rules/prefer-find.test.ts @@ -0,0 +1,574 @@ +import { noFormat, RuleTester } from '@typescript-eslint/rule-tester'; + +import rule from '../../src/rules/prefer-find'; +import { getFixturesRootDir } from '../RuleTester'; + +const rootDir = getFixturesRootDir(); + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', + parserOptions: { + tsconfigRootDir: rootDir, + project: './tsconfig.json', + }, +}); + +ruleTester.run('prefer-find', rule, { + valid: [ + ` + interface JerkCode { + filter(predicate: (item: T) => boolean): JerkCode; + } + + declare const jerkCode: JerkCode; + + jerkCode.filter(item => item === 'aha')[0]; + `, + ` + declare const arr: readonly string[]; + arr.filter(item => item === 'aha')[1]; + `, + ` + declare const arr: string[]; + arr.filter(item => item === 'aha').at(1); + `, + ` + declare const notNecessarilyAnArray: unknown[] | undefined | null | string; + notNecessarilyAnArray?.filter(item => true)[0]; + `, + // Be sure that we don't try to mess with this case, since the member access + // should not need to be optional for the cases the rule is concerned with. + '[].filter(() => true)?.[0];', + // Be sure that we don't try to mess with this case, since the member access + // should not need to be optional for the cases the rule is concerned with. + '[].filter(() => true)?.at?.(0);', + // Be sure that we don't try to mess with this case, since the function call + // should not need to be optional for the cases the rule is concerned with. + '[].filter?.(() => true)[0];', + '[1, 2, 3].filter(x => x > 0).at(-Infinity);', + ` + declare const arr: string[]; + declare const cond: Parameters['filter']>[0]; + const a = { arr }; + a?.arr.filter(cond).at(1); + `, + "['Just', 'a', 'filter'].filter(x => x.length > 4);", + "['Just', 'a', 'find'].find(x => x.length > 4);", + 'undefined.filter(x => x)[0];', + 'null?.filter(x => x)[0];', + ], + + invalid: [ + { + code: ` +declare const arr: string[]; +arr.filter(item => item === 'aha')[0]; + `, + errors: [ + { + line: 3, + messageId: 'preferFind', + suggestions: [ + { + messageId: 'preferFindSuggestion', + output: ` +declare const arr: string[]; +arr.find(item => item === 'aha'); + `, + }, + ], + }, + ], + }, + { + code: ` +declare const arr: Array; +const zero = 0; +arr.filter(item => item === 'aha')[zero]; + `, + errors: [ + { + line: 4, + messageId: 'preferFind', + suggestions: [ + { + messageId: 'preferFindSuggestion', + output: ` +declare const arr: Array; +const zero = 0; +arr.find(item => item === 'aha'); + `, + }, + ], + }, + ], + }, + { + code: ` +declare const arr: Array; +const zero = 0n; +arr.filter(item => item === 'aha')[zero]; + `, + errors: [ + { + line: 4, + messageId: 'preferFind', + suggestions: [ + { + messageId: 'preferFindSuggestion', + output: ` +declare const arr: Array; +const zero = 0n; +arr.find(item => item === 'aha'); + `, + }, + ], + }, + ], + }, + { + code: ` +declare const arr: Array; +const zero = -0n; +arr.filter(item => item === 'aha')[zero]; + `, + errors: [ + { + line: 4, + messageId: 'preferFind', + suggestions: [ + { + messageId: 'preferFindSuggestion', + output: ` +declare const arr: Array; +const zero = -0n; +arr.find(item => item === 'aha'); + `, + }, + ], + }, + ], + }, + { + code: ` +declare const arr: readonly string[]; +arr.filter(item => item === 'aha').at(0); + `, + errors: [ + { + line: 3, + messageId: 'preferFind', + suggestions: [ + { + messageId: 'preferFindSuggestion', + output: ` +declare const arr: readonly string[]; +arr.find(item => item === 'aha'); + `, + }, + ], + }, + ], + }, + { + code: ` +declare const arr: ReadonlyArray; +(undefined, arr.filter(item => item === 'aha')).at(0); + `, + errors: [ + { + line: 3, + messageId: 'preferFind', + suggestions: [ + { + messageId: 'preferFindSuggestion', + output: ` +declare const arr: ReadonlyArray; +(undefined, arr.find(item => item === 'aha')); + `, + }, + ], + }, + ], + }, + { + code: ` +declare const arr: string[]; +const zero = 0; +arr.filter(item => item === 'aha').at(zero); + `, + errors: [ + { + line: 4, + messageId: 'preferFind', + suggestions: [ + { + messageId: 'preferFindSuggestion', + output: ` +declare const arr: string[]; +const zero = 0; +arr.find(item => item === 'aha'); + `, + }, + ], + }, + ], + }, + { + code: ` +declare const arr: string[]; +arr.filter(item => item === 'aha')['0']; + `, + errors: [ + { + line: 3, + messageId: 'preferFind', + suggestions: [ + { + messageId: 'preferFindSuggestion', + output: ` +declare const arr: string[]; +arr.find(item => item === 'aha'); + `, + }, + ], + }, + ], + }, + { + code: 'const two = [1, 2, 3].filter(item => item === 2)[0];', + errors: [ + { + line: 1, + messageId: 'preferFind', + suggestions: [ + { + messageId: 'preferFindSuggestion', + output: `const two = [1, 2, 3].find(item => item === 2);`, + }, + ], + }, + ], + }, + { + code: noFormat`const fltr = "filter"; (([] as unknown[]))[fltr] ((item) => { return item === 2 } ) [ 0 ] ;`, + errors: [ + { + line: 1, + messageId: 'preferFind', + suggestions: [ + { + messageId: 'preferFindSuggestion', + output: + 'const fltr = "filter"; (([] as unknown[]))["find"] ((item) => { return item === 2 } ) ;', + }, + ], + }, + ], + }, + { + code: noFormat`(([] as unknown[]))?.["filter"] ((item) => { return item === 2 } ) [ 0 ] ;`, + errors: [ + { + line: 1, + messageId: 'preferFind', + suggestions: [ + { + messageId: 'preferFindSuggestion', + output: + '(([] as unknown[]))?.["find"] ((item) => { return item === 2 } ) ;', + }, + ], + }, + ], + }, + { + code: ` +declare const nullableArray: unknown[] | undefined | null; +nullableArray?.filter(item => true)[0]; + `, + errors: [ + { + line: 3, + messageId: 'preferFind', + suggestions: [ + { + messageId: 'preferFindSuggestion', + output: ` +declare const nullableArray: unknown[] | undefined | null; +nullableArray?.find(item => true); + `, + }, + ], + }, + ], + }, + { + code: '([]?.filter(f))[0];', + errors: [ + { + line: 1, + messageId: 'preferFind', + suggestions: [ + { + messageId: 'preferFindSuggestion', + output: '([]?.find(f));', + }, + ], + }, + ], + }, + { + code: ` +declare const objectWithArrayProperty: { arr: unknown[] }; +declare function cond(x: unknown): boolean; +console.log((1, 2, objectWithArrayProperty?.arr['filter'](cond)).at(0)); + `, + errors: [ + { + line: 4, + messageId: 'preferFind', + suggestions: [ + { + messageId: 'preferFindSuggestion', + output: ` +declare const objectWithArrayProperty: { arr: unknown[] }; +declare function cond(x: unknown): boolean; +console.log((1, 2, objectWithArrayProperty?.arr["find"](cond))); + `, + }, + ], + }, + ], + }, + { + code: ` +[1, 2, 3].filter(x => x > 0).at(NaN); + `, + errors: [ + { + line: 2, + messageId: 'preferFind', + suggestions: [ + { + messageId: 'preferFindSuggestion', + output: ` +[1, 2, 3].find(x => x > 0); + `, + }, + ], + }, + ], + }, + { + code: ` +const idxToLookUp = -0.12635678; +[1, 2, 3].filter(x => x > 0).at(idxToLookUp); + `, + errors: [ + { + line: 3, + messageId: 'preferFind', + suggestions: [ + { + messageId: 'preferFindSuggestion', + output: ` +const idxToLookUp = -0.12635678; +[1, 2, 3].find(x => x > 0); + `, + }, + ], + }, + ], + }, + { + code: ` +[1, 2, 3].filter(x => x > 0)[\`at\`](0); + `, + errors: [ + { + line: 2, + messageId: 'preferFind', + suggestions: [ + { + messageId: 'preferFindSuggestion', + output: ` +[1, 2, 3].find(x => x > 0); + `, + }, + ], + }, + ], + }, + { + code: ` +declare const arr: string[]; +declare const cond: Parameters['filter']>[0]; +const a = { arr }; +a?.arr + .filter(cond) /* what a bad spot for a comment. Let's make sure + there's some yucky symbols too. [ . ?. <> ' ' \\'] */ + .at('0'); + `, + errors: [ + { + line: 5, + messageId: 'preferFind', + suggestions: [ + { + messageId: 'preferFindSuggestion', + output: ` +declare const arr: string[]; +declare const cond: Parameters['filter']>[0]; +const a = { arr }; +a?.arr + .find(cond) /* what a bad spot for a comment. Let's make sure + there's some yucky symbols too. [ . ?. <> ' ' \\'] */ + ; + `, + }, + ], + }, + ], + }, + { + code: ` +const imNotActuallyAnArray = [ + [1, 2, 3], + [2, 3, 4], +] as const; +const butIAm = [4, 5, 6]; +butIAm.push( + // line comment! + ...imNotActuallyAnArray[/* comment */ 'filter' /* another comment */]( + x => x[1] > 0, + ) /**/[\`0\`]!, +); + `, + errors: [ + { + line: 9, + messageId: 'preferFind', + suggestions: [ + { + messageId: 'preferFindSuggestion', + output: ` +const imNotActuallyAnArray = [ + [1, 2, 3], + [2, 3, 4], +] as const; +const butIAm = [4, 5, 6]; +butIAm.push( + // line comment! + ...imNotActuallyAnArray[/* comment */ "find" /* another comment */]( + x => x[1] > 0, + ) /**/!, +); + `, + }, + ], + }, + ], + }, + { + code: ` +function actingOnArray(values: T) { + return values.filter(filter => filter === 'filter')[ + /* filter */ -0.0 /* filter */ + ]; +} + `, + errors: [ + { + line: 3, + messageId: 'preferFind', + suggestions: [ + { + messageId: 'preferFindSuggestion', + output: ` +function actingOnArray(values: T) { + return values.find(filter => filter === 'filter'); +} + `, + }, + ], + }, + ], + }, + { + code: ` +const nestedSequenceAbomination = + (1, + 2, + (1, + 2, + 3, + (1, 2, 3, 4), + (1, 2, 3, 4, 5, [1, 2, 3, 4, 5, 6].filter(x => x % 2 == 0)))['0']); + `, + errors: [ + { + line: 5, + messageId: 'preferFind', + suggestions: [ + { + messageId: 'preferFindSuggestion', + output: ` +const nestedSequenceAbomination = + (1, + 2, + (1, + 2, + 3, + (1, 2, 3, 4), + (1, 2, 3, 4, 5, [1, 2, 3, 4, 5, 6].find(x => x % 2 == 0)))); + `, + }, + ], + }, + ], + }, + { + code: ` +declare const arr: { a: 1 }[] & { b: 2 }[]; +arr.filter(f, thisArg)[0]; + `, + errors: [ + { + line: 3, + messageId: 'preferFind', + suggestions: [ + { + messageId: 'preferFindSuggestion', + output: ` +declare const arr: { a: 1 }[] & { b: 2 }[]; +arr.find(f, thisArg); + `, + }, + ], + }, + ], + }, + { + code: ` +declare const arr: { a: 1 }[] & ({ b: 2 }[] | { c: 3 }[]); +arr.filter(f, thisArg)[0]; + `, + errors: [ + { + line: 3, + messageId: 'preferFind', + suggestions: [ + { + messageId: 'preferFindSuggestion', + output: ` +declare const arr: { a: 1 }[] & ({ b: 2 }[] | { c: 3 }[]); +arr.find(f, thisArg); + `, + }, + ], + }, + ], + }, + ], +}); diff --git a/packages/eslint-plugin/tests/schema-snapshots/prefer-find.shot b/packages/eslint-plugin/tests/schema-snapshots/prefer-find.shot new file mode 100644 index 000000000000..941a7a764176 --- /dev/null +++ b/packages/eslint-plugin/tests/schema-snapshots/prefer-find.shot @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Rule schemas should be convertible to TS types for documentation purposes prefer-find 1`] = ` +" +# SCHEMA: + +[] + + +# TYPES: + +/** No options declared */ +type Options = [];" +`; diff --git a/packages/utils/src/eslint-utils/nullThrows.ts b/packages/utils/src/eslint-utils/nullThrows.ts index 1a79b2e09d43..655ec2084682 100644 --- a/packages/utils/src/eslint-utils/nullThrows.ts +++ b/packages/utils/src/eslint-utils/nullThrows.ts @@ -11,7 +11,7 @@ const NullThrowsReasons = { * Assert that a value must not be null or undefined. * This is a nice explicit alternative to the non-null assertion operator. */ -function nullThrows(value: T | null | undefined, message: string): T { +function nullThrows(value: T, message: string): NonNullable { // this function is primarily used to keep types happy in a safe way // i.e. is used when we expect that a value is never nullish // this means that it's pretty much impossible to test the below if... pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy