@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.
@@ -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 isJestMockCast = (node) => {
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
- const unwrapAssertions = (node) => {
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 = unwrapAssertions(node);
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 assertions so a type-pinned alias (`x as Foo`,
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 = unwrapAssertions(node);
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 = unwrapAssertions(node);
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, arrow function, forwardRef, or memo
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
- // Skip function components (uppercase name + arrow function)
223
- if (/^[A-Z]/.test(name) &&
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
- const { typeAnnotation } = current;
276
- if (typeAnnotation?.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
277
- typeAnnotation.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
278
- typeAnnotation.typeName.name === 'const') {
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 = unwrapAssertions(node);
291
- // Skip an initializer already wrapped in a non-`const` assertion
292
- // (`{...} as T`, `<T>{...}`, `{...} as unknown as T`). A `const`
293
- // assertion may only be applied to a literal, so appending one
294
- // after an assertion chain is TS1355 — the same failure mode the
295
- // regex/null/boolean carve-outs below exist for. Such a cast is
296
- // also the author pinning the type deliberately, exactly like the
297
- // `id.typeAnnotation` case skipped next.
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?.type === utils_1.AST_NODE_TYPES.Literal) {
75
- return initializer.value;
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
- if (node.type === utils_1.AST_NODE_TYPES.Literal &&
134
- typeof node.value === 'number') {
135
- return node.value;
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(node);
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 (node.left.type === utils_1.AST_NODE_TYPES.Literal &&
178
- node.right.type === utils_1.AST_NODE_TYPES.Literal &&
179
- typeof node.left.value === 'number' &&
180
- typeof node.right.value === 'number') {
181
- const result = node.left.value & node.right.value;
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 (node.left.type !== utils_1.AST_NODE_TYPES.Literal ||
194
- node.right.type !== utils_1.AST_NODE_TYPES.Literal) {
195
- const resolvedLeft = resolveNumericOperand(node.left);
196
- const resolvedRight = resolveNumericOperand(node.right);
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 = node.left.value;
203
- const rightValue = node.right.value;
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
- // Handle optional chaining with literal objects
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
- if (node.expression.optional) {
1063
- // Handle object?.prop
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 &&