@blumintinc/eslint-plugin-blumint 1.20.122 → 1.20.124

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/lib/index.js CHANGED
@@ -223,7 +223,7 @@ function noFrontendImportsFromFunctionsPatterns(pattern) {
223
223
  module.exports = {
224
224
  meta: {
225
225
  name: '@blumintinc/eslint-plugin-blumint',
226
- version: '1.20.122',
226
+ version: '1.20.124',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -41,6 +41,45 @@ exports.enforceConsoleError = (0, createRule_1.createRule)({
41
41
  node.callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
42
42
  node.callee.property.name === method);
43
43
  }
44
+ /**
45
+ * A type assertion carries no runtime behaviour, so `'error' as const`,
46
+ * `'error' satisfies Severity`, `<const>'error'` and `'error'!` all evaluate
47
+ * to exactly the string their inner expression evaluates to. Reading through
48
+ * the wrapper keeps a severity that is pinned at compile time out of the
49
+ * `dynamic` bucket, whose report demands both console methods and asserts
50
+ * the severity may render either dialog. The wrappers nest, so unwrap until
51
+ * a non-assertion node is reached.
52
+ */
53
+ function unwrapTypeAssertions(node) {
54
+ let current = node;
55
+ while (current.type === utils_1.AST_NODE_TYPES.TSAsExpression ||
56
+ current.type === utils_1.AST_NODE_TYPES.TSSatisfiesExpression ||
57
+ current.type === utils_1.AST_NODE_TYPES.TSNonNullExpression ||
58
+ current.type === utils_1.AST_NODE_TYPES.TSTypeAssertion) {
59
+ current = current.expression;
60
+ }
61
+ return current;
62
+ }
63
+ /**
64
+ * Reads a severity that is fixed at compile time, or null when the value can
65
+ * only be known at runtime. A template with no substitutions denotes a
66
+ * single constant string exactly as a quoted literal does; only an
67
+ * INTERPOLATED template is genuinely dynamic.
68
+ */
69
+ function getStaticSeverityValue(node) {
70
+ const value = unwrapTypeAssertions(node);
71
+ if (value.type === utils_1.AST_NODE_TYPES.Literal &&
72
+ typeof value.value === 'string') {
73
+ return value.value;
74
+ }
75
+ if (value.type === utils_1.AST_NODE_TYPES.TemplateLiteral &&
76
+ value.expressions.length === 0 &&
77
+ value.quasis.length === 1 &&
78
+ typeof value.quasis[0].value.cooked === 'string') {
79
+ return value.quasis[0].value.cooked;
80
+ }
81
+ return null;
82
+ }
44
83
  function getSeverityFromObjectExpression(node) {
45
84
  for (const prop of node.properties) {
46
85
  if (prop.type === utils_1.AST_NODE_TYPES.Property) {
@@ -57,11 +96,11 @@ exports.enforceConsoleError = (0, createRule_1.createRule)({
57
96
  isSeverityProperty = true;
58
97
  }
59
98
  if (isSeverityProperty) {
60
- if (prop.value.type === utils_1.AST_NODE_TYPES.Literal &&
61
- typeof prop.value.value === 'string') {
62
- return prop.value.value;
99
+ const staticSeverity = getStaticSeverityValue(prop.value);
100
+ if (staticSeverity !== null) {
101
+ return staticSeverity;
63
102
  }
64
- // If severity is not a literal, treat as dynamic
103
+ // A severity only known at runtime forces the stricter dynamic path
65
104
  return 'dynamic';
66
105
  }
67
106
  }
@@ -8,6 +8,77 @@ const FIRESTORE_PATHS = [
8
8
  'firebase-admin',
9
9
  'firebase-admin/firestore',
10
10
  ];
11
+ /**
12
+ * `x as T`, `<T>x`, `x satisfies T` and `x!` assert a type without contributing
13
+ * a value of their own, so a check that classifies the *shape* of an expression
14
+ * must look through all four alike.
15
+ *
16
+ * This matters beyond hand-written code: sibling rules' autofixes append
17
+ * ` as const` to the very objects this rule inspects
18
+ * (`enforce-object-literal-as-const` rewrites
19
+ * `jest.mock(path, () => { return ({ db }); })` into
20
+ * `jest.mock(path, () => { return ({ db } as const); })`). A bare
21
+ * `node.type === ObjectExpression` test taken on the wrapper therefore goes
22
+ * silent on code `eslint --fix` had just reported (Issue #1806).
23
+ */
24
+ const ASSERTION_EXPRESSION_TYPES = new Set([
25
+ utils_1.AST_NODE_TYPES.TSAsExpression,
26
+ utils_1.AST_NODE_TYPES.TSSatisfiesExpression,
27
+ utils_1.AST_NODE_TYPES.TSNonNullExpression,
28
+ utils_1.AST_NODE_TYPES.TSTypeAssertion,
29
+ ]);
30
+ const isAssertionExpression = (node) => ASSERTION_EXPRESSION_TYPES.has(node.type);
31
+ /**
32
+ * Peels every assertion wrapper off an expression, so `{ db } as const`,
33
+ * `<const>{ db }` and chains such as `{ db } as const satisfies Module` all
34
+ * classify as the object literal they wrap.
35
+ */
36
+ const unwrapAssertions = (node) => {
37
+ let target = node;
38
+ while (isAssertionExpression(target)) {
39
+ target = target.expression;
40
+ }
41
+ return target;
42
+ };
43
+ /**
44
+ * A `jest.mock` factory produces the same module shape whether it is written as
45
+ * a concise arrow, a block-bodied arrow, or a `function` expression. Matching
46
+ * the factory argument itself would only ever see the concise arrow, letting an
47
+ * identical mock evade the rule on a body-form choice alone. Resolving to the
48
+ * produced object keeps every spelling on one matching path.
49
+ *
50
+ * Assertions are peeled at both ends — off the factory and off the expression
51
+ * it produces — so every body form is covered by one unwrap rather than only
52
+ * the spelling a bug report happened to quote.
53
+ *
54
+ * A body with more than a lone `return` is deliberately unresolved: the object
55
+ * reaching the caller can no longer be read off a single expression.
56
+ */
57
+ const resolveFactoryReturn = (factory) => {
58
+ if (!factory) {
59
+ return undefined;
60
+ }
61
+ const callable = unwrapAssertions(factory);
62
+ if (callable.type !== utils_1.AST_NODE_TYPES.ArrowFunctionExpression &&
63
+ callable.type !== utils_1.AST_NODE_TYPES.FunctionExpression) {
64
+ return undefined;
65
+ }
66
+ // A concise arrow body is the produced expression itself. Parentheses around
67
+ // an object body are not part of the AST, so the node is matched directly.
68
+ if (callable.body.type !== utils_1.AST_NODE_TYPES.BlockStatement) {
69
+ return unwrapAssertions(callable.body);
70
+ }
71
+ const statements = callable.body.body;
72
+ if (statements.length !== 1) {
73
+ return undefined;
74
+ }
75
+ const [statement] = statements;
76
+ if (statement.type !== utils_1.AST_NODE_TYPES.ReturnStatement ||
77
+ !statement.argument) {
78
+ return undefined;
79
+ }
80
+ return unwrapAssertions(statement.argument);
81
+ };
11
82
  exports.enforceFirestoreMock = (0, createRule_1.createRule)({
12
83
  name: 'enforce-mock-firestore',
13
84
  meta: {
@@ -27,37 +98,51 @@ exports.enforceFirestoreMock = (0, createRule_1.createRule)({
27
98
  return {
28
99
  // Detect jest.mock() calls for firebaseAdmin
29
100
  CallExpression(node) {
30
- if (node.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
31
- node.callee.object.type === utils_1.AST_NODE_TYPES.Identifier &&
32
- node.callee.object.name === 'jest' &&
33
- node.callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
34
- node.callee.property.name === 'mock' &&
35
- node.arguments.length > 0 &&
36
- node.arguments[0].type === utils_1.AST_NODE_TYPES.Literal &&
37
- typeof node.arguments[0].value === 'string' &&
38
- FIRESTORE_PATHS.some((path) => node.arguments[0].type === utils_1.AST_NODE_TYPES.Literal &&
39
- typeof node.arguments[0].value === 'string' &&
40
- node.arguments[0].value?.includes(path))) {
41
- // Check if the mock includes Firestore-related properties
42
- const mockFn = node.arguments[1];
43
- if (mockFn &&
44
- mockFn.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
45
- const returnStmt = mockFn.body;
46
- if (returnStmt.type === utils_1.AST_NODE_TYPES.ObjectExpression &&
47
- returnStmt.properties.some((prop) => prop.type === utils_1.AST_NODE_TYPES.Property &&
48
- prop.key.type === utils_1.AST_NODE_TYPES.Identifier &&
49
- (prop.key.name === 'db' ||
50
- prop.key.name === 'firestore' ||
51
- prop.key.name === 'getFirestore'))) {
52
- context.report({
53
- node,
54
- messageId: 'noManualFirestoreMock',
55
- });
56
- }
57
- }
101
+ // `(jest.mock as any)(...)` and `jest!.mock(...)` call the same
102
+ // function, so the callee and its object are read through assertions
103
+ // too. The property name cannot carry one: an assertion there requires
104
+ // computed access, a spelling this rule does not resolve at all.
105
+ const callee = unwrapAssertions(node.callee);
106
+ if (callee.type !== utils_1.AST_NODE_TYPES.MemberExpression) {
107
+ return;
108
+ }
109
+ const calleeObject = unwrapAssertions(callee.object);
110
+ if (calleeObject.type !== utils_1.AST_NODE_TYPES.Identifier ||
111
+ calleeObject.name !== 'jest' ||
112
+ callee.property.type !== utils_1.AST_NODE_TYPES.Identifier ||
113
+ callee.property.name !== 'mock' ||
114
+ node.arguments.length === 0) {
115
+ return;
116
+ }
117
+ const modulePath = unwrapAssertions(node.arguments[0]);
118
+ if (modulePath.type !== utils_1.AST_NODE_TYPES.Literal) {
119
+ return;
120
+ }
121
+ const mockedPath = modulePath.value;
122
+ if (typeof mockedPath !== 'string' ||
123
+ !FIRESTORE_PATHS.some((path) => mockedPath.includes(path))) {
124
+ return;
125
+ }
126
+ // Check if the mock includes Firestore-related properties
127
+ const mockedModule = resolveFactoryReturn(node.arguments[1]);
128
+ if (mockedModule &&
129
+ mockedModule.type === utils_1.AST_NODE_TYPES.ObjectExpression &&
130
+ mockedModule.properties.some((prop) => prop.type === utils_1.AST_NODE_TYPES.Property &&
131
+ prop.key.type === utils_1.AST_NODE_TYPES.Identifier &&
132
+ (prop.key.name === 'db' ||
133
+ prop.key.name === 'firestore' ||
134
+ prop.key.name === 'getFirestore'))) {
135
+ context.report({
136
+ node,
137
+ messageId: 'noManualFirestoreMock',
138
+ });
58
139
  }
59
140
  },
60
- // Detect imports of mockFirebase
141
+ // Detect imports of mockFirebase.
142
+ //
143
+ // No unwrapping applies here: the grammar admits only a bare string
144
+ // literal as an import source and only a bare identifier as an imported
145
+ // name, so neither position can carry an assertion for a fixer to add.
61
146
  ImportDeclaration(node) {
62
147
  if (node.source.type === utils_1.AST_NODE_TYPES.Literal &&
63
148
  node.source.value === 'firestore-jest-mock' &&
@@ -477,6 +477,36 @@ exports.enforceQueryKeyTs = (0, createRule_1.createRule)({
477
477
  }
478
478
  return false;
479
479
  }
480
+ /**
481
+ * The key's value when it is knowable without running the program, paired
482
+ * with the node that spells it.
483
+ *
484
+ * The substituted `QUERY_KEY_*` name is derived from that value, so what a
485
+ * fix needs is the value — not the notation carrying it. Gating on the node
486
+ * type instead left a static template reported exactly like the quoted
487
+ * string it renders to but with no fix behind the report (#1803). Every
488
+ * genuinely underivable shape — concatenation, a ternary, a template WITH
489
+ * expressions — holds no single value and falls out here on its own, so the
490
+ * conservative carve-out survives without being keyed to notation.
491
+ *
492
+ * Read through `cooked` rather than `raw` so an escape names the character
493
+ * it renders to, and the two spellings of one key derive one constant.
494
+ */
495
+ function staticKeyOf(node) {
496
+ if (node.type === utils_1.AST_NODE_TYPES.Literal) {
497
+ return typeof node.value === 'string'
498
+ ? { node, text: node.value }
499
+ : null;
500
+ }
501
+ if (node.type === utils_1.AST_NODE_TYPES.TemplateLiteral &&
502
+ node.expressions.length === 0) {
503
+ const cooked = node.quasis[0]?.value.cooked;
504
+ // A cooked value is absent only for an invalid escape sequence, which
505
+ // names no character and so cannot name a constant either.
506
+ return typeof cooked === 'string' ? { node, text: cooked } : null;
507
+ }
508
+ return null;
509
+ }
480
510
  /**
481
511
  * Generate auto-fix suggestion for string literals
482
512
  */
@@ -549,18 +579,17 @@ exports.enforceQueryKeyTs = (0, createRule_1.createRule)({
549
579
  if (!isValidQueryKeyUsage(keyValue)) {
550
580
  // Check if it contains invalid string literals
551
581
  if (containsInvalidStringLiteral(keyValue)) {
552
- // Only simple string literals can be auto-fixed
553
- const suggestedConstant = keyValue.type === utils_1.AST_NODE_TYPES.Literal &&
554
- typeof keyValue.value === 'string'
555
- ? generateAutoFix(keyValue.value)
582
+ // Only a statically known key value can be auto-fixed.
583
+ const staticKey = staticKeyOf(keyValue);
584
+ const suggestedConstant = staticKey
585
+ ? generateAutoFix(staticKey.text)
556
586
  : null;
557
587
  pendingReports.push({
558
588
  node: keyValue,
559
589
  messageId: 'enforceQueryKeyImport',
560
- substitution: suggestedConstant &&
561
- keyValue.type === utils_1.AST_NODE_TYPES.Literal
590
+ substitution: staticKey && suggestedConstant
562
591
  ? {
563
- keyNode: keyValue,
592
+ keyNode: staticKey.node,
564
593
  constant: suggestedConstant,
565
594
  scope: scopeOf(keyValue),
566
595
  }
@@ -9,6 +9,34 @@ const createRule_1 = require("../utils/createRule");
9
9
  function hasPseudoElementSelector(selector) {
10
10
  return /::?(before|after)\b/i.test(selector);
11
11
  }
12
+ /**
13
+ * Reads the static string a node denotes, so a property name or value carries
14
+ * the same meaning however it is spelled. A no-substitution template literal is
15
+ * a notation-only rewrite of a quoted string, and CSS-in-JS code writes both.
16
+ *
17
+ * Reading every name and value through one accessor keeps detection and the
18
+ * `pointerEvents` exemption on the same footing. Widening only the detection
19
+ * side would make the rule report objects that already set `pointerEvents` in a
20
+ * spelling it cannot read, and its fixer would append a second key — an object
21
+ * literal with duplicate keys does not compile.
22
+ *
23
+ * An interpolated template stays opaque: its text is not known statically, so
24
+ * the rule keeps its conservative silence there.
25
+ */
26
+ function staticStringOf(node) {
27
+ if (node.type === utils_1.AST_NODE_TYPES.Literal) {
28
+ return String(node.value);
29
+ }
30
+ if (node.type === utils_1.AST_NODE_TYPES.Identifier) {
31
+ return node.name;
32
+ }
33
+ if (node.type === utils_1.AST_NODE_TYPES.TemplateLiteral &&
34
+ node.expressions.length === 0 &&
35
+ node.quasis.length === 1) {
36
+ return node.quasis[0].value.cooked ?? node.quasis[0].value.raw;
37
+ }
38
+ return undefined;
39
+ }
12
40
  /**
13
41
  * Checks if a property name is position with absolute or fixed value
14
42
  */
@@ -170,35 +198,18 @@ exports.ensurePointerEventsNone = (0, createRule_1.createRule)({
170
198
  for (const property of node.properties) {
171
199
  if (property.type !== utils_1.AST_NODE_TYPES.Property)
172
200
  continue;
173
- let propertyName = '';
174
- let propertyValue;
175
- // Get property name
176
- if (property.key.type === utils_1.AST_NODE_TYPES.Identifier) {
177
- propertyName = property.key.name;
178
- }
179
- else if (property.key.type === utils_1.AST_NODE_TYPES.Literal &&
180
- typeof property.key.value === 'string') {
181
- propertyName = property.key.value;
182
- }
183
- // Get property value if it's a string literal
184
- if (property.value.type === utils_1.AST_NODE_TYPES.Literal) {
185
- propertyValue = String(property.value.value);
186
- }
187
- else if (property.value.type === utils_1.AST_NODE_TYPES.Identifier) {
188
- propertyValue = property.value.name;
189
- }
201
+ const propertyName = staticStringOf(property.key) ?? '';
202
+ const propertyValue = staticStringOf(property.value);
190
203
  // Check if this is position: absolute/fixed
191
204
  if (isAbsoluteOrFixedPosition(propertyName, propertyValue)) {
192
205
  hasAbsolutePosition = true;
193
206
  }
194
- // Check if this is pointer-events property
195
- if (isPointerEventsProperty(propertyName)) {
196
- if (property.value.type === utils_1.AST_NODE_TYPES.Literal) {
197
- pointerEventsValue = String(property.value.value);
198
- }
199
- else if (property.value.type === utils_1.AST_NODE_TYPES.Identifier) {
200
- pointerEventsValue = property.value.name;
201
- }
207
+ // Check if this is pointer-events property. A value that cannot be read
208
+ // statically never clears one already read: the rule's only remedy is to
209
+ // append a `pointerEvents` key, which would duplicate the existing one.
210
+ if (isPointerEventsProperty(propertyName) &&
211
+ propertyValue !== undefined) {
212
+ pointerEventsValue = propertyValue;
202
213
  }
203
214
  // Track inset offsets to detect hit-slop touch-target extensions
204
215
  if (INSET_PROPERTIES.has(propertyName)) {
@@ -350,12 +361,12 @@ exports.ensurePointerEventsNone = (0, createRule_1.createRule)({
350
361
  // Process CSS-in-JS libraries that use objects with selectors
351
362
  Property(node) {
352
363
  // Check for patterns like { '&::before': { ... } }
353
- if (node.key.type === utils_1.AST_NODE_TYPES.Literal &&
354
- typeof node.key.value === 'string' &&
355
- hasPseudoElementSelector(node.key.value) &&
364
+ const selector = staticStringOf(node.key);
365
+ if (selector !== undefined &&
366
+ hasPseudoElementSelector(selector) &&
356
367
  node.value.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
357
368
  processStyleObject(node.value);
358
- checkStyleObject(node.value, node.key.value);
369
+ checkStyleObject(node.value, selector);
359
370
  }
360
371
  },
361
372
  };
@@ -15,14 +15,23 @@ const TYPE_EXPRESSION_WRAPPERS = new Set([
15
15
  function isHookLikeName(name) {
16
16
  return /^use[A-Z0-9]/.test(name);
17
17
  }
18
+ /**
19
+ * The hook carve-out is a *suppression*, so failing to recognize a callee costs
20
+ * more than a missed report: `handleSideEffects` would hoist the call, and
21
+ * reordering hook calls is the one reordering React forbids outright. The
22
+ * assertion wrappers are therefore peeled off both the callee and the receiver
23
+ * it hangs from, so `(useTrack as any)()` and `(ref as Ref).useThing()` keep the
24
+ * suppression their bare spellings get.
25
+ */
18
26
  function isHookCallee(callee) {
19
- if (callee.type === utils_1.AST_NODE_TYPES.Identifier) {
20
- return isHookLikeName(callee.name);
27
+ const target = unwrapAssertions(callee);
28
+ if (target.type === utils_1.AST_NODE_TYPES.Identifier) {
29
+ return isHookLikeName(target.name);
21
30
  }
22
- if (callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
23
- !callee.computed &&
24
- callee.property.type === utils_1.AST_NODE_TYPES.Identifier) {
25
- return isHookLikeName(callee.property.name);
31
+ if (target.type === utils_1.AST_NODE_TYPES.MemberExpression &&
32
+ !target.computed &&
33
+ target.property.type === utils_1.AST_NODE_TYPES.Identifier) {
34
+ return isHookLikeName(target.property.name);
26
35
  }
27
36
  return false;
28
37
  }
@@ -35,6 +44,29 @@ function isTypeNode(node) {
35
44
  }
36
45
  return node.type.startsWith('TS');
37
46
  }
47
+ /**
48
+ * Peels every type-only wrapper off an expression: `x as T`, `<T>x`,
49
+ * `x satisfies T`, `x!` and `f<T>` all assert or instantiate a type without
50
+ * contributing a value, so a classifier asking about the *shape* of an
51
+ * expression must read straight through them.
52
+ *
53
+ * This matters beyond hand-written code. Sibling rules' autofixes put these
54
+ * wrappers on the very expressions this rule inspects — `global-const-style`
55
+ * appends ` as const` to module constants, `enforce-object-literal-as-const`
56
+ * to object literals — so a bare `init.type === Literal` test goes silent on a
57
+ * declaration `eslint --fix` had just reported (#1807). In the callee-resolving
58
+ * direction the same blindness is worse than silence: an unresolved callee
59
+ * contributes none of its captures, and the reordering fix then hoists the call
60
+ * above a binding it reads.
61
+ */
62
+ function unwrapAssertions(node) {
63
+ let target = node;
64
+ while (TYPE_EXPRESSION_WRAPPERS.has(target.type) &&
65
+ 'expression' in target) {
66
+ target = target.expression;
67
+ }
68
+ return target;
69
+ }
38
70
  function unwrapTypeExpression(expression) {
39
71
  switch (expression.type) {
40
72
  case utils_1.AST_NODE_TYPES.TSAsExpression:
@@ -1006,11 +1038,26 @@ function isSiblingSourceDerivation(statement, sourceNodes, sourceDeclarators) {
1006
1038
  return !sourceDeclarators.has(name);
1007
1039
  });
1008
1040
  }
1041
+ /**
1042
+ * A declarator's initializer with every type-only wrapper removed, or null when
1043
+ * there is none.
1044
+ *
1045
+ * The candidate test and the dependency read below must agree on which node the
1046
+ * initializer *is*: accepting `x as const` as a movable candidate while reading
1047
+ * its dependency off the wrapper would make the move miss the very name it
1048
+ * depends on. One accessor keeps both in step.
1049
+ */
1050
+ function unwrappedInitOf(declarator) {
1051
+ return declarator.init ? unwrapAssertions(declarator.init) : null;
1052
+ }
1009
1053
  /**
1010
1054
  * Restrict late-declaration candidates to simple variables with at most an Identifier or
1011
1055
  * Literal initializer. This ensures they are pure values that do not have side effects or
1012
1056
  * change execution order when moved closer to their usage. More complex initializers are
1013
1057
  * excluded to maintain temporal safety.
1058
+ *
1059
+ * The classification runs on the unwrapped initializer: an assertion is erased
1060
+ * before the code runs, so `1 as const` is exactly the movable literal `1` is.
1014
1061
  */
1015
1062
  function lateDeclarationCandidateOf(statement) {
1016
1063
  const declaration = variableDeclarationOf(statement);
@@ -1021,9 +1068,10 @@ function lateDeclarationCandidateOf(statement) {
1021
1068
  if (declarator.id.type !== utils_1.AST_NODE_TYPES.Identifier) {
1022
1069
  return null;
1023
1070
  }
1024
- if (declarator.init &&
1025
- declarator.init.type !== utils_1.AST_NODE_TYPES.Identifier &&
1026
- declarator.init.type !== utils_1.AST_NODE_TYPES.Literal) {
1071
+ const init = unwrappedInitOf(declarator);
1072
+ if (init &&
1073
+ init.type !== utils_1.AST_NODE_TYPES.Identifier &&
1074
+ init.type !== utils_1.AST_NODE_TYPES.Literal) {
1027
1075
  return null;
1028
1076
  }
1029
1077
  return declarator;
@@ -1068,8 +1116,9 @@ function handleLateDeclarations(sink, body) {
1068
1116
  }
1069
1117
  const name = declarator.id.name;
1070
1118
  const dependencies = new Set();
1071
- if (declarator.init && declarator.init.type === utils_1.AST_NODE_TYPES.Identifier) {
1072
- dependencies.add(declarator.init.name);
1119
+ const init = unwrappedInitOf(declarator);
1120
+ if (init && init.type === utils_1.AST_NODE_TYPES.Identifier) {
1121
+ dependencies.add(init.name);
1073
1122
  }
1074
1123
  const nameSet = new Set([name]);
1075
1124
  const usageIndex = findFirstUsageIndex(body, nameSet, index + 1);
@@ -1117,13 +1166,21 @@ function handleLateDeclarations(sink, body) {
1117
1166
  record(sink, statement, 'moveDeclarationCloser', { name }, index, usageIndex);
1118
1167
  });
1119
1168
  }
1169
+ /**
1170
+ * Assertions are peeled at both ends of the optional-chain wrapper, so
1171
+ * `send() as void`, `(send?.())!` and `(send?.() as void)` are all recognized as
1172
+ * the call they perform.
1173
+ */
1120
1174
  function extractCallExpression(expression) {
1121
- if (expression.type === utils_1.AST_NODE_TYPES.CallExpression) {
1122
- return expression;
1175
+ const unwrapped = unwrapAssertions(expression);
1176
+ if (unwrapped.type === utils_1.AST_NODE_TYPES.CallExpression) {
1177
+ return unwrapped;
1123
1178
  }
1124
- if (expression.type === utils_1.AST_NODE_TYPES.ChainExpression &&
1125
- expression.expression.type === utils_1.AST_NODE_TYPES.CallExpression) {
1126
- return expression.expression;
1179
+ if (unwrapped.type === utils_1.AST_NODE_TYPES.ChainExpression) {
1180
+ const chained = unwrapAssertions(unwrapped.expression);
1181
+ if (chained.type === utils_1.AST_NODE_TYPES.CallExpression) {
1182
+ return chained;
1183
+ }
1127
1184
  }
1128
1185
  return null;
1129
1186
  }
@@ -1187,40 +1244,49 @@ function resolveValueForIdentifier(body, name, beforeIndex) {
1187
1244
  return null;
1188
1245
  }
1189
1246
  function resolveValueNode(body, node, visited, beforeIndex) {
1190
- if (node.type === utils_1.AST_NODE_TYPES.Identifier) {
1191
- if (visited.has(node.name)) {
1247
+ // Every caller feeds its value through here, so unwrapping once at the entry
1248
+ // covers the object, class and function shapes `descend` matches on: an
1249
+ // `as const` on a lookup table must not turn its members opaque.
1250
+ const target = unwrapAssertions(node);
1251
+ if (target.type === utils_1.AST_NODE_TYPES.Identifier) {
1252
+ if (visited.has(target.name)) {
1192
1253
  return null;
1193
1254
  }
1194
- visited.add(node.name);
1195
- const resolved = resolveValueForIdentifier(body, node.name, beforeIndex);
1255
+ visited.add(target.name);
1256
+ const resolved = resolveValueForIdentifier(body, target.name, beforeIndex);
1196
1257
  if (!resolved) {
1197
1258
  return null;
1198
1259
  }
1199
1260
  return resolveValueNode(body, resolved, visited, beforeIndex);
1200
1261
  }
1201
- if (node.type === utils_1.AST_NODE_TYPES.NewExpression &&
1202
- node.callee.type === utils_1.AST_NODE_TYPES.Identifier) {
1203
- const resolvedClass = resolveValueForIdentifier(body, node.callee.name, beforeIndex);
1204
- if (resolvedClass &&
1205
- (resolvedClass.type === utils_1.AST_NODE_TYPES.ClassDeclaration ||
1206
- resolvedClass.type === utils_1.AST_NODE_TYPES.ClassExpression)) {
1207
- return resolvedClass;
1262
+ if (target.type === utils_1.AST_NODE_TYPES.NewExpression) {
1263
+ const constructor = unwrapAssertions(target.callee);
1264
+ if (constructor.type === utils_1.AST_NODE_TYPES.Identifier) {
1265
+ const resolvedClass = resolveValueForIdentifier(body, constructor.name, beforeIndex);
1266
+ if (resolvedClass &&
1267
+ (resolvedClass.type === utils_1.AST_NODE_TYPES.ClassDeclaration ||
1268
+ resolvedClass.type === utils_1.AST_NODE_TYPES.ClassExpression)) {
1269
+ return resolvedClass;
1270
+ }
1208
1271
  }
1209
1272
  }
1210
- return node;
1273
+ return target;
1211
1274
  }
1212
1275
  function resolveMemberFunction(body, member, beforeIndex) {
1213
1276
  if (member.computed || member.property.type !== utils_1.AST_NODE_TYPES.Identifier) {
1214
1277
  return null;
1215
1278
  }
1216
1279
  const path = [];
1217
- let cursor = member;
1280
+ // A receiver may carry an assertion at any link — `(api as Api).run` — and an
1281
+ // unresolved receiver costs the captures of the function it names, which is
1282
+ // what stops the call being hoisted above them.
1283
+ let cursor = unwrapAssertions(member);
1218
1284
  while (cursor &&
1219
1285
  cursor.type === utils_1.AST_NODE_TYPES.MemberExpression &&
1220
1286
  !cursor.computed &&
1221
1287
  cursor.property.type === utils_1.AST_NODE_TYPES.Identifier) {
1222
1288
  path.unshift(cursor.property.name);
1223
- cursor = cursor.object;
1289
+ cursor = unwrapAssertions(cursor.object);
1224
1290
  }
1225
1291
  if (!cursor || cursor.type !== utils_1.AST_NODE_TYPES.Identifier) {
1226
1292
  return null;
@@ -1276,12 +1342,12 @@ function getMemberCalleeKey(member) {
1276
1342
  return null;
1277
1343
  }
1278
1344
  const parts = [member.property.name];
1279
- let cursor = member.object;
1345
+ let cursor = unwrapAssertions(member.object);
1280
1346
  while (cursor.type === utils_1.AST_NODE_TYPES.MemberExpression &&
1281
1347
  !cursor.computed &&
1282
1348
  cursor.property.type === utils_1.AST_NODE_TYPES.Identifier) {
1283
1349
  parts.unshift(cursor.property.name);
1284
- cursor = cursor.object;
1350
+ cursor = unwrapAssertions(cursor.object);
1285
1351
  }
1286
1352
  if (cursor.type !== utils_1.AST_NODE_TYPES.Identifier) {
1287
1353
  return null;
@@ -1357,12 +1423,19 @@ function collectCalleeDependencies(body, callee, dependencies, callIndex, visite
1357
1423
  continue;
1358
1424
  }
1359
1425
  for (const declarator of statement.declarations) {
1360
- if (declarator.id.type === utils_1.AST_NODE_TYPES.Identifier &&
1361
- declarator.id.name === name &&
1362
- declarator.init &&
1363
- (declarator.init.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
1364
- declarator.init.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression)) {
1365
- return collectFunctionBodyDependencies(declarator.init, dependencies, {
1426
+ if (declarator.id.type !== utils_1.AST_NODE_TYPES.Identifier ||
1427
+ declarator.id.name !== name) {
1428
+ continue;
1429
+ }
1430
+ // Missing the function behind an assertion does not merely lose a
1431
+ // report: the scan falls through to "resolved with no dependencies",
1432
+ // and the reordering fix then hoists the call above the bindings the
1433
+ // function body reads.
1434
+ const init = unwrappedInitOf(declarator);
1435
+ if (init &&
1436
+ (init.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
1437
+ init.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression)) {
1438
+ return collectFunctionBodyDependencies(init, dependencies, {
1366
1439
  body,
1367
1440
  callIndex,
1368
1441
  visitedCallees,
@@ -1380,9 +1453,8 @@ function collectCalleeDependencies(body, callee, dependencies, callIndex, visite
1380
1453
  }
1381
1454
  visitedCallees.add(memberKey);
1382
1455
  }
1383
- const rootName = callee.object.type === utils_1.AST_NODE_TYPES.Identifier
1384
- ? callee.object.name
1385
- : null;
1456
+ const receiver = unwrapAssertions(callee.object);
1457
+ const rootName = receiver.type === utils_1.AST_NODE_TYPES.Identifier ? receiver.name : null;
1386
1458
  if (rootName && isIdentifierMutated(body, rootName, callIndex)) {
1387
1459
  return false;
1388
1460
  }
@@ -1481,6 +1553,12 @@ function applyMove(body, fromIndex, toIndex) {
1481
1553
  * an await. An await buried deeper (`const x = (await f()).y`) is deliberately not
1482
1554
  * counted — that rule does not group it either, so protecting it would cost autofixes
1483
1555
  * for no gain.
1556
+ *
1557
+ * The same reasoning keeps assertion wrappers *out* of this one test, against the
1558
+ * grain of the rest of the file: `parallelize-async-operations` matches an await
1559
+ * initializer with the identical bare type check, so `const x = (await f()) as T`
1560
+ * is outside its runs. Peeling the wrapper here would protect a run that rule
1561
+ * never forms, so the narrowness is what keeps the two in step (#1807).
1484
1562
  */
1485
1563
  function isAwaitBearingStatement(statement) {
1486
1564
  if (statement.type === utils_1.AST_NODE_TYPES.ExpressionStatement) {
@@ -4,6 +4,41 @@ exports.noComplexCloudParams = void 0;
4
4
  const utils_1 = require("@typescript-eslint/utils");
5
5
  const createRule_1 = require("../utils/createRule");
6
6
  const PASCAL_CASE_RE = /^[A-Z][a-zA-Z0-9]*$/;
7
+ /**
8
+ * Resolves a member expression's property to the name it accesses, so that a
9
+ * computed spelling such as `Object['create']` is recognized as the same
10
+ * access as `Object.create`. Returns null when the property is only known at
11
+ * runtime, in which case callers must fall back to their conservative path.
12
+ */
13
+ function staticPropertyName(property, computed) {
14
+ if (!computed && property.type === utils_1.AST_NODE_TYPES.Identifier) {
15
+ return property.name;
16
+ }
17
+ if (computed &&
18
+ property.type === utils_1.AST_NODE_TYPES.Literal &&
19
+ (typeof property.value === 'string' || typeof property.value === 'number')) {
20
+ return String(property.value);
21
+ }
22
+ return null;
23
+ }
24
+ /**
25
+ * Reads the module path of a dynamic import when the path is fixed in the
26
+ * source. A no-substitution template literal spells exactly the same path as a
27
+ * string literal, so both must be tracked; an interpolated template resolves to
28
+ * a different module per call and is deliberately left untracked to keep this
29
+ * to a question of notation.
30
+ */
31
+ function staticModulePath(source) {
32
+ if (source.type === utils_1.AST_NODE_TYPES.Literal &&
33
+ typeof source.value === 'string') {
34
+ return source.value;
35
+ }
36
+ if (source.type === utils_1.AST_NODE_TYPES.TemplateLiteral &&
37
+ source.expressions.length === 0) {
38
+ return source.quasis[0]?.value.cooked ?? null;
39
+ }
40
+ return null;
41
+ }
7
42
  exports.noComplexCloudParams = (0, createRule_1.createRule)({
8
43
  name: 'no-complex-cloud-params',
9
44
  meta: {
@@ -49,8 +84,7 @@ exports.noComplexCloudParams = (0, createRule_1.createRule)({
49
84
  if (node.value &&
50
85
  node.value.type === utils_1.AST_NODE_TYPES.CallExpression &&
51
86
  node.value.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
52
- node.value.callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
53
- node.value.callee.property.name === 'bind') {
87
+ staticPropertyName(node.value.callee.property, node.value.callee.computed) === 'bind') {
54
88
  return true;
55
89
  }
56
90
  // Check for generator methods
@@ -184,20 +218,21 @@ exports.noComplexCloudParams = (0, createRule_1.createRule)({
184
218
  }
185
219
  // Check for method calls that could create complex objects
186
220
  if (node.type === utils_1.AST_NODE_TYPES.CallExpression) {
221
+ const calleeProperty = node.callee.type === utils_1.AST_NODE_TYPES.MemberExpression
222
+ ? staticPropertyName(node.callee.property, node.callee.computed)
223
+ : null;
187
224
  // Allow JSON.stringify
188
225
  if (node.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
189
226
  node.callee.object.type === utils_1.AST_NODE_TYPES.Identifier &&
190
227
  node.callee.object.name === 'JSON' &&
191
- node.callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
192
- node.callee.property.name === 'stringify') {
228
+ calleeProperty === 'stringify') {
193
229
  return false;
194
230
  }
195
231
  // Allow Object.create(null)
196
232
  if (node.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
197
233
  node.callee.object.type === utils_1.AST_NODE_TYPES.Identifier &&
198
234
  node.callee.object.name === 'Object' &&
199
- node.callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
200
- node.callee.property.name === 'create') {
235
+ calleeProperty === 'create') {
201
236
  // Only allow Object.create(null), check if the prototype object is complex
202
237
  if (node.arguments.length === 1) {
203
238
  if (node.arguments[0].type === utils_1.AST_NODE_TYPES.Literal &&
@@ -210,8 +245,7 @@ exports.noComplexCloudParams = (0, createRule_1.createRule)({
210
245
  }
211
246
  // Check for function binding
212
247
  if (node.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
213
- node.callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
214
- node.callee.property.name === 'bind') {
248
+ calleeProperty === 'bind') {
215
249
  return true;
216
250
  }
217
251
  return isComplexValue(node.callee);
@@ -345,9 +379,8 @@ exports.noComplexCloudParams = (0, createRule_1.createRule)({
345
379
  return {
346
380
  // Track cloud function imports
347
381
  ImportExpression(node) {
348
- if (node.source.type === utils_1.AST_NODE_TYPES.Literal &&
349
- typeof node.source.value === 'string' &&
350
- node.source.value.includes('firebaseCloud')) {
382
+ const modulePath = staticModulePath(node.source);
383
+ if (modulePath !== null && modulePath.includes('firebaseCloud')) {
351
384
  // Find the variable declarator that contains this import
352
385
  let parent = node.parent;
353
386
  while (parent && parent.type !== utils_1.AST_NODE_TYPES.VariableDeclarator) {
@@ -21,6 +21,18 @@ exports.noConditionalLiteralsInJsx = (0, createRule_1.createRule)({
21
21
  },
22
22
  defaultOptions: [],
23
23
  create(context) {
24
+ /**
25
+ * A template literal without substitutions renders exactly the text its
26
+ * quoted spelling renders, so notation must never decide whether a value
27
+ * counts as JSX text. Substitution-bearing templates are excluded because
28
+ * their rendered value is not decidable syntactically. Numeric and boolean
29
+ * literals are excluded to avoid misleading messages for values that are
30
+ * not text.
31
+ */
32
+ const isTextLiteral = (astNode) => (astNode.type === utils_1.TSESTree.AST_NODE_TYPES.Literal &&
33
+ typeof astNode.value === 'string') ||
34
+ (astNode.type === utils_1.TSESTree.AST_NODE_TYPES.TemplateLiteral &&
35
+ astNode.expressions.length === 0);
24
36
  return {
25
37
  // Imagine evaluating <div>text {conditional && 'string'}</div>
26
38
  JSXExpressionContainer(node) {
@@ -62,21 +74,20 @@ exports.noConditionalLiteralsInJsx = (0, createRule_1.createRule)({
62
74
  const logicalExpression = node.expression;
63
75
  const literalNode = logicalExpression.right;
64
76
  const conditionalNode = logicalExpression.left;
65
- // Only enforce when the literal is the expression's return value.
66
- if (literalNode.type !== utils_1.TSESTree.AST_NODE_TYPES.Literal) {
67
- return;
68
- }
69
- // Only enforce for string literals to avoid misleading messages for
70
- // numeric or boolean literals rendered conditionally.
71
- if (typeof literalNode.value !== 'string') {
77
+ // Only enforce when a text literal is the expression's return value.
78
+ if (!isTextLiteral(literalNode)) {
72
79
  return;
73
80
  }
74
81
  /**
75
82
  * Ignore logical expressions that do not actually render the literal
76
83
  * conditionally (e.g., literal && condition or literal || condition)
77
- * and expressions with two literals.
84
+ * and expressions with two literals. Any literal on the left is
85
+ * unconditional, including the numeric and boolean ones that are never
86
+ * reported as a rendered value, so this exemption is wider than
87
+ * isTextLiteral on purpose.
78
88
  */
79
- if (conditionalNode.type === utils_1.TSESTree.AST_NODE_TYPES.Literal) {
89
+ if (conditionalNode.type === utils_1.TSESTree.AST_NODE_TYPES.Literal ||
90
+ isTextLiteral(conditionalNode)) {
80
91
  return;
81
92
  }
82
93
  const sourceCode = context.getSourceCode();
@@ -15,6 +15,37 @@ function normalizePropertyName(name) {
15
15
  // Convert camelCase to kebab-case
16
16
  return toKebabCase(name).toLowerCase();
17
17
  }
18
+ /**
19
+ * `x as T`, `<T>x`, `x satisfies T` and `x!` assert a type without contributing
20
+ * a value of their own, so a check that classifies the *shape* of an expression
21
+ * must look through all four alike.
22
+ *
23
+ * This matters beyond hand-written code: sibling rules' autofixes append
24
+ * ` as const` to the very object literals this rule inspects
25
+ * (`global-const-style` rewrites `const styles = { margin: 8 }` into
26
+ * `const STYLES = { margin: 8 } as const`). A bare
27
+ * `node.type === ObjectExpression` test taken on the wrapper therefore goes
28
+ * silent on code `eslint --fix` had just reported (Issue #1805).
29
+ */
30
+ const ASSERTION_EXPRESSION_TYPES = new Set([
31
+ utils_1.AST_NODE_TYPES.TSAsExpression,
32
+ utils_1.AST_NODE_TYPES.TSSatisfiesExpression,
33
+ utils_1.AST_NODE_TYPES.TSNonNullExpression,
34
+ utils_1.AST_NODE_TYPES.TSTypeAssertion,
35
+ ]);
36
+ const isAssertionExpression = (node) => ASSERTION_EXPRESSION_TYPES.has(node.type);
37
+ /**
38
+ * Peels every assertion wrapper off an expression, so `{ m: 1 } as const`,
39
+ * `<const>{ m: 1 }` and chains such as `{ m: 1 } as const satisfies Styles`
40
+ * all classify as the object literal they wrap.
41
+ */
42
+ function unwrapAssertions(node) {
43
+ let target = node;
44
+ while (isAssertionExpression(target)) {
45
+ target = target.expression;
46
+ }
47
+ return target;
48
+ }
18
49
  // List of margin properties to flag
19
50
  const MARGIN_PROPERTIES = new Set([
20
51
  'margin',
@@ -63,6 +94,11 @@ exports.noMarginProperties = (0, createRule_1.createRule)({
63
94
  */
64
95
  function isMuiStylingContext(node) {
65
96
  let current = node;
97
+ // An assertion wrapper (`{ m: 1 } as const`) is transparent to this
98
+ // climb: it matches none of the terminal predicates below, so the loop
99
+ // steps over it and keeps ascending. Any terminal branch added here must
100
+ // preserve that — concluding *at* an assertion would decide the styling
101
+ // context from the type syntax an author happened to reach for.
66
102
  while (current?.parent) {
67
103
  // Check for JSX sx attribute (MUI specific)
68
104
  if (current.parent.type === utils_1.AST_NODE_TYPES.JSXAttribute &&
@@ -77,10 +113,12 @@ exports.noMarginProperties = (0, createRule_1.createRule)({
77
113
  return true;
78
114
  }
79
115
  // Check for MUI's css function
80
- if (current.parent.type === utils_1.AST_NODE_TYPES.CallExpression &&
81
- current.parent.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
82
- current.parent.callee.name === 'css') {
83
- return true;
116
+ if (current.parent.type === utils_1.AST_NODE_TYPES.CallExpression) {
117
+ const callee = unwrapAssertions(current.parent.callee);
118
+ if (callee.type === utils_1.AST_NODE_TYPES.Identifier &&
119
+ callee.name === 'css') {
120
+ return true;
121
+ }
84
122
  }
85
123
  // Skip if we're in a TypeScript type definition
86
124
  if (current.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration ||
@@ -97,19 +135,21 @@ exports.noMarginProperties = (0, createRule_1.createRule)({
97
135
  if (seenNodes.has(node))
98
136
  return;
99
137
  seenNodes.add(node);
138
+ // A computed key carries its own assertions (`['margin' as const]`), so
139
+ // the key is classified through them as well.
140
+ const key = unwrapAssertions(node.key);
100
141
  let propertyName = '';
101
142
  // Get property name
102
- if (node.key.type === utils_1.AST_NODE_TYPES.Identifier) {
103
- propertyName = node.key.name;
143
+ if (key.type === utils_1.AST_NODE_TYPES.Identifier) {
144
+ propertyName = key.name;
104
145
  }
105
- else if (node.key.type === utils_1.AST_NODE_TYPES.Literal) {
106
- propertyName = String(node.key.value);
146
+ else if (key.type === utils_1.AST_NODE_TYPES.Literal) {
147
+ propertyName = String(key.value);
107
148
  }
108
- else if (node.computed &&
109
- node.key.type === utils_1.AST_NODE_TYPES.TemplateLiteral) {
149
+ else if (node.computed && key.type === utils_1.AST_NODE_TYPES.TemplateLiteral) {
110
150
  // Handle template literals like [`${prop}Top`]
111
- const quasis = node.key.quasis.map((q) => q.value.raw).join('');
112
- const expressions = node.key.expressions
151
+ const quasis = key.quasis.map((q) => q.value.raw).join('');
152
+ const expressions = key.expressions
113
153
  .map((exp) => {
114
154
  if (exp.type === utils_1.AST_NODE_TYPES.Identifier) {
115
155
  return exp.name;
@@ -132,23 +172,36 @@ exports.noMarginProperties = (0, createRule_1.createRule)({
132
172
  }
133
173
  }
134
174
  }
175
+ /**
176
+ * Resolves an identifier to the object literal it is initialized with,
177
+ * looking through any assertion wrappers on that initializer.
178
+ */
179
+ function resolveObjectLiteral(variableName) {
180
+ const scope = context.getScope();
181
+ const variable = scope.variables.find((v) => v.name === variableName);
182
+ if (!variable || variable.defs.length === 0)
183
+ return undefined;
184
+ const def = variable.defs[0];
185
+ if (def.node.type !== utils_1.AST_NODE_TYPES.VariableDeclarator ||
186
+ !def.node.init) {
187
+ return undefined;
188
+ }
189
+ const init = unwrapAssertions(def.node.init);
190
+ return init.type === utils_1.AST_NODE_TYPES.ObjectExpression ? init : undefined;
191
+ }
135
192
  // Check object expression for margin properties
136
193
  function checkObjectExpression(objExp) {
137
194
  objExp.properties.forEach((prop) => {
138
195
  if (prop.type === utils_1.AST_NODE_TYPES.Property) {
139
196
  checkNode(prop);
140
197
  }
141
- else if (prop.type === utils_1.AST_NODE_TYPES.SpreadElement &&
142
- prop.argument.type === utils_1.AST_NODE_TYPES.Identifier) {
198
+ else if (prop.type === utils_1.AST_NODE_TYPES.SpreadElement) {
143
199
  // Handle spread elements by looking up the variable
144
- const variableName = prop.argument.name;
145
- const scope = context.getScope();
146
- const variable = scope.variables.find((v) => v.name === variableName);
147
- if (variable && variable.defs.length > 0) {
148
- const def = variable.defs[0];
149
- if (def.node.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
150
- def.node.init?.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
151
- checkObjectExpression(def.node.init);
200
+ const spreadArgument = unwrapAssertions(prop.argument);
201
+ if (spreadArgument.type === utils_1.AST_NODE_TYPES.Identifier) {
202
+ const spreadSource = resolveObjectLiteral(spreadArgument.name);
203
+ if (spreadSource) {
204
+ checkObjectExpression(spreadSource);
152
205
  }
153
206
  }
154
207
  }
@@ -166,69 +219,69 @@ exports.noMarginProperties = (0, createRule_1.createRule)({
166
219
  if (node.name.type !== utils_1.AST_NODE_TYPES.JSXIdentifier ||
167
220
  node.name.name !== 'sx')
168
221
  return;
169
- if (node.value?.type === utils_1.AST_NODE_TYPES.JSXExpressionContainer &&
170
- node.value.expression.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
171
- checkObjectExpression(node.value.expression);
222
+ if (node.value?.type !== utils_1.AST_NODE_TYPES.JSXExpressionContainer)
223
+ return;
224
+ const expression = unwrapAssertions(node.value.expression);
225
+ if (expression.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
226
+ checkObjectExpression(expression);
172
227
  }
173
- else if (node.value?.type === utils_1.AST_NODE_TYPES.JSXExpressionContainer &&
174
- node.value.expression.type === utils_1.AST_NODE_TYPES.Identifier) {
228
+ else if (expression.type === utils_1.AST_NODE_TYPES.Identifier) {
175
229
  // Handle variable reference in sx prop
176
- const variableName = node.value.expression.name;
177
- const scope = context.getScope();
178
- const variable = scope.variables.find((v) => v.name === variableName);
179
- if (variable && variable.defs.length > 0) {
180
- const def = variable.defs[0];
181
- if (def.node.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
182
- def.node.init?.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
183
- checkObjectExpression(def.node.init);
184
- }
230
+ const referenced = resolveObjectLiteral(expression.name);
231
+ if (referenced) {
232
+ checkObjectExpression(referenced);
185
233
  }
186
234
  }
187
- else if (node.value?.type === utils_1.AST_NODE_TYPES.JSXExpressionContainer &&
188
- node.value.expression.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
235
+ else if (expression.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
189
236
  // Handle function-based sx props
190
- if (node.value.expression.body.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
237
+ const body = unwrapAssertions(expression.body);
238
+ if (body.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
191
239
  // Arrow function with object expression body
192
- checkObjectExpression(node.value.expression.body);
240
+ checkObjectExpression(body);
193
241
  }
194
- else if (node.value.expression.body.type === utils_1.AST_NODE_TYPES.BlockStatement) {
242
+ else if (body.type === utils_1.AST_NODE_TYPES.BlockStatement) {
195
243
  // Arrow function with block body
196
- const returnStatements = node.value.expression.body.body.filter((stmt) => stmt.type === utils_1.AST_NODE_TYPES.ReturnStatement);
244
+ const returnStatements = body.body.filter((stmt) => stmt.type === utils_1.AST_NODE_TYPES.ReturnStatement);
197
245
  returnStatements.forEach((returnStmt) => {
198
- if (returnStmt.argument?.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
199
- checkObjectExpression(returnStmt.argument);
246
+ if (!returnStmt.argument)
247
+ return;
248
+ const returned = unwrapAssertions(returnStmt.argument);
249
+ if (returned.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
250
+ checkObjectExpression(returned);
200
251
  }
201
252
  });
202
253
  }
203
254
  }
204
- else if (node.value?.type === utils_1.AST_NODE_TYPES.JSXExpressionContainer &&
205
- node.value.expression.type === utils_1.AST_NODE_TYPES.ConditionalExpression) {
255
+ else if (expression.type === utils_1.AST_NODE_TYPES.ConditionalExpression) {
206
256
  // Handle conditional expressions in sx props
207
- if (node.value.expression.consequent.type ===
208
- utils_1.AST_NODE_TYPES.ObjectExpression) {
209
- checkObjectExpression(node.value.expression.consequent);
257
+ const consequent = unwrapAssertions(expression.consequent);
258
+ if (consequent.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
259
+ checkObjectExpression(consequent);
210
260
  }
211
- if (node.value.expression.alternate.type ===
212
- utils_1.AST_NODE_TYPES.ObjectExpression) {
213
- checkObjectExpression(node.value.expression.alternate);
261
+ const alternate = unwrapAssertions(expression.alternate);
262
+ if (alternate.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
263
+ checkObjectExpression(alternate);
214
264
  }
215
265
  }
216
266
  },
217
267
  // Handle variable declarations that might be used in sx props
218
268
  VariableDeclarator(node) {
219
- if (node.init?.type === utils_1.AST_NODE_TYPES.ObjectExpression &&
220
- node.id.type === utils_1.AST_NODE_TYPES.Identifier) {
269
+ if (!node.init || node.id.type !== utils_1.AST_NODE_TYPES.Identifier)
270
+ return;
271
+ const init = unwrapAssertions(node.init);
272
+ if (init.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
221
273
  const variableName = node.id.name;
222
274
  const sourceText = context.sourceCode.getText();
223
275
  // Check for margin properties in the object
224
- node.init.properties.forEach((prop) => {
276
+ init.properties.forEach((prop) => {
225
277
  if (prop.type === utils_1.AST_NODE_TYPES.Property) {
278
+ const key = unwrapAssertions(prop.key);
226
279
  let propertyName = '';
227
- if (prop.key.type === utils_1.AST_NODE_TYPES.Identifier) {
228
- propertyName = prop.key.name;
280
+ if (key.type === utils_1.AST_NODE_TYPES.Identifier) {
281
+ propertyName = key.name;
229
282
  }
230
- else if (prop.key.type === utils_1.AST_NODE_TYPES.Literal) {
231
- propertyName = String(prop.key.value);
283
+ else if (key.type === utils_1.AST_NODE_TYPES.Literal) {
284
+ propertyName = String(key.value);
232
285
  }
233
286
  if (propertyName && checkProperty(propertyName)) {
234
287
  // Check if this variable is used in an sx prop
@@ -268,10 +321,11 @@ exports.noMarginProperties = (0, createRule_1.createRule)({
268
321
  },
269
322
  // Handle MUI's css function
270
323
  CallExpression(node) {
271
- if (node.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
272
- node.callee.name === 'css' &&
324
+ const callee = unwrapAssertions(node.callee);
325
+ if (callee.type === utils_1.AST_NODE_TYPES.Identifier &&
326
+ callee.name === 'css' &&
273
327
  node.arguments.length > 0) {
274
- const arg = node.arguments[0];
328
+ const arg = unwrapAssertions(node.arguments[0]);
275
329
  if (arg.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
276
330
  checkObjectExpression(arg);
277
331
  }
@@ -305,6 +305,36 @@ exports.preferGlobalRouterStateKey = (0, createRule_1.createRule)({
305
305
  }
306
306
  return false;
307
307
  }
308
+ /**
309
+ * The key's value when it is knowable without running the program, paired
310
+ * with the node that spells it.
311
+ *
312
+ * The substituted `QUERY_KEY_*` name is derived from that value, so what a
313
+ * fix needs is the value — not the notation carrying it. Gating on the node
314
+ * type instead left a static template reported exactly like the quoted
315
+ * string it renders to but with no fix behind the report (#1804). Every
316
+ * genuinely underivable shape — concatenation, a ternary, a template WITH
317
+ * expressions — holds no single value and falls out here on its own, so the
318
+ * conservative carve-out survives without being keyed to notation.
319
+ *
320
+ * Read through `cooked` rather than `raw` so an escape names the character
321
+ * it renders to, and the two spellings of one key derive one constant.
322
+ */
323
+ function staticKeyOf(node) {
324
+ if (node.type === utils_1.AST_NODE_TYPES.Literal) {
325
+ return typeof node.value === 'string'
326
+ ? { node, text: node.value }
327
+ : null;
328
+ }
329
+ if (node.type === utils_1.AST_NODE_TYPES.TemplateLiteral &&
330
+ node.expressions.length === 0) {
331
+ const cooked = node.quasis[0]?.value.cooked;
332
+ // A cooked value is absent only for an invalid escape sequence, which
333
+ // names no character and so cannot name a constant either.
334
+ return typeof cooked === 'string' ? { node, text: cooked } : null;
335
+ }
336
+ return null;
337
+ }
308
338
  /**
309
339
  * Generate auto-fix suggestion for string literals
310
340
  */
@@ -384,9 +414,10 @@ exports.preferGlobalRouterStateKey = (0, createRule_1.createRule)({
384
414
  keyValue: sourceCode.getText(keyValue),
385
415
  },
386
416
  fix(fixer) {
387
- if (keyValue.type === utils_1.AST_NODE_TYPES.Literal &&
388
- typeof keyValue.value === 'string') {
389
- const suggestedConstant = generateAutoFix(keyValue.value);
417
+ // Only a statically known key value can be auto-fixed.
418
+ const staticKey = staticKeyOf(keyValue);
419
+ if (staticKey) {
420
+ const suggestedConstant = generateAutoFix(staticKey.text);
390
421
  if (suggestedConstant) {
391
422
  const fixes = [];
392
423
  const namespaceAlias = findImportKey(namespaceImports, isQueryKeysSource);
@@ -440,8 +471,8 @@ exports.preferGlobalRouterStateKey = (0, createRule_1.createRule)({
440
471
  if (visibleBinding && !bindingIsQueryKeyImport) {
441
472
  return null;
442
473
  }
443
- // 1) Replace the literal with the constant (qualify if alias exists)
444
- fixes.push(fixer.replaceText(keyValue, replacementText));
474
+ // 1) Replace the key with the constant (qualify if alias exists)
475
+ fixes.push(fixer.replaceText(staticKey.node, replacementText));
445
476
  // 2) Ensure an import exists for the suggested constant
446
477
  const hasNamespaceOrDefault = Boolean(importAlias);
447
478
  if (!existingNamedImport &&
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.20.122",
3
+ "version": "1.20.124",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -1,4 +1,96 @@
1
1
  [
2
+ {
3
+ "version": "1.20.124",
4
+ "date": "2026-08-06T15:15:14.462Z",
5
+ "rules": [
6
+ {
7
+ "name": "enforce-console-error",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 1801
11
+ ],
12
+ "summary": "treat a no-substitution template and an assertion-wrapped literal as a static severity (closes #1801)"
13
+ },
14
+ {
15
+ "name": "enforce-mock-firestore",
16
+ "changeType": "fix",
17
+ "issues": [
18
+ 1806
19
+ ],
20
+ "summary": "treat expression assertions as transparent when resolving the jest.mock factory's object (closes #1806)"
21
+ },
22
+ {
23
+ "name": "enforce-querykey-ts",
24
+ "changeType": "fix",
25
+ "issues": [
26
+ 1803
27
+ ],
28
+ "summary": "gate the autofix on the key's value rather than its notation (closes #1803)"
29
+ },
30
+ {
31
+ "name": "ensure-pointer-events-none",
32
+ "changeType": "fix",
33
+ "issues": [
34
+ 1800
35
+ ],
36
+ "summary": "read a no-substitution template as the static string it denotes (closes #1800)"
37
+ },
38
+ {
39
+ "name": "logical-top-to-bottom-grouping",
40
+ "changeType": "fix",
41
+ "issues": [
42
+ 1807
43
+ ],
44
+ "summary": "treat expression assertions as transparent in every movability and dependency read (closes #1807)"
45
+ },
46
+ {
47
+ "name": "no-complex-cloud-params",
48
+ "changeType": "fix",
49
+ "issues": [
50
+ 1799
51
+ ],
52
+ "summary": "track template-literal cloud imports and computed escape-hatch spellings (closes #1799)"
53
+ },
54
+ {
55
+ "name": "no-conditional-literals-in-jsx",
56
+ "changeType": "fix",
57
+ "issues": [
58
+ 1802
59
+ ],
60
+ "summary": "recognise a no-substitution template on both operands (closes #1802)"
61
+ },
62
+ {
63
+ "name": "no-margin-properties",
64
+ "changeType": "fix",
65
+ "issues": [
66
+ 1805
67
+ ],
68
+ "summary": "treat expression assertions as transparent when classifying a style object (closes #1805)"
69
+ },
70
+ {
71
+ "name": "prefer-global-router-state-key",
72
+ "changeType": "fix",
73
+ "issues": [
74
+ 1804
75
+ ],
76
+ "summary": "gate the autofix on the key's value rather than its notation (closes #1804)"
77
+ }
78
+ ]
79
+ },
80
+ {
81
+ "version": "1.20.123",
82
+ "date": "2026-08-06T11:53:46.588Z",
83
+ "rules": [
84
+ {
85
+ "name": "enforce-mock-firestore",
86
+ "changeType": "fix",
87
+ "issues": [
88
+ 1798
89
+ ],
90
+ "summary": "resolve the jest.mock factory's returned object in every body form (closes #1798)"
91
+ }
92
+ ]
93
+ },
2
94
  {
3
95
  "version": "1.20.122",
4
96
  "date": "2026-08-06T11:06:53.595Z",