From b85866da82f954dd88d64adbc7b0a720d2971817 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85smund=20Grammeltvedt?= Date: Thu, 19 Mar 2020 21:00:04 +0100 Subject: [PATCH 1/3] feat(eslint-plugin): [no-unsafe-array-reduce] Add rule Add rule for protecting against unsafe array reduce calls. 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. --- packages/eslint-plugin/README.md | 1 + .../docs/rules/no-unsafe-array-reduce.md | 38 ++++++++++ packages/eslint-plugin/src/configs/all.json | 1 + packages/eslint-plugin/src/rules/index.ts | 2 + .../src/rules/no-unsafe-array-reduce.ts | 74 +++++++++++++++++++ .../rules/no-unsafe-array-reduce.test.ts | 67 +++++++++++++++++ 6 files changed, 183 insertions(+) create mode 100644 packages/eslint-plugin/docs/rules/no-unsafe-array-reduce.md create mode 100644 packages/eslint-plugin/src/rules/no-unsafe-array-reduce.ts create mode 100644 packages/eslint-plugin/tests/rules/no-unsafe-array-reduce.test.ts diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index b79277579247..8987310f174a 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 000000000000..c91d2a185c22 --- /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 2626767b84cc..70f929f221e4 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 486b8a97945b..d75559320eb8 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 000000000000..a47f28e1c3da --- /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 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 000000000000..eb03bc2f77bd --- /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' }], + }, + ], +}); From 3debc91cff0f377665d9e2fee624ba55da7ba544 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85smund=20Grammeltvedt?= Date: Sat, 21 Mar 2020 16:17:04 +0100 Subject: [PATCH 2/3] Avoid default import --- packages/eslint-plugin/src/rules/no-unsafe-array-reduce.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/no-unsafe-array-reduce.ts b/packages/eslint-plugin/src/rules/no-unsafe-array-reduce.ts index a47f28e1c3da..fb7c08bd9f56 100644 --- a/packages/eslint-plugin/src/rules/no-unsafe-array-reduce.ts +++ b/packages/eslint-plugin/src/rules/no-unsafe-array-reduce.ts @@ -1,5 +1,5 @@ import { TSESTree } from '@typescript-eslint/experimental-utils'; -import ts from 'typescript'; +import * as ts from 'typescript'; import * as util from '../util'; type MessageIds = 'unsafeArrayReduce'; From 0d36dda175617f0bb3abe2f48c3b0ff9daaf1a4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85smund=20Grammeltvedt?= Date: Sat, 21 Mar 2020 18:40:09 +0100 Subject: [PATCH 3/3] Force CI