diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index b7927757924..8987310f174 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -132,6 +132,7 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int | [`@typescript-eslint/no-unnecessary-qualifier`](./docs/rules/no-unnecessary-qualifier.md) | Warns when a namespace qualifier is unnecessary | | :wrench: | :thought_balloon: | | [`@typescript-eslint/no-unnecessary-type-arguments`](./docs/rules/no-unnecessary-type-arguments.md) | Enforces that type arguments will not be used if not required | | :wrench: | :thought_balloon: | | [`@typescript-eslint/no-unnecessary-type-assertion`](./docs/rules/no-unnecessary-type-assertion.md) | Warns if a type assertion does not change the type of an expression | :heavy_check_mark: | :wrench: | :thought_balloon: | +| [`@typescript-eslint/no-unsafe-array-reduce`](./docs/rules/no-unsafe-array-reduce.md) | Disallows inferring wide type on array reduce | | | :thought_balloon: | | [`@typescript-eslint/no-unsafe-call`](./docs/rules/no-unsafe-call.md) | Disallows calling an any type value | | | :thought_balloon: | | [`@typescript-eslint/no-unsafe-member-access`](./docs/rules/no-unsafe-member-access.md) | Disallows member access on any typed variables | | | :thought_balloon: | | [`@typescript-eslint/no-unsafe-return`](./docs/rules/no-unsafe-return.md) | Disallows returning any from a function | | | :thought_balloon: | diff --git a/packages/eslint-plugin/docs/rules/no-unsafe-array-reduce.md b/packages/eslint-plugin/docs/rules/no-unsafe-array-reduce.md new file mode 100644 index 00000000000..c91d2a185c2 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/no-unsafe-array-reduce.md @@ -0,0 +1,38 @@ +# Disallows inferring wide type on array reduce (`no-unsafe-array-reduce`) + +## Rule Details + +The type of {} is {}. Any object is assignable to {}. {} is assignable +to any indexed type (`{[key in string]: whatever}`) + +A reduce call with an empty object initializer and no type signature, +will infer the {} type for the accumulator and result of the reduce +expression. Since anything is assignable to {}, this means the reduce +function is essentially unchecked. The result of the expression can then +also be assigned to an incompatible type without raising any errors. + +This rule warns if a reduce call takes an empty object as the initial +value and has no type signatures. + +Examples of **incorrect** code for this rule: + +```ts +/* eslint @typescript-eslint/no-unsafe-array-reduce: ["error"] */ + +[].reduce((acc, cur) => acc, {}); +``` + +Examples of **correct** code for this rule: + +```ts +/* eslint @typescript-eslint/no-unsafe-array-reduce: ["error"] */ + +// Type parameter is fine +[].reduce((acc, _) => acc, {}); + +// Typed accumulator is fine +[].reduce((acc: Dict, _) => acc, {}); + +// Typed init value is fine +[].reduce((acc, _) => acc, {} as Dict); +``` diff --git a/packages/eslint-plugin/src/configs/all.json b/packages/eslint-plugin/src/configs/all.json index 2626767b84c..70f929f221e 100644 --- a/packages/eslint-plugin/src/configs/all.json +++ b/packages/eslint-plugin/src/configs/all.json @@ -61,6 +61,7 @@ "@typescript-eslint/no-unnecessary-qualifier": "error", "@typescript-eslint/no-unnecessary-type-arguments": "error", "@typescript-eslint/no-unnecessary-type-assertion": "error", + "@typescript-eslint/no-unsafe-array-reduce": "error", "@typescript-eslint/no-unsafe-call": "error", "@typescript-eslint/no-unsafe-member-access": "error", "@typescript-eslint/no-unsafe-return": "error", diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index 486b8a97945..d75559320eb 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -53,6 +53,7 @@ import noUnnecessaryCondition from './no-unnecessary-condition'; import noUnnecessaryQualifier from './no-unnecessary-qualifier'; import noUnnecessaryTypeArguments from './no-unnecessary-type-arguments'; import noUnnecessaryTypeAssertion from './no-unnecessary-type-assertion'; +import noUnsafeArrayReduce from './no-unsafe-array-reduce'; import noUnsafeCall from './no-unsafe-call'; import noUnsafeMemberAccess from './no-unsafe-member-access'; import noUnsafeReturn from './no-unsafe-return'; @@ -147,6 +148,7 @@ export default { 'no-unnecessary-qualifier': noUnnecessaryQualifier, 'no-unnecessary-type-arguments': noUnnecessaryTypeArguments, 'no-unnecessary-type-assertion': noUnnecessaryTypeAssertion, + 'no-unsafe-array-reduce': noUnsafeArrayReduce, 'no-unsafe-call': noUnsafeCall, 'no-unsafe-member-access': noUnsafeMemberAccess, 'no-unsafe-return': noUnsafeReturn, diff --git a/packages/eslint-plugin/src/rules/no-unsafe-array-reduce.ts b/packages/eslint-plugin/src/rules/no-unsafe-array-reduce.ts new file mode 100644 index 00000000000..fb7c08bd9f5 --- /dev/null +++ b/packages/eslint-plugin/src/rules/no-unsafe-array-reduce.ts @@ -0,0 +1,74 @@ +import { TSESTree } from '@typescript-eslint/experimental-utils'; +import * as ts from 'typescript'; +import * as util from '../util'; + +type MessageIds = 'unsafeArrayReduce'; + +export default util.createRule<[], MessageIds>({ + name: 'no-unsafe-array-reduce', + meta: { + type: 'problem', + docs: { + description: 'Disallows inferring wide type on array reduce', + category: 'Possible Errors', + recommended: false, + requiresTypeChecking: true, + }, + messages: { + unsafeArrayReduce: + 'This reduce call does not have sufficient type information to be safe.', + }, + schema: [], + }, + defaultOptions: [], + + create(context) { + const parserServices = util.getParserServices(context); + const checker = parserServices.program.getTypeChecker(); + + return { + CallExpression(node: TSESTree.CallExpression): void { + const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node); + + // foo.reduce(...) + // ^ ^ ^ + // | \ - node.typeArguments + // \ - node.expression.name + // - node.expression.expression + if (ts.isPropertyAccessExpression(originalNode.expression)) { + const isArray = checker.isArrayType( + checker.getTypeAtLocation(originalNode.expression.expression), + ); + + if (isArray && originalNode.expression.name.text === 'reduce') { + const funcArgument: ts.Expression | undefined = + originalNode.arguments[0]; + const initArgument: ts.Expression | undefined = + originalNode.arguments[1]; + + if ( + // Init argument is an empty object literal (with no type assertion, + // in case you're a bad developer and have disabled no literal type + // assertions) + initArgument && + ts.isObjectLiteralExpression(initArgument) && + !initArgument.properties.length && + !ts.isAsExpression(initArgument) && + // There's no type argument reduce + !originalNode.typeArguments && + // There's no accumulator type declaration + ts.isArrowFunction(funcArgument) && + funcArgument.parameters[0] && + !funcArgument.parameters[0].type + ) { + context.report({ + node, + messageId: 'unsafeArrayReduce', + }); + } + } + } + }, + }; + }, +}); diff --git a/packages/eslint-plugin/tests/rules/no-unsafe-array-reduce.test.ts b/packages/eslint-plugin/tests/rules/no-unsafe-array-reduce.test.ts new file mode 100644 index 00000000000..eb03bc2f77b --- /dev/null +++ b/packages/eslint-plugin/tests/rules/no-unsafe-array-reduce.test.ts @@ -0,0 +1,67 @@ +import path from 'path'; +import rule from '../../src/rules/no-unsafe-array-reduce'; +import { RuleTester } from '../RuleTester'; + +const rootDir = path.resolve(__dirname, '../fixtures/'); +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', + parserOptions: { + sourceType: 'module', + tsconfigRootDir: rootDir, + project: './tsconfig.json', + }, +}); + +ruleTester.run('no-unsafe-array-reduce', rule, { + valid: [ + { + // Type parameter is fine + code: ` +type Dict = { [key in string]?: string } +[].reduce((acc, _) => acc, {});`, + }, + { + // Typed accumulator is fine + code: ` +type Dict = { [key in string]?: string } +[].reduce((acc: Dict, _) => acc, {});`, + }, + { + // Typed init value is fine + code: ` +type Dict = { [key in string]?: string } +[].reduce((acc, _) => acc, {} as Dict);`, + }, + { + // Non-object init value is fine + code: ` +type Dict = { [key in string]?: string } +[].reduce((acc, _) => acc, []);`, + }, + { + // Non-reduce is fine + code: `[].filter(() => true);`, + }, + { + // Non-array reduce is fine + code: `reduce((acc, _) => acc, {});`, + }, + ], + + invalid: [ + { + code: `[].reduce((acc, cur) => acc, {});`, + output: `[].reduce((acc, cur) => acc, {});`, + errors: [{ messageId: 'unsafeArrayReduce' }], + }, + { + code: ` +const arr = [] +arr.reduce((acc, cur) => acc, {});`, + output: ` +const arr = [] +arr.reduce((acc, cur) => acc, {});`, + errors: [{ messageId: 'unsafeArrayReduce' }], + }, + ], +});