Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(eslint-plugin): [no-unsafe-array-reduce] Add rule #1779

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/eslint-plugin/README.md
Expand Up @@ -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: |
Expand Down
38 changes: 38 additions & 0 deletions 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<Dict>((acc, _) => acc, {});

// Typed accumulator is fine
[].reduce((acc: Dict, _) => acc, {});

// Typed init value is fine
[].reduce((acc, _) => acc, {} as Dict);
```
1 change: 1 addition & 0 deletions packages/eslint-plugin/src/configs/all.json
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions packages/eslint-plugin/src/rules/index.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand Down
74 changes: 74 additions & 0 deletions 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<T>(...)
// ^ ^ ^
// | \ - 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',
});
}
}
}
},
};
},
});
67 changes: 67 additions & 0 deletions 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<Dict>((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' }],
},
],
});