-
Notifications
You must be signed in to change notification settings - Fork 4
/
no-partial-array-reduce.ts
137 lines (123 loc) · 4.56 KB
/
no-partial-array-reduce.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
/* eslint-disable functional/prefer-immutable-types */
import { AST_NODE_TYPES, ESLintUtils } from "@typescript-eslint/utils";
import { isTupleType, isTupleTypeReference, unionTypeParts } from "tsutils";
import { createRule } from "./common";
/**
* An ESLint rule to ban partial Array.prototype.reduce().
*/
// eslint-disable-next-line total-functions/no-unsafe-readonly-mutable-assignment
const noPartialArrayReduce = createRule({
name: "no-partial-array-reduce",
meta: {
type: "problem",
docs: {
description: "Bans partial Array.prototype.reduce()",
recommended: "error",
},
messages: {
errorStringGeneric:
"Array.prototype.reduce() is partial. It will throw if the array is empty. Provide an initial value or prove the array is non-empty to prevent this error.",
},
schema: [],
},
create: (context) => {
const parserServices = ESLintUtils.getParserServices(context);
const checker = parserServices.program.getTypeChecker();
return {
// eslint-disable-next-line functional/no-return-void, sonarjs/cognitive-complexity
CallExpression: (node) => {
// We only care if this call is a member expression.
// eslint-disable-next-line functional/no-conditional-statements
if (node.callee.type !== AST_NODE_TYPES.MemberExpression) {
return;
}
// Non-empty array literal are safe.
// eslint-disable-next-line functional/no-conditional-statements
if (
node.callee.object.type === AST_NODE_TYPES.ArrayExpression &&
node.callee.object.elements[0] !== undefined &&
node.callee.object.elements[0]?.type !== AST_NODE_TYPES.SpreadElement
) {
return;
}
// We only care if this call has exactly one argument.
// eslint-disable-next-line functional/no-conditional-statements
if (node.arguments.length !== 1) {
return;
}
const objectType = checker.getTypeAtLocation(
parserServices.esTreeNodeToTSNodeMap.get(node.callee.object),
);
const typeParts = unionTypeParts(objectType);
// We only care if this call is on an array or tuple (or a type that is a union that includes one or more arrays or tuples)
// eslint-disable-next-line functional/no-conditional-statements
if (
!typeParts.some(
(t) =>
checker.isArrayType(t) ||
isTupleType(t) ||
isTupleTypeReference(t),
)
) {
return;
}
const isProvablyNonEmpty = typeParts.every((t) => {
return (
(isTupleType(t) && t.minLength >= 1) ||
(isTupleTypeReference(t) && t.target.minLength >= 1)
);
});
// eslint-disable-next-line functional/no-conditional-statements
if (isProvablyNonEmpty) {
return;
}
const unsafeMethods: readonly string[] = [
"reduce",
"reduceRight",
] as const;
// Detect this form:
// [].reduce(() => "")
//
// Or this form:
// []["reduce"](() => "")
//
// Or this form:
// const n = "reduce" as const;
// const foo = [][n](() => "");
//
// Or this form:
// declare const n: "reduce" | "reduceRight";
// const foo = [""][n](() => "");
const isReduce =
(node.callee.property.type === AST_NODE_TYPES.Literal &&
node.callee.computed &&
typeof node.callee.property.value === "string" &&
unsafeMethods.includes(node.callee.property.value)) ||
(node.callee.property.type === AST_NODE_TYPES.Identifier &&
!node.callee.computed &&
unsafeMethods.includes(node.callee.property.name)) ||
(node.callee.property.type === AST_NODE_TYPES.Identifier &&
node.callee.computed &&
unionTypeParts(
checker.getTypeAtLocation(
parserServices.esTreeNodeToTSNodeMap.get(node.callee.property),
),
).some(
(type) =>
type.isStringLiteral() && unsafeMethods.includes(type.value),
));
// eslint-disable-next-line functional/no-conditional-statements
if (!isReduce) {
return;
}
// eslint-disable-next-line functional/no-expression-statements
context.report({
node: node,
messageId: "errorStringGeneric",
} as const);
},
};
},
defaultOptions: [],
} as const);
export default noPartialArrayReduce;