@blumintinc/eslint-plugin-blumint 1.20.94 → 1.20.96
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/lib/index.js +1 -1
- package/lib/rules/enforce-css-media-queries.d.ts +2 -1
- package/lib/rules/enforce-css-media-queries.js +203 -8
- package/lib/rules/enforce-exported-function-types.js +209 -6
- package/lib/rules/enforce-firestore-doc-ref-generic.js +71 -18
- package/lib/rules/enforce-mui-rounded-icons.d.ts +2 -1
- package/lib/rules/enforce-mui-rounded-icons.js +194 -13
- package/lib/rules/generic-starts-with-t.js +49 -0
- package/lib/rules/global-const-style.js +101 -51
- package/lib/rules/no-always-true-false-conditions.js +231 -62
- package/lib/rules/no-redundant-param-types.js +69 -37
- package/lib/rules/no-unnecessary-verb-suffix.js +52 -5
- package/lib/rules/parallelize-loop-awaits.d.ts +1 -0
- package/lib/rules/parallelize-loop-awaits.js +233 -43
- package/lib/rules/prefer-getter-over-parameterless-method.d.ts +2 -1
- package/lib/rules/prefer-getter-over-parameterless-method.js +259 -0
- package/lib/rules/prefer-type-alias-over-typeof-constant.js +63 -4
- package/lib/rules/require-https-error.js +6 -2
- package/lib/rules/use-custom-link.js +45 -1
- package/lib/rules/use-custom-memo.js +30 -0
- package/lib/rules/use-custom-router.js +32 -2
- package/package.json +1 -1
- package/release-manifest.json +142 -0
|
@@ -30,6 +30,28 @@ exports.noAlwaysTrueFalseConditions = (0, createRule_1.createRule)({
|
|
|
30
30
|
const evaluatedParentNodes = new Set();
|
|
31
31
|
// Sentinel distinguishes "no const literal resolved" from valid literal values (including null/undefined)
|
|
32
32
|
const NOT_FOUND = Symbol('literal-not-found');
|
|
33
|
+
const ASSERTION_EXPRESSION_TYPES = new Set([
|
|
34
|
+
utils_1.AST_NODE_TYPES.TSAsExpression,
|
|
35
|
+
utils_1.AST_NODE_TYPES.TSSatisfiesExpression,
|
|
36
|
+
utils_1.AST_NODE_TYPES.TSNonNullExpression,
|
|
37
|
+
utils_1.AST_NODE_TYPES.TSTypeAssertion,
|
|
38
|
+
]);
|
|
39
|
+
/**
|
|
40
|
+
* Strips type/non-null assertions from an expression. An assertion only
|
|
41
|
+
* describes a value, so the value behind it is exactly as knowable as the
|
|
42
|
+
* value itself — `"a" as const` is *more* certain than `"a"`, never less.
|
|
43
|
+
* Every resolution site strips them, otherwise a wrapper silently retires
|
|
44
|
+
* detection: sibling fixers add `as const` to module-scope constants, so a
|
|
45
|
+
* resolver that stopped at the wrapper would go blind on each constant they
|
|
46
|
+
* touch.
|
|
47
|
+
*/
|
|
48
|
+
function unwrapAssertions(node) {
|
|
49
|
+
let current = node;
|
|
50
|
+
while (ASSERTION_EXPRESSION_TYPES.has(current.type)) {
|
|
51
|
+
current = current.expression;
|
|
52
|
+
}
|
|
53
|
+
return current;
|
|
54
|
+
}
|
|
33
55
|
/**
|
|
34
56
|
* Walks the current scope chain to locate a variable by name so identifier
|
|
35
57
|
* lookups can resolve const initializers even when declared in parent scopes.
|
|
@@ -71,11 +93,74 @@ exports.noAlwaysTrueFalseConditions = (0, createRule_1.createRule)({
|
|
|
71
93
|
return NOT_FOUND;
|
|
72
94
|
}
|
|
73
95
|
const initializer = definition.node.init;
|
|
74
|
-
if (initializer
|
|
75
|
-
return
|
|
96
|
+
if (!initializer) {
|
|
97
|
+
return NOT_FOUND;
|
|
98
|
+
}
|
|
99
|
+
const value = unwrapAssertions(initializer);
|
|
100
|
+
if (value.type === utils_1.AST_NODE_TYPES.Literal) {
|
|
101
|
+
return value.value;
|
|
76
102
|
}
|
|
77
103
|
return NOT_FOUND;
|
|
78
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* A const binding freezes the binding, not the object it points at, so a
|
|
107
|
+
* resolved object literal only stays constant while every reference merely
|
|
108
|
+
* reads a property off it. Writing through a member expression, or handing
|
|
109
|
+
* the object to anything that can retain and mutate it, makes the property
|
|
110
|
+
* values unknowable from the declaration alone.
|
|
111
|
+
*/
|
|
112
|
+
function isPropertyReadReference(reference) {
|
|
113
|
+
const { identifier } = reference;
|
|
114
|
+
const parent = identifier.parent;
|
|
115
|
+
if (!parent) {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
// The declaration's own initialization write.
|
|
119
|
+
if (parent.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
120
|
+
parent.id === identifier) {
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
if (parent.type !== utils_1.AST_NODE_TYPES.MemberExpression ||
|
|
124
|
+
parent.object !== identifier) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
const access = parent.parent?.type === utils_1.AST_NODE_TYPES.ChainExpression
|
|
128
|
+
? parent.parent.parent
|
|
129
|
+
: parent.parent;
|
|
130
|
+
if (!access) {
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
const writesTheProperty = (access.type === utils_1.AST_NODE_TYPES.AssignmentExpression &&
|
|
134
|
+
access.left === parent) ||
|
|
135
|
+
access.type === utils_1.AST_NODE_TYPES.UpdateExpression ||
|
|
136
|
+
(access.type === utils_1.AST_NODE_TYPES.UnaryExpression &&
|
|
137
|
+
access.operator === 'delete');
|
|
138
|
+
return !writesTheProperty;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Resolves an identifier to the object literal it is initialized with when
|
|
142
|
+
* declared as a const nothing can mutate. Returns undefined whenever the
|
|
143
|
+
* binding is absent, is not a const, is initialized with anything other
|
|
144
|
+
* than an object literal, or escapes to code that could rewrite it.
|
|
145
|
+
*/
|
|
146
|
+
function resolveIdentifierObject(identifier) {
|
|
147
|
+
const variable = findVariableInScopes(identifier.name);
|
|
148
|
+
if (!variable) {
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
const definition = findConstIdentifierDefinition(variable, identifier.name);
|
|
152
|
+
if (!definition?.node.init) {
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
const initializer = unwrapAssertions(definition.node.init);
|
|
156
|
+
if (initializer.type !== utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
if (!variable.references.every(isPropertyReadReference)) {
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
return initializer;
|
|
163
|
+
}
|
|
79
164
|
/**
|
|
80
165
|
* Checks if a literal value is always truthy or falsy
|
|
81
166
|
*/
|
|
@@ -95,6 +180,117 @@ exports.noAlwaysTrueFalseConditions = (0, createRule_1.createRule)({
|
|
|
95
180
|
return {};
|
|
96
181
|
}
|
|
97
182
|
}
|
|
183
|
+
/**
|
|
184
|
+
* Classifies an expression that a constant declaration binds, so a resolved
|
|
185
|
+
* property value is judged by what it is rather than by where it was found.
|
|
186
|
+
* Anything whose value depends on runtime state stays unresolved.
|
|
187
|
+
*/
|
|
188
|
+
function checkConstantValue(node) {
|
|
189
|
+
const value = unwrapAssertions(node);
|
|
190
|
+
switch (value.type) {
|
|
191
|
+
case utils_1.AST_NODE_TYPES.Literal:
|
|
192
|
+
// A regex literal is an object, and its `value` is unusable when the
|
|
193
|
+
// linting runtime cannot construct the pattern.
|
|
194
|
+
return 'regex' in value
|
|
195
|
+
? { isTruthy: true }
|
|
196
|
+
: checkLiteralValue(value);
|
|
197
|
+
case utils_1.AST_NODE_TYPES.ObjectExpression:
|
|
198
|
+
case utils_1.AST_NODE_TYPES.ArrayExpression:
|
|
199
|
+
case utils_1.AST_NODE_TYPES.ArrowFunctionExpression:
|
|
200
|
+
case utils_1.AST_NODE_TYPES.FunctionExpression:
|
|
201
|
+
case utils_1.AST_NODE_TYPES.ClassExpression:
|
|
202
|
+
return { isTruthy: true };
|
|
203
|
+
case utils_1.AST_NODE_TYPES.TemplateLiteral:
|
|
204
|
+
return value.expressions.length === 0
|
|
205
|
+
? value.quasis[0].value.raw === ''
|
|
206
|
+
? { isFalsy: true }
|
|
207
|
+
: { isTruthy: true }
|
|
208
|
+
: {};
|
|
209
|
+
default:
|
|
210
|
+
return {};
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Reads the property name a member expression accesses. Returns undefined
|
|
215
|
+
* when the key is computed from something other than a literal, since the
|
|
216
|
+
* accessed property is then unknown at lint time.
|
|
217
|
+
*/
|
|
218
|
+
function memberPropertyName(member) {
|
|
219
|
+
if (member.computed) {
|
|
220
|
+
const key = unwrapAssertions(member.property);
|
|
221
|
+
return key.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
222
|
+
(typeof key.value === 'string' || typeof key.value === 'number')
|
|
223
|
+
? String(key.value)
|
|
224
|
+
: undefined;
|
|
225
|
+
}
|
|
226
|
+
return member.property.type === utils_1.AST_NODE_TYPES.Identifier
|
|
227
|
+
? member.property.name
|
|
228
|
+
: undefined;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Reads the key an object literal property declares, mirroring the member
|
|
232
|
+
* access side so both are compared as strings.
|
|
233
|
+
*/
|
|
234
|
+
function propertyKeyName(property) {
|
|
235
|
+
const { key } = property;
|
|
236
|
+
if (key.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
237
|
+
(typeof key.value === 'string' || typeof key.value === 'number')) {
|
|
238
|
+
return String(key.value);
|
|
239
|
+
}
|
|
240
|
+
return !property.computed && key.type === utils_1.AST_NODE_TYPES.Identifier
|
|
241
|
+
? key.name
|
|
242
|
+
: undefined;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Evaluates a property read against a resolved object literal. The result
|
|
246
|
+
* stays empty whenever the literal's shape hides the answer — a spread, a
|
|
247
|
+
* key computed at runtime, an accessor — and whenever the property is
|
|
248
|
+
* absent, because a missing own property can still resolve through the
|
|
249
|
+
* prototype chain (`config.toString`) or through declaration merging.
|
|
250
|
+
*/
|
|
251
|
+
function checkObjectPropertyAccess(objectExpression, member) {
|
|
252
|
+
const propertyName = memberPropertyName(member);
|
|
253
|
+
if (propertyName === undefined) {
|
|
254
|
+
return {};
|
|
255
|
+
}
|
|
256
|
+
let resolvedValue;
|
|
257
|
+
for (const property of objectExpression.properties) {
|
|
258
|
+
if (property.type !== utils_1.AST_NODE_TYPES.Property) {
|
|
259
|
+
// A spread can contribute or overwrite any key.
|
|
260
|
+
return {};
|
|
261
|
+
}
|
|
262
|
+
const keyName = propertyKeyName(property);
|
|
263
|
+
if (keyName === undefined) {
|
|
264
|
+
return {};
|
|
265
|
+
}
|
|
266
|
+
if (keyName === propertyName) {
|
|
267
|
+
if (property.kind !== 'init') {
|
|
268
|
+
// An accessor returns whatever it likes.
|
|
269
|
+
return {};
|
|
270
|
+
}
|
|
271
|
+
// A later duplicate key wins.
|
|
272
|
+
resolvedValue = property.value;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return resolvedValue ? checkConstantValue(resolvedValue) : {};
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Evaluates a member access whose object the rule can pin to an object
|
|
279
|
+
* literal, whether the literal is written inline or reached through a const
|
|
280
|
+
* binding. Optional chaining off such an object behaves like plain access,
|
|
281
|
+
* because the object is known not to be nullish.
|
|
282
|
+
*/
|
|
283
|
+
function checkMemberAccess(member) {
|
|
284
|
+
const object = unwrapAssertions(member.object);
|
|
285
|
+
if (object.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
286
|
+
return checkObjectPropertyAccess(object, member);
|
|
287
|
+
}
|
|
288
|
+
if (object.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
289
|
+
const resolved = resolveIdentifierObject(object);
|
|
290
|
+
return resolved ? checkObjectPropertyAccess(resolved, member) : {};
|
|
291
|
+
}
|
|
292
|
+
return {};
|
|
293
|
+
}
|
|
98
294
|
const MATH_FOLDABLE_METHODS = new Set(['max', 'min']);
|
|
99
295
|
/**
|
|
100
296
|
* Resolves a `Math.max`/`Math.min` call over numeric literals to the number
|
|
@@ -130,11 +326,12 @@ exports.noAlwaysTrueFalseConditions = (0, createRule_1.createRule)({
|
|
|
130
326
|
* Returns undefined when the operand depends on runtime values.
|
|
131
327
|
*/
|
|
132
328
|
function resolveNumericOperand(node) {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
329
|
+
const operand = unwrapAssertions(node);
|
|
330
|
+
if (operand.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
331
|
+
typeof operand.value === 'number') {
|
|
332
|
+
return operand.value;
|
|
136
333
|
}
|
|
137
|
-
return evaluateMathMinMaxCall(
|
|
334
|
+
return evaluateMathMinMaxCall(operand);
|
|
138
335
|
}
|
|
139
336
|
function compareNumericValues(leftValue, operator, rightValue) {
|
|
140
337
|
switch (operator) {
|
|
@@ -172,13 +369,16 @@ exports.noAlwaysTrueFalseConditions = (0, createRule_1.createRule)({
|
|
|
172
369
|
* Checks if a binary expression with literals is always truthy or falsy
|
|
173
370
|
*/
|
|
174
371
|
function checkBinaryExpression(node) {
|
|
372
|
+
// An assertion around an operand leaves the compared value untouched.
|
|
373
|
+
const left = unwrapAssertions(node.left);
|
|
374
|
+
const right = unwrapAssertions(node.right);
|
|
175
375
|
// Check for bitwise operations
|
|
176
376
|
if (node.operator === '&') {
|
|
177
|
-
if (
|
|
178
|
-
|
|
179
|
-
typeof
|
|
180
|
-
typeof
|
|
181
|
-
const result =
|
|
377
|
+
if (left.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
378
|
+
right.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
379
|
+
typeof left.value === 'number' &&
|
|
380
|
+
typeof right.value === 'number') {
|
|
381
|
+
const result = left.value & right.value;
|
|
182
382
|
if (result === 0) {
|
|
183
383
|
return { isFalsy: true };
|
|
184
384
|
}
|
|
@@ -190,17 +390,17 @@ exports.noAlwaysTrueFalseConditions = (0, createRule_1.createRule)({
|
|
|
190
390
|
// A non-literal operand can still be a compile-time constant, so resolve
|
|
191
391
|
// it before abandoning the comparison. Bailing out unconditionally here
|
|
192
392
|
// hid every folded-call comparison (Math.max(1, 2) === 0) from the rule.
|
|
193
|
-
if (
|
|
194
|
-
|
|
195
|
-
const resolvedLeft = resolveNumericOperand(
|
|
196
|
-
const resolvedRight = resolveNumericOperand(
|
|
393
|
+
if (left.type !== utils_1.AST_NODE_TYPES.Literal ||
|
|
394
|
+
right.type !== utils_1.AST_NODE_TYPES.Literal) {
|
|
395
|
+
const resolvedLeft = resolveNumericOperand(left);
|
|
396
|
+
const resolvedRight = resolveNumericOperand(right);
|
|
197
397
|
if (resolvedLeft === undefined || resolvedRight === undefined) {
|
|
198
398
|
return {};
|
|
199
399
|
}
|
|
200
400
|
return compareNumericValues(resolvedLeft, node.operator, resolvedRight);
|
|
201
401
|
}
|
|
202
|
-
const leftValue =
|
|
203
|
-
const rightValue =
|
|
402
|
+
const leftValue = left.value;
|
|
403
|
+
const rightValue = right.value;
|
|
204
404
|
// Skip if either value is null or undefined
|
|
205
405
|
if (leftValue === null ||
|
|
206
406
|
leftValue === undefined ||
|
|
@@ -376,15 +576,6 @@ exports.noAlwaysTrueFalseConditions = (0, createRule_1.createRule)({
|
|
|
376
576
|
}
|
|
377
577
|
return {};
|
|
378
578
|
}
|
|
379
|
-
/**
|
|
380
|
-
* Check if a node is an "as const" expression
|
|
381
|
-
*/
|
|
382
|
-
function isAsConstExpression(node) {
|
|
383
|
-
return (node.type === utils_1.AST_NODE_TYPES.TSAsExpression &&
|
|
384
|
-
node.typeAnnotation.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
|
|
385
|
-
node.typeAnnotation.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
386
|
-
node.typeAnnotation.typeName.name === 'const');
|
|
387
|
-
}
|
|
388
579
|
/**
|
|
389
580
|
* Check if a logical expression is being used for default value assignment
|
|
390
581
|
* This includes patterns like:
|
|
@@ -870,6 +1061,11 @@ exports.noAlwaysTrueFalseConditions = (0, createRule_1.createRule)({
|
|
|
870
1061
|
if (evaluatedParentNodes.has(node)) {
|
|
871
1062
|
return {};
|
|
872
1063
|
}
|
|
1064
|
+
// An assertion wrapper carries the same value as the expression inside it
|
|
1065
|
+
const unwrapped = unwrapAssertions(node);
|
|
1066
|
+
if (unwrapped !== node) {
|
|
1067
|
+
return evaluateConstantExpression(unwrapped);
|
|
1068
|
+
}
|
|
873
1069
|
// Check special constants and identifiers first
|
|
874
1070
|
const specialResult = checkSpecialValues(node);
|
|
875
1071
|
if (specialResult.isTruthy || specialResult.isFalsy) {
|
|
@@ -950,11 +1146,6 @@ exports.noAlwaysTrueFalseConditions = (0, createRule_1.createRule)({
|
|
|
950
1146
|
return { isFalsy: true };
|
|
951
1147
|
}
|
|
952
1148
|
}
|
|
953
|
-
// "as const" expressions
|
|
954
|
-
if (isAsConstExpression(node) &&
|
|
955
|
-
node.expression.type === utils_1.AST_NODE_TYPES.Literal) {
|
|
956
|
-
return checkLiteralValue(node.expression);
|
|
957
|
-
}
|
|
958
1149
|
// Special checks for const variables in tests
|
|
959
1150
|
if (node.type === utils_1.AST_NODE_TYPES.BinaryExpression) {
|
|
960
1151
|
if (node.left.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
@@ -1056,38 +1247,16 @@ exports.noAlwaysTrueFalseConditions = (0, createRule_1.createRule)({
|
|
|
1056
1247
|
return evaluateConstantExpression(logicalNode.left);
|
|
1057
1248
|
}
|
|
1058
1249
|
}
|
|
1059
|
-
//
|
|
1250
|
+
// Optional chaining off an object the rule can pin to an object literal.
|
|
1251
|
+
// The guard the operator adds is dead there, and the property it reads is
|
|
1252
|
+
// as knowable as the literal that declares it, whatever the binding and
|
|
1253
|
+
// the property are named. Anything else — a binding initialized from a
|
|
1254
|
+
// call, a parameter, a value that may legitimately be undefined — stays
|
|
1255
|
+
// unresolved.
|
|
1060
1256
|
if (node.type === utils_1.AST_NODE_TYPES.ChainExpression &&
|
|
1061
|
-
node.expression.type === utils_1.AST_NODE_TYPES.MemberExpression
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
if (
|
|
1065
|
-
// Only consider it always truthy if it's a literal object expression
|
|
1066
|
-
node.expression.object.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
1067
|
-
// If we have a property access and we know the object is a literal object
|
|
1068
|
-
return { isTruthy: true };
|
|
1069
|
-
}
|
|
1070
|
-
// Special case for identifiers that are explicitly defined in the same scope
|
|
1071
|
-
// This is to handle test cases like `const obj = { prop: "value" }; if (obj?.prop) {}`
|
|
1072
|
-
if (node.expression.object.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
1073
|
-
node.expression.object.name === 'obj' &&
|
|
1074
|
-
node.expression.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
1075
|
-
node.expression.property.name === 'prop') {
|
|
1076
|
-
// This is specifically for the test case "obj?.prop"
|
|
1077
|
-
return { isTruthy: true };
|
|
1078
|
-
}
|
|
1079
|
-
// Special case for array length checks with optional chaining
|
|
1080
|
-
// This handles cases like `filtered?.length` where filtered could be undefined
|
|
1081
|
-
if (node.expression.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
1082
|
-
node.expression.property.name === 'length') {
|
|
1083
|
-
// For array length checks with optional chaining, we can't determine
|
|
1084
|
-
// if the condition is always truthy or falsy, so return empty result
|
|
1085
|
-
return {};
|
|
1086
|
-
}
|
|
1087
|
-
// For other cases like arrays, identifiers, etc., we can't determine
|
|
1088
|
-
// if the condition is always truthy or falsy, so return empty result
|
|
1089
|
-
return {};
|
|
1090
|
-
}
|
|
1257
|
+
node.expression.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
1258
|
+
node.expression.optional) {
|
|
1259
|
+
return checkMemberAccess(node.expression);
|
|
1091
1260
|
}
|
|
1092
1261
|
// Handle Object.keys().length
|
|
1093
1262
|
if (node.type === utils_1.AST_NODE_TYPES.BinaryExpression &&
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.noRedundantParamTypes = void 0;
|
|
4
4
|
const utils_1 = require("@typescript-eslint/utils");
|
|
5
5
|
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
const disableDirectives_1 = require("../utils/disableDirectives");
|
|
6
7
|
const importRemoval_1 = require("../utils/importRemoval");
|
|
7
8
|
/**
|
|
8
9
|
* The annotation a parameter carries, or `undefined` when it has none. A
|
|
@@ -68,57 +69,88 @@ exports.noRedundantParamTypes = (0, createRule_1.createRule)({
|
|
|
68
69
|
create(context) {
|
|
69
70
|
const sourceCode = context.getSourceCode();
|
|
70
71
|
/**
|
|
71
|
-
*
|
|
72
|
+
* An annotation is stripped together with any import it was the only
|
|
72
73
|
* consumer of. The two are one fix: applying either half alone leaves the
|
|
73
74
|
* file worse than applying neither — a stripped annotation with its import
|
|
74
75
|
* left behind fails `no-unused-vars`, and since this rule's own report is
|
|
75
76
|
* resolved by the fix, nothing re-reports the debt.
|
|
76
77
|
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
78
|
+
* Reporting is therefore deferred to `Program:exit`: an import is unbound
|
|
79
|
+
* only once no reference to it survives the fix, and a file where two
|
|
80
|
+
* annotations name the same imported type strips both in a single pass.
|
|
81
|
+
* Judging each removal alone sees the sibling annotation still standing,
|
|
82
|
+
* concludes the binding is alive, and leaves the import stranded with no
|
|
83
|
+
* later pass to notice.
|
|
84
|
+
*/
|
|
85
|
+
const sites = [];
|
|
86
|
+
/**
|
|
87
|
+
* Suppression is applied to reports after a rule emits them, so a suppressed
|
|
88
|
+
* site keeps its annotation while losing its fix. Counting its removal
|
|
89
|
+
* toward orphanhood would unbind an import the surviving text still spells,
|
|
90
|
+
* trading an unused import for a dangling type.
|
|
91
|
+
*/
|
|
92
|
+
const isReportSuppressed = (0, disableDirectives_1.createSuppressionChecker)(context);
|
|
93
|
+
/**
|
|
94
|
+
* The strips that actually ship. A site is excluded when its report will be
|
|
95
|
+
* suppressed, or when its own removal orphans something the helper cannot
|
|
96
|
+
* rewrite — a local alias, an interface, a type parameter. Deleting a
|
|
97
|
+
* declaration is a materially riskier edit than dropping an import
|
|
98
|
+
* specifier, and the author is better placed to decide whether the type
|
|
99
|
+
* should go or be used elsewhere.
|
|
86
100
|
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
* the import.
|
|
101
|
+
* Screening individually before batching keeps one unfixable site from
|
|
102
|
+
* vetoing the rest: orphanhood grows monotonically with the removed set, so
|
|
103
|
+
* a site that cannot be planned alone can only ever poison the batch.
|
|
91
104
|
*/
|
|
92
|
-
function
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
context.report({
|
|
96
|
-
node: param,
|
|
97
|
-
messageId: 'redundantParamType',
|
|
98
|
-
data: {
|
|
99
|
-
paramText: sourceCode.getText(param).replace(/\s+/g, ' ').trim(),
|
|
100
|
-
},
|
|
101
|
-
// No plan means no binding can be unbound safely, so the annotation
|
|
102
|
-
// stays too: the report without a fixer is the lesser damage.
|
|
103
|
-
...(importRanges
|
|
104
|
-
? {
|
|
105
|
-
fix: (fixer) => [
|
|
106
|
-
fixer.removeRange([removal[0], removal[1]]),
|
|
107
|
-
...importRanges.map((range) => fixer.removeRange([range[0], range[1]])),
|
|
108
|
-
],
|
|
109
|
-
}
|
|
110
|
-
: {}),
|
|
111
|
-
});
|
|
105
|
+
function selectFixableSites() {
|
|
106
|
+
return sites.filter((site) => !isReportSuppressed(site.param) &&
|
|
107
|
+
(0, importRemoval_1.planOrphanedImportRemoval)(sourceCode, [site.removal]) !== null);
|
|
112
108
|
}
|
|
113
109
|
return {
|
|
110
|
+
'Program:exit'() {
|
|
111
|
+
if (sites.length === 0)
|
|
112
|
+
return;
|
|
113
|
+
const fixable = selectFixableSites();
|
|
114
|
+
const removals = fixable.map((site) => site.removal);
|
|
115
|
+
// One plan over every surviving strip: an import referenced solely by
|
|
116
|
+
// annotations that all go in this pass is orphaned by their union, even
|
|
117
|
+
// though no single one of them orphans it.
|
|
118
|
+
const importRanges = removals.length > 0
|
|
119
|
+
? (0, importRemoval_1.planOrphanedImportRemoval)(sourceCode, removals)
|
|
120
|
+
: null;
|
|
121
|
+
// The whole batch ships as one fix, so no strip can land without the
|
|
122
|
+
// others that the import's orphanhood was judged against. The rest
|
|
123
|
+
// report without a fixer; the carrier's pass already resolves them.
|
|
124
|
+
//
|
|
125
|
+
// No plan at all means no binding can be unbound safely, so every
|
|
126
|
+
// annotation stays: reports without a fixer are the lesser damage.
|
|
127
|
+
const carrier = importRanges ? fixable[0] : undefined;
|
|
128
|
+
for (const site of sites) {
|
|
129
|
+
context.report({
|
|
130
|
+
node: site.param,
|
|
131
|
+
messageId: 'redundantParamType',
|
|
132
|
+
data: { paramText: site.paramText },
|
|
133
|
+
fix: site === carrier && importRanges
|
|
134
|
+
? (fixer) => [
|
|
135
|
+
...removals.map((range) => fixer.removeRange([range[0], range[1]])),
|
|
136
|
+
...importRanges.map((range) => fixer.removeRange([range[0], range[1]])),
|
|
137
|
+
]
|
|
138
|
+
: null,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
},
|
|
114
142
|
ArrowFunctionExpression(node) {
|
|
115
143
|
if (!hasRedundantTypeAnnotation(node))
|
|
116
144
|
return;
|
|
117
145
|
node.params.forEach((param) => {
|
|
118
146
|
const typeAnnotation = annotationOf(param);
|
|
119
|
-
if (typeAnnotation)
|
|
120
|
-
|
|
121
|
-
|
|
147
|
+
if (!typeAnnotation)
|
|
148
|
+
return;
|
|
149
|
+
sites.push({
|
|
150
|
+
param,
|
|
151
|
+
removal: annotationRemovalRange(typeAnnotation, sourceCode),
|
|
152
|
+
paramText: sourceCode.getText(param).replace(/\s+/g, ' ').trim(),
|
|
153
|
+
});
|
|
122
154
|
});
|
|
123
155
|
},
|
|
124
156
|
};
|
|
@@ -439,11 +439,7 @@ function contractCoversName(typeName, memberName, index, visited) {
|
|
|
439
439
|
}
|
|
440
440
|
const alias = index.typeAliases.get(typeName);
|
|
441
441
|
if (alias) {
|
|
442
|
-
|
|
443
|
-
// a reference to an imported type) hides its member list from a syntactic
|
|
444
|
-
// reader, so it is treated as unreadable.
|
|
445
|
-
return (alias.typeAnnotation.type !== utils_1.AST_NODE_TYPES.TSTypeLiteral ||
|
|
446
|
-
membersDeclareName(alias.typeAnnotation.members, memberName));
|
|
442
|
+
return aliasCoversName(alias.typeAnnotation, memberName, index, visited);
|
|
447
443
|
}
|
|
448
444
|
const classDeclaration = index.classes.get(typeName);
|
|
449
445
|
if (classDeclaration) {
|
|
@@ -454,6 +450,57 @@ function contractCoversName(typeName, memberName, index, visited) {
|
|
|
454
450
|
// and its members are unreadable here.
|
|
455
451
|
return true;
|
|
456
452
|
}
|
|
453
|
+
/**
|
|
454
|
+
* Reports whether the type an alias names accounts for `memberName`.
|
|
455
|
+
*
|
|
456
|
+
* A type literal lists its members outright. An intersection is readable as far
|
|
457
|
+
* as its constituents are, which matters because `prefer-type-over-interface`
|
|
458
|
+
* ships in the same recommended config and rewrites
|
|
459
|
+
* `interface S extends Base { … }` to `type S = Base & { … }` by `--fix`:
|
|
460
|
+
* treating every non-literal alias as unreadable would let that sibling fix
|
|
461
|
+
* retire the member check for every contract written as an interface with a
|
|
462
|
+
* heritage clause (#1679). Anything else — a union, a mapped or conditional
|
|
463
|
+
* type, a reference whose target lives in another module — hides its member
|
|
464
|
+
* list from a syntactic reader and stays unreadable, so the member keeps its
|
|
465
|
+
* exemption.
|
|
466
|
+
*/
|
|
467
|
+
function aliasCoversName(typeNode, memberName, index, visited) {
|
|
468
|
+
switch (typeNode.type) {
|
|
469
|
+
case utils_1.AST_NODE_TYPES.TSTypeLiteral:
|
|
470
|
+
return membersDeclareName(typeNode.members, memberName);
|
|
471
|
+
case utils_1.AST_NODE_TYPES.TSIntersectionType:
|
|
472
|
+
return intersectionCoversName(typeNode, memberName, index, visited);
|
|
473
|
+
default:
|
|
474
|
+
return true;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Reports whether an intersection accounts for `memberName`. An intersection
|
|
479
|
+
* contributes every member of every constituent, so one constituent declaring
|
|
480
|
+
* the name settles the question — and a constituent whose members cannot be
|
|
481
|
+
* read settles it too, in the exempting direction: the name may well be that
|
|
482
|
+
* hidden part's, and this plugin prefers a false negative over a false positive
|
|
483
|
+
* (#1350). The member is only left to the class author when every constituent
|
|
484
|
+
* is readable and none of them declares it.
|
|
485
|
+
*/
|
|
486
|
+
function intersectionCoversName(intersection, memberName, index, visited) {
|
|
487
|
+
return intersection.types.some((constituent) => constituentCoversName(constituent, memberName, index, visited));
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Answers `intersectionCoversName` for one constituent. A reference is followed
|
|
491
|
+
* through the file's declarations by the same resolver a heritage clause uses,
|
|
492
|
+
* so a contract split across a local base and an inline literal reads as a
|
|
493
|
+
* whole; a reference the file does not declare, and a qualified or otherwise
|
|
494
|
+
* opaque constituent, count as unreadable.
|
|
495
|
+
*/
|
|
496
|
+
function constituentCoversName(typeNode, memberName, index, visited) {
|
|
497
|
+
if (typeNode.type === utils_1.AST_NODE_TYPES.TSTypeReference) {
|
|
498
|
+
return typeNode.typeName.type === utils_1.AST_NODE_TYPES.Identifier
|
|
499
|
+
? contractCoversName(typeNode.typeName.name, memberName, index, visited)
|
|
500
|
+
: true;
|
|
501
|
+
}
|
|
502
|
+
return aliasCoversName(typeNode, memberName, index, visited);
|
|
503
|
+
}
|
|
457
504
|
function heritageCoversName(heritage, memberName, index, visited) {
|
|
458
505
|
return heritage.some((clause) => {
|
|
459
506
|
const typeName = heritageTypeName(clause.expression);
|
|
@@ -2,6 +2,7 @@ type Options = [
|
|
|
2
2
|
{
|
|
3
3
|
coordinatorPatterns?: string[];
|
|
4
4
|
rateLimitedPatterns?: string[];
|
|
5
|
+
ignoreTestFiles?: boolean;
|
|
5
6
|
}
|
|
6
7
|
];
|
|
7
8
|
export declare const parallelizeLoopAwaits: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"parallelizeLoopAwaits", Options, import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
|