@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 +1 -1
- package/lib/rules/enforce-console-error.js +43 -4
- package/lib/rules/enforce-mock-firestore.js +114 -29
- package/lib/rules/enforce-querykey-ts.js +36 -7
- package/lib/rules/ensure-pointer-events-none.js +40 -29
- package/lib/rules/logical-top-to-bottom-grouping.js +119 -41
- package/lib/rules/no-complex-cloud-params.js +44 -11
- package/lib/rules/no-conditional-literals-in-jsx.js +20 -9
- package/lib/rules/no-margin-properties.js +116 -62
- package/lib/rules/prefer-global-router-state-key.js +36 -5
- package/package.json +1 -1
- package/release-manifest.json +92 -0
package/lib/index.js
CHANGED
|
@@ -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
|
-
|
|
61
|
-
|
|
62
|
-
return
|
|
99
|
+
const staticSeverity = getStaticSeverityValue(prop.value);
|
|
100
|
+
if (staticSeverity !== null) {
|
|
101
|
+
return staticSeverity;
|
|
63
102
|
}
|
|
64
|
-
//
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
|
553
|
-
const
|
|
554
|
-
|
|
555
|
-
? generateAutoFix(
|
|
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:
|
|
561
|
-
keyValue.type === utils_1.AST_NODE_TYPES.Literal
|
|
590
|
+
substitution: staticKey && suggestedConstant
|
|
562
591
|
? {
|
|
563
|
-
keyNode:
|
|
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
|
-
|
|
174
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
354
|
-
|
|
355
|
-
hasPseudoElementSelector(
|
|
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,
|
|
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
|
-
|
|
20
|
-
|
|
27
|
+
const target = unwrapAssertions(callee);
|
|
28
|
+
if (target.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
29
|
+
return isHookLikeName(target.name);
|
|
21
30
|
}
|
|
22
|
-
if (
|
|
23
|
-
!
|
|
24
|
-
|
|
25
|
-
return isHookLikeName(
|
|
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
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
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
|
-
|
|
1072
|
-
|
|
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
|
-
|
|
1122
|
-
|
|
1175
|
+
const unwrapped = unwrapAssertions(expression);
|
|
1176
|
+
if (unwrapped.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
1177
|
+
return unwrapped;
|
|
1123
1178
|
}
|
|
1124
|
-
if (
|
|
1125
|
-
|
|
1126
|
-
|
|
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
|
-
|
|
1191
|
-
|
|
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(
|
|
1195
|
-
const resolved = resolveValueForIdentifier(body,
|
|
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 (
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
(resolvedClass
|
|
1206
|
-
resolvedClass.type === utils_1.AST_NODE_TYPES.
|
|
1207
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
1361
|
-
declarator.id.name
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
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
|
|
1384
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
349
|
-
|
|
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
|
|
66
|
-
if (literalNode
|
|
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
|
|
82
|
-
|
|
83
|
-
|
|
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 (
|
|
103
|
-
propertyName =
|
|
143
|
+
if (key.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
144
|
+
propertyName = key.name;
|
|
104
145
|
}
|
|
105
|
-
else if (
|
|
106
|
-
propertyName = String(
|
|
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 =
|
|
112
|
-
const 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
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
|
170
|
-
|
|
171
|
-
|
|
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 (
|
|
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
|
|
177
|
-
|
|
178
|
-
|
|
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 (
|
|
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
|
-
|
|
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(
|
|
240
|
+
checkObjectExpression(body);
|
|
193
241
|
}
|
|
194
|
-
else if (
|
|
242
|
+
else if (body.type === utils_1.AST_NODE_TYPES.BlockStatement) {
|
|
195
243
|
// Arrow function with block body
|
|
196
|
-
const returnStatements =
|
|
244
|
+
const returnStatements = body.body.filter((stmt) => stmt.type === utils_1.AST_NODE_TYPES.ReturnStatement);
|
|
197
245
|
returnStatements.forEach((returnStmt) => {
|
|
198
|
-
if (returnStmt.argument
|
|
199
|
-
|
|
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 (
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
checkObjectExpression(
|
|
257
|
+
const consequent = unwrapAssertions(expression.consequent);
|
|
258
|
+
if (consequent.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
259
|
+
checkObjectExpression(consequent);
|
|
210
260
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
checkObjectExpression(
|
|
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
|
|
220
|
-
|
|
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
|
-
|
|
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 (
|
|
228
|
-
propertyName =
|
|
280
|
+
if (key.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
281
|
+
propertyName = key.name;
|
|
229
282
|
}
|
|
230
|
-
else if (
|
|
231
|
-
propertyName = String(
|
|
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
|
-
|
|
272
|
-
|
|
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
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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
|
|
444
|
-
fixes.push(fixer.replaceText(
|
|
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
package/release-manifest.json
CHANGED
|
@@ -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",
|