@blumintinc/eslint-plugin-blumint 1.20.95 → 1.20.97
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-boolean-naming-prefixes.js +38 -3
- 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/enforce-positive-naming.js +112 -24
- 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-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/lib/utils/harvestRuleTesterCases.d.ts +48 -0
- package/lib/utils/harvestRuleTesterCases.js +168 -0
- package/package.json +1 -1
- package/release-manifest.json +151 -0
|
@@ -23,6 +23,28 @@ const toUpperSnakeCase = (name) => name
|
|
|
23
23
|
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
|
|
24
24
|
.toUpperCase()
|
|
25
25
|
.replace(/^_/, '');
|
|
26
|
+
// `x as T`, `<T>x`, `x satisfies T` and `x!` annotate or assert an expression
|
|
27
|
+
// without contributing a value of their own, so a check that classifies the
|
|
28
|
+
// *shape* of an initializer must look through all four alike. Recognizing only
|
|
29
|
+
// some of them makes the rule's carve-outs depend on which type syntax an
|
|
30
|
+
// author happened to reach for: a React component written
|
|
31
|
+
// `memo(Foo) satisfies ComponentType` or `memo(Foo)!` read as opaque
|
|
32
|
+
// expressions and were renamed to UPPER_SNAKE_CASE while `memo(Foo) as FC` was
|
|
33
|
+
// exempt (Issue #1681).
|
|
34
|
+
const VALUE_WRAPPER_TYPES = new Set([
|
|
35
|
+
utils_1.AST_NODE_TYPES.TSAsExpression,
|
|
36
|
+
utils_1.AST_NODE_TYPES.TSTypeAssertion,
|
|
37
|
+
utils_1.AST_NODE_TYPES.TSSatisfiesExpression,
|
|
38
|
+
utils_1.AST_NODE_TYPES.TSNonNullExpression,
|
|
39
|
+
]);
|
|
40
|
+
const isValueWrapper = (node) => VALUE_WRAPPER_TYPES.has(node.type);
|
|
41
|
+
const unwrapValueWrappers = (node) => {
|
|
42
|
+
let target = node;
|
|
43
|
+
while (isValueWrapper(target)) {
|
|
44
|
+
target = target.expression;
|
|
45
|
+
}
|
|
46
|
+
return target;
|
|
47
|
+
};
|
|
26
48
|
// Jest mock handles produced by an `as` cast to a `jest.Mock*` type are
|
|
27
49
|
// stateful test doubles that are reassigned/mutated through
|
|
28
50
|
// `.mockImplementation()`, `.mockReturnValue()`, etc. They are not immutable
|
|
@@ -38,11 +60,7 @@ const JEST_MOCK_TYPE_NAMES = new Set([
|
|
|
38
60
|
// `jest.Mocked<...>` / `jest.MockedClass<...>`. The match is kept deliberately
|
|
39
61
|
// narrow — a qualified `jest.<MockType>` type reference — so unrelated `as`
|
|
40
62
|
// casts keep triggering the rename check.
|
|
41
|
-
const
|
|
42
|
-
if (node.type !== utils_1.AST_NODE_TYPES.TSAsExpression) {
|
|
43
|
-
return false;
|
|
44
|
-
}
|
|
45
|
-
const typeAnnotation = node.typeAnnotation;
|
|
63
|
+
const isJestMockTypeReference = (typeAnnotation) => {
|
|
46
64
|
if (typeAnnotation.type !== utils_1.AST_NODE_TYPES.TSTypeReference) {
|
|
47
65
|
return false;
|
|
48
66
|
}
|
|
@@ -53,6 +71,43 @@ const isJestMockCast = (node) => {
|
|
|
53
71
|
typeName.right.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
54
72
|
JEST_MOCK_TYPE_NAMES.has(typeName.right.name));
|
|
55
73
|
};
|
|
74
|
+
// The cast can sit anywhere in a wrapper chain (`(foo as jest.Mock)!`,
|
|
75
|
+
// `foo as jest.Mock satisfies unknown`), so the whole chain is scanned rather
|
|
76
|
+
// than the outermost node alone — a mock handle stays a mock handle whatever is
|
|
77
|
+
// wrapped around the cast.
|
|
78
|
+
const isJestMockCast = (node) => {
|
|
79
|
+
let current = node;
|
|
80
|
+
while (isValueWrapper(current)) {
|
|
81
|
+
if (current.type === utils_1.AST_NODE_TYPES.TSAsExpression &&
|
|
82
|
+
isJestMockTypeReference(current.typeAnnotation)) {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
current = current.expression;
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
88
|
+
};
|
|
89
|
+
// React's component factories, called bare (`memo(Foo)`) or through a namespace
|
|
90
|
+
// import (`React.memo(Foo)`).
|
|
91
|
+
const COMPONENT_FACTORY_NAMES = new Set(['forwardRef', 'memo']);
|
|
92
|
+
const isComponentFactoryCall = (node) => {
|
|
93
|
+
if (node.type !== utils_1.AST_NODE_TYPES.CallExpression) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
const { callee } = node;
|
|
97
|
+
if (callee.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
98
|
+
return COMPONENT_FACTORY_NAMES.has(callee.name);
|
|
99
|
+
}
|
|
100
|
+
return (callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
101
|
+
!callee.computed &&
|
|
102
|
+
callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
103
|
+
COMPONENT_FACTORY_NAMES.has(callee.property.name));
|
|
104
|
+
};
|
|
105
|
+
// A function value is a component, hook or helper, never the module-level
|
|
106
|
+
// configuration value this rule governs. The two spellings are interchangeable
|
|
107
|
+
// at a declaration site, so `const Row = function (props) {...}` is exempt on
|
|
108
|
+
// the same terms as `const Row = (props) => {...}` (Issue #1681).
|
|
109
|
+
const isFunctionValue = (node) => node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
110
|
+
node.type === utils_1.AST_NODE_TYPES.FunctionExpression;
|
|
56
111
|
/**
|
|
57
112
|
* Walks the scope chain upward from `scope` (inclusive) and reports whether
|
|
58
113
|
* `targetName` is bound anywhere between `scope` and `stopScope` (inclusive).
|
|
@@ -141,7 +196,16 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
141
196
|
// Check if the file is a TypeScript file
|
|
142
197
|
const isTypeScript = context.getFilename().endsWith('.ts') ||
|
|
143
198
|
context.getFilename().endsWith('.tsx');
|
|
144
|
-
|
|
199
|
+
/**
|
|
200
|
+
* Strips `as`/`<T>` casts only, and is deliberately narrower than
|
|
201
|
+
* `unwrapValueWrappers`. The two carve-outs below — dynamic values and
|
|
202
|
+
* binding aliases — silence the rule entirely, so widening them to see
|
|
203
|
+
* through `!`/`satisfies` would newly exempt `const value = getValue()!`
|
|
204
|
+
* and `const alias = other!` from the rename check. That is a detection
|
|
205
|
+
* change of its own, distinct from the wrapper-blind component exemption
|
|
206
|
+
* `unwrapValueWrappers` cures (Issue #1681).
|
|
207
|
+
*/
|
|
208
|
+
const unwrapCasts = (node) => {
|
|
145
209
|
let target = node;
|
|
146
210
|
while (target.type === utils_1.AST_NODE_TYPES.TSTypeAssertion ||
|
|
147
211
|
target.type === utils_1.AST_NODE_TYPES.TSAsExpression) {
|
|
@@ -150,7 +214,7 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
150
214
|
return target;
|
|
151
215
|
};
|
|
152
216
|
const isDynamicValue = (node) => {
|
|
153
|
-
const target =
|
|
217
|
+
const target = unwrapCasts(node);
|
|
154
218
|
if (target.type === utils_1.AST_NODE_TYPES.CallExpression ||
|
|
155
219
|
target.type === utils_1.AST_NODE_TYPES.NewExpression ||
|
|
156
220
|
target.type === utils_1.AST_NODE_TYPES.BinaryExpression) {
|
|
@@ -174,18 +238,18 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
174
238
|
* such a re-export is preserving a name importers depend on and a
|
|
175
239
|
* single-file fixer cannot rewrite them (Issue #1418).
|
|
176
240
|
*
|
|
177
|
-
* The check unwraps
|
|
241
|
+
* The check unwraps casts so a type-pinned alias (`x as Foo`,
|
|
178
242
|
* `x as const`) is treated the same as the bare form. A `MemberExpression`
|
|
179
243
|
* (`Foo.bar`) is deliberately not covered — it keeps whatever behavior
|
|
180
244
|
* `isDynamicValue` already gives it.
|
|
181
245
|
*/
|
|
182
246
|
const isBindingAlias = (node) => {
|
|
183
|
-
const target =
|
|
247
|
+
const target = unwrapCasts(node);
|
|
184
248
|
return (target.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
185
249
|
!PRIMITIVE_VALUE_GLOBALS.has(target.name));
|
|
186
250
|
};
|
|
187
251
|
const describeValueKind = (node) => {
|
|
188
|
-
const target =
|
|
252
|
+
const target = unwrapValueWrappers(node);
|
|
189
253
|
if (target.type === utils_1.AST_NODE_TYPES.ArrayExpression) {
|
|
190
254
|
return 'an array literal';
|
|
191
255
|
}
|
|
@@ -208,41 +272,22 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
208
272
|
node.parent?.type !== utils_1.AST_NODE_TYPES.ExportNamedDeclaration) {
|
|
209
273
|
return;
|
|
210
274
|
}
|
|
211
|
-
// Skip if any declaration is a function component,
|
|
275
|
+
// Skip if any declaration is a function value (component, hook or
|
|
276
|
+
// helper) or a `memo`/`forwardRef` component factory call. The
|
|
277
|
+
// initializer is classified through any type wrappers, so the pinned
|
|
278
|
+
// forms (`… as FC`, `… satisfies ComponentType`, `…!`) are exempt on
|
|
279
|
+
// the same terms as the bare expression they wrap.
|
|
212
280
|
const shouldSkip = node.declarations.some((declaration) => {
|
|
213
281
|
if (declaration.id.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
214
282
|
return false;
|
|
215
283
|
}
|
|
216
|
-
const name = declaration.id.name;
|
|
217
284
|
const init = declaration.init;
|
|
218
285
|
// Skip if no initializer
|
|
219
286
|
if (!init) {
|
|
220
287
|
return false;
|
|
221
288
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
init.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
225
|
-
return true;
|
|
226
|
-
}
|
|
227
|
-
// Skip any arrow function
|
|
228
|
-
if (init.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
229
|
-
return true;
|
|
230
|
-
}
|
|
231
|
-
// Skip forwardRef and memo calls
|
|
232
|
-
if (init.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
233
|
-
if (init.callee.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
234
|
-
return ['forwardRef', 'memo'].includes(init.callee.name);
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
// Skip type assertions on forwardRef and memo calls
|
|
238
|
-
if (init.type === utils_1.AST_NODE_TYPES.TSAsExpression) {
|
|
239
|
-
const expression = init.expression;
|
|
240
|
-
if (expression.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
241
|
-
expression.callee.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
242
|
-
return ['forwardRef', 'memo'].includes(expression.callee.name);
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
return false;
|
|
289
|
+
const target = unwrapValueWrappers(init);
|
|
290
|
+
return isFunctionValue(target) || isComponentFactoryCall(target);
|
|
246
291
|
});
|
|
247
292
|
if (shouldSkip) {
|
|
248
293
|
return;
|
|
@@ -267,15 +312,19 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
267
312
|
: '';
|
|
268
313
|
// Only check for as const in TypeScript files
|
|
269
314
|
if (isTypeScript) {
|
|
315
|
+
// An `as const` anywhere in the wrapper chain already freezes the
|
|
316
|
+
// value, including when a later wrapper hides it
|
|
317
|
+
// (`{...} as const satisfies Config`, `({...} as const)!`).
|
|
270
318
|
const hasAsConstAssertion = (node) => {
|
|
271
319
|
let current = node;
|
|
272
|
-
while (current
|
|
273
|
-
(current.type === utils_1.AST_NODE_TYPES.TSAsExpression ||
|
|
274
|
-
current.type === utils_1.AST_NODE_TYPES.TSTypeAssertion)
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
typeAnnotation.typeName.type ===
|
|
278
|
-
|
|
320
|
+
while (isValueWrapper(current)) {
|
|
321
|
+
if ((current.type === utils_1.AST_NODE_TYPES.TSAsExpression ||
|
|
322
|
+
current.type === utils_1.AST_NODE_TYPES.TSTypeAssertion) &&
|
|
323
|
+
current.typeAnnotation.type ===
|
|
324
|
+
utils_1.AST_NODE_TYPES.TSTypeReference &&
|
|
325
|
+
current.typeAnnotation.typeName.type ===
|
|
326
|
+
utils_1.AST_NODE_TYPES.Identifier &&
|
|
327
|
+
current.typeAnnotation.typeName.name === 'const') {
|
|
279
328
|
return true;
|
|
280
329
|
}
|
|
281
330
|
current = current.expression;
|
|
@@ -287,14 +336,15 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
287
336
|
if (hasAsConstAssertion(node)) {
|
|
288
337
|
return false;
|
|
289
338
|
}
|
|
290
|
-
const target =
|
|
291
|
-
// Skip an initializer already wrapped in a non-`const`
|
|
292
|
-
// (`{...} as T`, `<T>{...}`, `{...} as unknown as T
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
// regex/null/boolean carve-outs
|
|
296
|
-
//
|
|
297
|
-
// `id.typeAnnotation` case
|
|
339
|
+
const target = unwrapValueWrappers(node);
|
|
340
|
+
// Skip an initializer already wrapped in a non-`const` type
|
|
341
|
+
// wrapper (`{...} as T`, `<T>{...}`, `{...} as unknown as T`,
|
|
342
|
+
// `{...} satisfies T`, `{...}!`). A `const` assertion may only be
|
|
343
|
+
// applied to a literal, so appending one after such a chain is
|
|
344
|
+
// TS1355 — the same failure mode the regex/null/boolean carve-outs
|
|
345
|
+
// below exist for. Such a wrapper is also the author pinning the
|
|
346
|
+
// type deliberately, exactly like the `id.typeAnnotation` case
|
|
347
|
+
// skipped next.
|
|
298
348
|
if (target !== node) {
|
|
299
349
|
return false;
|
|
300
350
|
}
|
|
@@ -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 &&
|