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...