@blumintinc/eslint-plugin-blumint 1.21.4 → 1.21.6
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-microdiff.d.ts +1 -1
- package/lib/rules/enforce-microdiff.js +65 -1
- package/lib/rules/flatten-push-calls.js +272 -1
- package/lib/rules/global-const-style.js +89 -10
- package/lib/rules/no-try-catch-already-exists-in-transaction.d.ts +2 -1
- package/lib/rules/no-try-catch-already-exists-in-transaction.js +115 -2
- package/lib/rules/prefer-sx-prop-over-system-props.js +109 -9
- package/package.json +1 -1
- package/release-manifest.json +52 -0
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { TSESLint } from '@typescript-eslint/utils';
|
|
2
|
-
type MessageIds = 'enforceMicrodiff' | 'enforceMicrodiffImport';
|
|
2
|
+
type MessageIds = 'enforceMicrodiff' | 'enforceMicrodiffManual' | 'enforceMicrodiffImport';
|
|
3
3
|
export declare const enforceMicrodiff: TSESLint.RuleModule<MessageIds, [], TSESLint.RuleListener>;
|
|
4
4
|
export {};
|
|
@@ -112,6 +112,30 @@ const COMPETING_DIFF_MODULES = new Set([
|
|
|
112
112
|
// 'fast-deep-equal' and 'fast-deep-equal/es6' stay out of this set: they are
|
|
113
113
|
// allowed alternatives to microdiff, so their imports survive the fix.
|
|
114
114
|
]);
|
|
115
|
+
/**
|
|
116
|
+
* The competing libraries whose calls are reported but never rewritten, because
|
|
117
|
+
* microdiff answers a different question than either of them does.
|
|
118
|
+
*
|
|
119
|
+
* jsdiff (`diff`) is a Myers sequence diff. `diffArrays(a, b)` returns runs of
|
|
120
|
+
* `{value, added, removed, count}`, so equal inputs yield one *kept* run where
|
|
121
|
+
* microdiff returns the empty list — `changes.length === 0` flips meaning under
|
|
122
|
+
* the rename — and a consumer reading `.added`, `.removed`, `.value` or
|
|
123
|
+
* `.count` gets `.type`, `.path` and `.oldValue` instead. Neither loss is a
|
|
124
|
+
* compile error, so nothing downstream flags it. Its optional third argument is
|
|
125
|
+
* a comparator bag with no counterpart in `Partial<MicrodiffOptions>`, which is
|
|
126
|
+
* a compile error (TS2345).
|
|
127
|
+
*
|
|
128
|
+
* `fast-diff` diffs STRINGS. Its operands satisfy neither half of microdiff's
|
|
129
|
+
* `TData extends Record<string, unknown> | unknown[]` bound, so that arm cannot
|
|
130
|
+
* emit a rewrite that compiles at all (TS2345).
|
|
131
|
+
*
|
|
132
|
+
* Detection is unchanged — both keep their entries in `DIFF_FUNCTION_NAMES` and
|
|
133
|
+
* `COMPETING_DIFF_MODULES` — because reaching for either where a structural
|
|
134
|
+
* per-path diff is wanted is still the finding. Only the rewrite is withheld,
|
|
135
|
+
* and the import retirement with it: dropping the declaration while the calls
|
|
136
|
+
* it binds stay behind is what would leave the file with an unbound name.
|
|
137
|
+
*/
|
|
138
|
+
const UNCONVERTIBLE_DIFF_MODULES = new Set(['diff', 'fast-diff']);
|
|
115
139
|
/**
|
|
116
140
|
* A specifier that makes a bare `diff` resolve to microdiff's diff function:
|
|
117
141
|
* its default export, or its named `diff` export, bound under the name the fix
|
|
@@ -162,7 +186,12 @@ function collectClaimableSpecifiers(program) {
|
|
|
162
186
|
.forEach((specifier) => claimable.add(specifier));
|
|
163
187
|
return;
|
|
164
188
|
}
|
|
165
|
-
|
|
189
|
+
// A declaration no fix retires keeps every name it binds, so a `diff` it
|
|
190
|
+
// declares is not the fix's to write over: microdiff's import emitted
|
|
191
|
+
// beside a surviving `import { diff } from 'diff'` duplicates the binding
|
|
192
|
+
// (TS2300).
|
|
193
|
+
if (COMPETING_DIFF_MODULES.has(source) &&
|
|
194
|
+
!UNCONVERTIBLE_DIFF_MODULES.has(source)) {
|
|
166
195
|
statement.specifiers.forEach((specifier) => claimable.add(specifier));
|
|
167
196
|
}
|
|
168
197
|
});
|
|
@@ -480,6 +509,7 @@ exports.enforceMicrodiff = (0, createRule_1.createRule)({
|
|
|
480
509
|
schema: [],
|
|
481
510
|
messages: {
|
|
482
511
|
enforceMicrodiff: 'Use the microdiff library for object and array comparison operations',
|
|
512
|
+
enforceMicrodiffManual: 'Use the microdiff library for object and array comparison operations. Convert this {{importSource}} call by hand: it returns a different result shape than the structural change list microdiff produces.',
|
|
483
513
|
enforceMicrodiffImport: 'Import diff from microdiff instead of {{importSource}}',
|
|
484
514
|
},
|
|
485
515
|
},
|
|
@@ -777,6 +807,12 @@ exports.enforceMicrodiff = (0, createRule_1.createRule)({
|
|
|
777
807
|
importSource,
|
|
778
808
|
},
|
|
779
809
|
fix(fixer) {
|
|
810
|
+
// Every call this declaration serves is left for manual
|
|
811
|
+
// conversion, so retiring it would strand each of them on a name
|
|
812
|
+
// nothing binds.
|
|
813
|
+
if (UNCONVERTIBLE_DIFF_MODULES.has(importSource)) {
|
|
814
|
+
return null;
|
|
815
|
+
}
|
|
780
816
|
// Decline rather than duplicate or shadow a `diff` this file
|
|
781
817
|
// already binds to something else, and rather than retire an
|
|
782
818
|
// import whose references no call fix rewrites. The report stands
|
|
@@ -853,6 +889,19 @@ exports.enforceMicrodiff = (0, createRule_1.createRule)({
|
|
|
853
889
|
if (ALLOWED_DIFF_MODULES.has(importSource)) {
|
|
854
890
|
return;
|
|
855
891
|
}
|
|
892
|
+
// A library microdiff does not stand in for is reported without
|
|
893
|
+
// the rename. The substitution would compile at some of these
|
|
894
|
+
// call sites and answer a different question there, which is a
|
|
895
|
+
// loss nothing downstream can flag.
|
|
896
|
+
if (UNCONVERTIBLE_DIFF_MODULES.has(importSource)) {
|
|
897
|
+
reportedNodes.add(node);
|
|
898
|
+
context.report({
|
|
899
|
+
node,
|
|
900
|
+
messageId: 'enforceMicrodiffManual',
|
|
901
|
+
data: { importSource },
|
|
902
|
+
});
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
856
905
|
// Report it if it's from any other tracked library
|
|
857
906
|
reportedNodes.add(node);
|
|
858
907
|
context.report({
|
|
@@ -874,6 +923,21 @@ exports.enforceMicrodiff = (0, createRule_1.createRule)({
|
|
|
874
923
|
if (!competingImport) {
|
|
875
924
|
return;
|
|
876
925
|
}
|
|
926
|
+
// The same withholding as above, reached by the other resolution
|
|
927
|
+
// path: a default import and an unrecognised export name are both
|
|
928
|
+
// bound here rather than tracked as specifiers, so gating only the
|
|
929
|
+
// tracked branch would leave `import fastDiff from 'fast-diff'`
|
|
930
|
+
// rewritten.
|
|
931
|
+
const competingSource = String(competingImport.source.value);
|
|
932
|
+
if (UNCONVERTIBLE_DIFF_MODULES.has(competingSource)) {
|
|
933
|
+
reportedNodes.add(node);
|
|
934
|
+
context.report({
|
|
935
|
+
node,
|
|
936
|
+
messageId: 'enforceMicrodiffManual',
|
|
937
|
+
data: { importSource: competingSource },
|
|
938
|
+
});
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
877
941
|
// Track this import name as used
|
|
878
942
|
usedImportNames.add(name);
|
|
879
943
|
// What the callee resolves to already settles this: the argument
|
|
@@ -223,6 +223,206 @@ function canSafelyFix(group) {
|
|
|
223
223
|
});
|
|
224
224
|
});
|
|
225
225
|
}
|
|
226
|
+
/**
|
|
227
|
+
* `Array.prototype.push` is variadic, so merging consecutive appends onto one
|
|
228
|
+
* array preserves meaning. A method named `push` on any other receiver need not
|
|
229
|
+
* be: Next.js's `Router.push(url, as, options)` and react-router's
|
|
230
|
+
* `history.push(path, state)` are POSITIONAL, so folding two navigations into
|
|
231
|
+
* one call performs a single navigation with the address bar masked by the
|
|
232
|
+
* second argument. The merge is therefore gated on syntactic evidence that the
|
|
233
|
+
* receiver is an array, and stays silent without it — a missed consolidation
|
|
234
|
+
* costs far less than a fix that changes what the code does.
|
|
235
|
+
*/
|
|
236
|
+
const ARRAY_TYPE_NAMES = new Set(['Array', 'ReadonlyArray']);
|
|
237
|
+
const ARRAY_FACTORY_METHODS = new Set(['from', 'of']);
|
|
238
|
+
const ARRAY_RETURNING_METHODS = new Set([
|
|
239
|
+
'map',
|
|
240
|
+
'filter',
|
|
241
|
+
'slice',
|
|
242
|
+
'concat',
|
|
243
|
+
'split',
|
|
244
|
+
]);
|
|
245
|
+
/** Hooks whose array type argument describes the binding they hand back. */
|
|
246
|
+
const ARRAY_STATE_HOOKS = new Set(['useState', 'useRef']);
|
|
247
|
+
function isFunctionNode(node) {
|
|
248
|
+
return (node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
|
|
249
|
+
node.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
250
|
+
node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression);
|
|
251
|
+
}
|
|
252
|
+
function isArrayTypeNode(node) {
|
|
253
|
+
if (!node)
|
|
254
|
+
return false;
|
|
255
|
+
switch (node.type) {
|
|
256
|
+
case utils_1.AST_NODE_TYPES.TSArrayType:
|
|
257
|
+
case utils_1.AST_NODE_TYPES.TSTupleType:
|
|
258
|
+
return true;
|
|
259
|
+
case utils_1.AST_NODE_TYPES.TSTypeReference:
|
|
260
|
+
return (node.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
261
|
+
ARRAY_TYPE_NAMES.has(node.typeName.name));
|
|
262
|
+
case utils_1.AST_NODE_TYPES.TSTypeOperator:
|
|
263
|
+
return (node.operator === 'readonly' && isArrayTypeNode(node.typeAnnotation));
|
|
264
|
+
/** A union counts only when every member is an array, so `T[] | undefined`
|
|
265
|
+
* — whose `push` can be absent — does not. */
|
|
266
|
+
case utils_1.AST_NODE_TYPES.TSUnionType:
|
|
267
|
+
return node.types.every((member) => isArrayTypeNode(member));
|
|
268
|
+
default:
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
function isArrayAnnotation(annotation) {
|
|
273
|
+
return isArrayTypeNode(annotation?.typeAnnotation);
|
|
274
|
+
}
|
|
275
|
+
function hasArrayTypeArgument(call) {
|
|
276
|
+
return isArrayTypeNode(call.typeParameters?.params[0]);
|
|
277
|
+
}
|
|
278
|
+
function getCalleeName(call) {
|
|
279
|
+
const callee = unwrapExpression(call.callee);
|
|
280
|
+
if (callee.type === utils_1.AST_NODE_TYPES.Identifier)
|
|
281
|
+
return callee.name;
|
|
282
|
+
if (callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
283
|
+
!callee.computed &&
|
|
284
|
+
callee.property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
285
|
+
return callee.property.name;
|
|
286
|
+
}
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
function isArrayStateHookCall(expression) {
|
|
290
|
+
if (!expression)
|
|
291
|
+
return false;
|
|
292
|
+
const node = unwrapExpression(expression);
|
|
293
|
+
if (node.type !== utils_1.AST_NODE_TYPES.CallExpression)
|
|
294
|
+
return false;
|
|
295
|
+
const name = getCalleeName(node);
|
|
296
|
+
return (name !== null && ARRAY_STATE_HOOKS.has(name) && hasArrayTypeArgument(node));
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* A call whose RESULT is an array. The state hooks are deliberately absent:
|
|
300
|
+
* `useRef<T[]>()` hands back a ref object and `useState<T[]>()` a tuple, so
|
|
301
|
+
* only the destructured head of that tuple is an array.
|
|
302
|
+
*/
|
|
303
|
+
function isArrayProducingCall(call) {
|
|
304
|
+
const callee = unwrapExpression(call.callee);
|
|
305
|
+
if (callee.type !== utils_1.AST_NODE_TYPES.MemberExpression ||
|
|
306
|
+
callee.computed ||
|
|
307
|
+
callee.property.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
const object = unwrapExpression(callee.object);
|
|
311
|
+
if (object.type === utils_1.AST_NODE_TYPES.Identifier && object.name === 'Array') {
|
|
312
|
+
return ARRAY_FACTORY_METHODS.has(callee.property.name);
|
|
313
|
+
}
|
|
314
|
+
return ARRAY_RETURNING_METHODS.has(callee.property.name);
|
|
315
|
+
}
|
|
316
|
+
function isArrayInitializer(expression) {
|
|
317
|
+
if (!expression)
|
|
318
|
+
return false;
|
|
319
|
+
const node = unwrapExpression(expression);
|
|
320
|
+
switch (node.type) {
|
|
321
|
+
case utils_1.AST_NODE_TYPES.ArrayExpression:
|
|
322
|
+
return true;
|
|
323
|
+
case utils_1.AST_NODE_TYPES.NewExpression:
|
|
324
|
+
return (node.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
325
|
+
node.callee.name === 'Array');
|
|
326
|
+
case utils_1.AST_NODE_TYPES.CallExpression:
|
|
327
|
+
return isArrayProducingCall(node);
|
|
328
|
+
default:
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
function isArrayDeclarator(declarator, name) {
|
|
333
|
+
if (declarator.id === name) {
|
|
334
|
+
return isArrayInitializer(declarator.init);
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* The head of a `useState<T[]>()` / `useRef<T[]>()` tuple holds the array
|
|
338
|
+
* itself. Every other destructured binding holds an ELEMENT of whatever was
|
|
339
|
+
* destructured, which says nothing about the binding.
|
|
340
|
+
*/
|
|
341
|
+
return (declarator.id.type === utils_1.AST_NODE_TYPES.ArrayPattern &&
|
|
342
|
+
declarator.id.elements[0] === name &&
|
|
343
|
+
isArrayStateHookCall(declarator.init));
|
|
344
|
+
}
|
|
345
|
+
function isArrayParameterElement(param, name) {
|
|
346
|
+
switch (param.type) {
|
|
347
|
+
case utils_1.AST_NODE_TYPES.AssignmentPattern:
|
|
348
|
+
return param.left === name && isArrayInitializer(param.right);
|
|
349
|
+
/** A rest parameter binds an array by construction. */
|
|
350
|
+
case utils_1.AST_NODE_TYPES.RestElement:
|
|
351
|
+
return param.argument === name;
|
|
352
|
+
case utils_1.AST_NODE_TYPES.TSParameterProperty:
|
|
353
|
+
return isArrayParameterElement(param.parameter, name);
|
|
354
|
+
default:
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
function isArrayParameter(fn, name) {
|
|
359
|
+
return fn.params.some((param) => isArrayParameterElement(param, name));
|
|
360
|
+
}
|
|
361
|
+
function hasDefinitionArrayEvidence(def) {
|
|
362
|
+
const name = def.name;
|
|
363
|
+
/** A binding introduced by anything but a plain name carries no annotation. */
|
|
364
|
+
if (name.type !== utils_1.AST_NODE_TYPES.Identifier)
|
|
365
|
+
return false;
|
|
366
|
+
if (isArrayAnnotation(name.typeAnnotation))
|
|
367
|
+
return true;
|
|
368
|
+
if (def.node.type === utils_1.AST_NODE_TYPES.VariableDeclarator) {
|
|
369
|
+
return isArrayDeclarator(def.node, name);
|
|
370
|
+
}
|
|
371
|
+
return isFunctionNode(def.node) && isArrayParameter(def.node, name);
|
|
372
|
+
}
|
|
373
|
+
function isArrayParameterProperty(param, name) {
|
|
374
|
+
const binding = param.parameter.type === utils_1.AST_NODE_TYPES.AssignmentPattern
|
|
375
|
+
? param.parameter.left
|
|
376
|
+
: param.parameter;
|
|
377
|
+
if (binding.type !== utils_1.AST_NODE_TYPES.Identifier || binding.name !== name) {
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
380
|
+
return (isArrayAnnotation(binding.typeAnnotation) ||
|
|
381
|
+
(param.parameter.type === utils_1.AST_NODE_TYPES.AssignmentPattern &&
|
|
382
|
+
isArrayInitializer(param.parameter.right)));
|
|
383
|
+
}
|
|
384
|
+
function hasClassPropertyArrayEvidence(classBody, name) {
|
|
385
|
+
return classBody.body.some((member) => {
|
|
386
|
+
if (member.type === utils_1.AST_NODE_TYPES.PropertyDefinition) {
|
|
387
|
+
if (getPropertyKey(member.key, Boolean(member.computed)) !== name) {
|
|
388
|
+
return false;
|
|
389
|
+
}
|
|
390
|
+
return (isArrayAnnotation(member.typeAnnotation) ||
|
|
391
|
+
isArrayInitializer(member.value));
|
|
392
|
+
}
|
|
393
|
+
if (member.type === utils_1.AST_NODE_TYPES.MethodDefinition &&
|
|
394
|
+
member.kind === 'constructor') {
|
|
395
|
+
return member.value.params.some((param) => param.type === utils_1.AST_NODE_TYPES.TSParameterProperty &&
|
|
396
|
+
isArrayParameterProperty(param, name));
|
|
397
|
+
}
|
|
398
|
+
return false;
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* The class whose fields describe `this` at this position. A plain function
|
|
403
|
+
* rebinds `this` to whatever calls it, so fields declared around it say nothing
|
|
404
|
+
* about the receiver; an arrow function keeps the lexical `this`, and a method's
|
|
405
|
+
* own function expression is the class's.
|
|
406
|
+
*/
|
|
407
|
+
function findEnclosingClassBody(node) {
|
|
408
|
+
let current = node.parent;
|
|
409
|
+
while (current) {
|
|
410
|
+
if (current.type === utils_1.AST_NODE_TYPES.ClassBody)
|
|
411
|
+
return current;
|
|
412
|
+
if (current.type === utils_1.AST_NODE_TYPES.FunctionDeclaration)
|
|
413
|
+
return null;
|
|
414
|
+
if (current.type === utils_1.AST_NODE_TYPES.FunctionExpression) {
|
|
415
|
+
const owner = current.parent;
|
|
416
|
+
const isClassMember = owner !== undefined &&
|
|
417
|
+
(owner.type === utils_1.AST_NODE_TYPES.MethodDefinition ||
|
|
418
|
+
owner.type === utils_1.AST_NODE_TYPES.PropertyDefinition);
|
|
419
|
+
if (!isClassMember)
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
current = current.parent;
|
|
423
|
+
}
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
226
426
|
function isPushCallStatement(statement, sourceCode) {
|
|
227
427
|
if (statement.type !== utils_1.AST_NODE_TYPES.ExpressionStatement)
|
|
228
428
|
return null;
|
|
@@ -332,6 +532,73 @@ exports.flattenPushCalls = (0, createRule_1.createRule)({
|
|
|
332
532
|
defaultOptions: [],
|
|
333
533
|
create(context) {
|
|
334
534
|
const sourceCode = context.getSourceCode();
|
|
535
|
+
/**
|
|
536
|
+
* Evidence is cached per binding rather than per receiver expression, so a
|
|
537
|
+
* long run of pushes onto one array resolves its declaration once.
|
|
538
|
+
*/
|
|
539
|
+
const evidenceByVariable = new Map();
|
|
540
|
+
const evidenceByClassProperty = new Map();
|
|
541
|
+
let variablesByReference = null;
|
|
542
|
+
/**
|
|
543
|
+
* Resolution runs off the scope manager's own reference list rather than
|
|
544
|
+
* the scope of the visited node: `Program` yields the global scope, whose
|
|
545
|
+
* chain does not reach the module scope a top-level `const` lives in.
|
|
546
|
+
*/
|
|
547
|
+
function resolveReference(identifier) {
|
|
548
|
+
if (!variablesByReference) {
|
|
549
|
+
const resolved = new Map();
|
|
550
|
+
sourceCode.scopeManager?.scopes.forEach((scope) => {
|
|
551
|
+
scope.references.forEach((reference) => {
|
|
552
|
+
resolved.set(reference.identifier, reference.resolved);
|
|
553
|
+
});
|
|
554
|
+
});
|
|
555
|
+
variablesByReference = resolved;
|
|
556
|
+
}
|
|
557
|
+
return variablesByReference.get(identifier) ?? null;
|
|
558
|
+
}
|
|
559
|
+
function hasVariableArrayEvidence(variable) {
|
|
560
|
+
const cached = evidenceByVariable.get(variable);
|
|
561
|
+
if (cached !== undefined)
|
|
562
|
+
return cached;
|
|
563
|
+
const evidence = variable.defs.some((def) => hasDefinitionArrayEvidence(def));
|
|
564
|
+
evidenceByVariable.set(variable, evidence);
|
|
565
|
+
return evidence;
|
|
566
|
+
}
|
|
567
|
+
function hasThisPropertyArrayEvidence(receiver) {
|
|
568
|
+
const name = getPropertyKey(receiver.property, Boolean(receiver.computed));
|
|
569
|
+
if (name === null)
|
|
570
|
+
return false;
|
|
571
|
+
const classBody = findEnclosingClassBody(receiver);
|
|
572
|
+
if (!classBody)
|
|
573
|
+
return false;
|
|
574
|
+
const key = `${classBody.range[0]}:${name}`;
|
|
575
|
+
const cached = evidenceByClassProperty.get(key);
|
|
576
|
+
if (cached !== undefined)
|
|
577
|
+
return cached;
|
|
578
|
+
const evidence = hasClassPropertyArrayEvidence(classBody, name);
|
|
579
|
+
evidenceByClassProperty.set(key, evidence);
|
|
580
|
+
return evidence;
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Whether the receiver is syntactically an array. Anything the source does
|
|
584
|
+
* not describe — an unresolved name, a member chain, a call result — counts
|
|
585
|
+
* as no evidence, which keeps the rule silent rather than guessing.
|
|
586
|
+
*/
|
|
587
|
+
function hasArrayReceiverEvidence(receiver) {
|
|
588
|
+
const node = unwrapExpression(receiver);
|
|
589
|
+
if (node.type === utils_1.AST_NODE_TYPES.ArrayExpression)
|
|
590
|
+
return true;
|
|
591
|
+
if (node.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
592
|
+
const variable = resolveReference(node);
|
|
593
|
+
return variable !== null && hasVariableArrayEvidence(variable);
|
|
594
|
+
}
|
|
595
|
+
if (node.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
596
|
+
unwrapExpression(node.object).type ===
|
|
597
|
+
utils_1.AST_NODE_TYPES.ThisExpression) {
|
|
598
|
+
return hasThisPropertyArrayEvidence(node);
|
|
599
|
+
}
|
|
600
|
+
return false;
|
|
601
|
+
}
|
|
335
602
|
function buildChunks(call) {
|
|
336
603
|
return call.arguments.map((arg) => ({
|
|
337
604
|
leading: sourceCode.getCommentsBefore(arg),
|
|
@@ -563,7 +830,11 @@ exports.flattenPushCalls = (0, createRule_1.createRule)({
|
|
|
563
830
|
return false;
|
|
564
831
|
const totalArgs = group.reduce((count, entry) => count + entry.call.arguments.length, 0);
|
|
565
832
|
const firstArgs = group[0].call.arguments.length;
|
|
566
|
-
|
|
833
|
+
if (totalArgs <= firstArgs || !canSafelyFix(group))
|
|
834
|
+
return false;
|
|
835
|
+
const receiver = group[0].call.callee
|
|
836
|
+
.object;
|
|
837
|
+
return hasArrayReceiverEvidence(receiver);
|
|
567
838
|
}
|
|
568
839
|
function reportViolation(group) {
|
|
569
840
|
context.report({
|
|
@@ -306,16 +306,86 @@ const isWriteTarget = (node) => {
|
|
|
306
306
|
}
|
|
307
307
|
};
|
|
308
308
|
/**
|
|
309
|
-
*
|
|
310
|
-
*
|
|
311
|
-
* name
|
|
312
|
-
*
|
|
313
|
-
*
|
|
309
|
+
* The declarator a reference initializes IN WHOLE — `OTHER` in
|
|
310
|
+
* `const OTHER = ITEMS` — or null for every other position. Such a declaration
|
|
311
|
+
* introduces a second name for one value, so whatever is done to that name is
|
|
312
|
+
* done to this binding.
|
|
313
|
+
*
|
|
314
|
+
* Type wrappers are climbed because they annotate a value without replacing it:
|
|
315
|
+
* `const OTHER = ITEMS!` and `const OTHER = ITEMS satisfies T` denote the same
|
|
316
|
+
* array as the bare form, and each breaks the same way once it is frozen. A
|
|
317
|
+
* cast that erases the element type (`ITEMS as any`) is climbed on the same
|
|
318
|
+
* terms, which withholds the assertion from a mutation the compiler would have
|
|
319
|
+
* tolerated — staying silent is the cheap error here, emitting a fix that stops
|
|
320
|
+
* the file compiling is not.
|
|
321
|
+
*
|
|
322
|
+
* A reference that is only PART of an initializer builds a fresh value rather
|
|
323
|
+
* than aliasing this one (`const COPY = [...ITEMS]`), and a destructuring id
|
|
324
|
+
* extracts a member rather than the whole, so neither is an alias here.
|
|
314
325
|
*/
|
|
315
|
-
const
|
|
316
|
-
const
|
|
317
|
-
|
|
318
|
-
|
|
326
|
+
const aliasDeclaratorOf = (identifier) => {
|
|
327
|
+
const value = outermostValueOf(identifier);
|
|
328
|
+
const declarator = value.parent;
|
|
329
|
+
if (declarator?.type !== utils_1.AST_NODE_TYPES.VariableDeclarator ||
|
|
330
|
+
declarator.init !== value ||
|
|
331
|
+
declarator.id.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
return declarator;
|
|
335
|
+
};
|
|
336
|
+
/**
|
|
337
|
+
* Whether the binding is written through anywhere in the file, under its own
|
|
338
|
+
* name or through an alias of it. Answered from the scope manager's reference
|
|
339
|
+
* list rather than a textual search for the name, so a same-named binding in
|
|
340
|
+
* another scope (`const arr` shadowed inside a callback) contributes nothing,
|
|
341
|
+
* and a same-named method on an unrelated receiver (`other.push(1)`) is never
|
|
342
|
+
* even visited.
|
|
343
|
+
*
|
|
344
|
+
* The walk follows aliases because a binding's own reference list is not where
|
|
345
|
+
* a mutation through one is recorded: in
|
|
346
|
+
* `const OTHER = ITEMS; OTHER.push(3);` the mutating call references `OTHER`, a
|
|
347
|
+
* separate variable this one never enrols, and reading only `ITEMS`'s
|
|
348
|
+
* references sees a plain read. Appending `as const` there emits TS2339 for an
|
|
349
|
+
* input that compiled (Issue #2324). Following is transitive — every hop names
|
|
350
|
+
* the one value — and `visited` keeps a chain that leads back on itself, which
|
|
351
|
+
* a redeclared `var` can build, from looping forever.
|
|
352
|
+
*
|
|
353
|
+
* The declaring KEYWORD is deliberately not screened. `as const` types the
|
|
354
|
+
* value `readonly`, and a binding takes its declared type from its initializer,
|
|
355
|
+
* so `let other = ITEMS; other.push(3);` is the same TS2339 as the `const`
|
|
356
|
+
* spelling; reassigning such a `let` does not recover mutability either,
|
|
357
|
+
* because the reassignment is then rejected against that same frozen type. A
|
|
358
|
+
* check keyed on `const` would leave the `let` spelling breaking builds under
|
|
359
|
+
* `--fix`.
|
|
360
|
+
*/
|
|
361
|
+
const isBindingMutated = (variable, declaredVariablesOf) => {
|
|
362
|
+
// Grown in place and walked by index: an alias found mid-walk is appended and
|
|
363
|
+
// reached by the same loop, so the traversal needs no recursion of its own.
|
|
364
|
+
const pending = [variable];
|
|
365
|
+
const visited = new Set(pending);
|
|
366
|
+
for (let index = 0; index < pending.length; index += 1) {
|
|
367
|
+
for (const reference of pending[index].references) {
|
|
368
|
+
const path = accessPathOf(reference.identifier);
|
|
369
|
+
if (path !== null) {
|
|
370
|
+
if (isMutatingMethodCall(path) || isWriteTarget(path)) {
|
|
371
|
+
return true;
|
|
372
|
+
}
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
const declarator = aliasDeclaratorOf(reference.identifier);
|
|
376
|
+
if (!declarator) {
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
for (const alias of declaredVariablesOf(declarator)) {
|
|
380
|
+
if (!visited.has(alias)) {
|
|
381
|
+
visited.add(alias);
|
|
382
|
+
pending.push(alias);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return false;
|
|
388
|
+
};
|
|
319
389
|
/**
|
|
320
390
|
* Walks the scope chain upward from `scope` (inclusive) and reports whether
|
|
321
391
|
* `targetName` is bound anywhere between `scope` and `stopScope` (inclusive).
|
|
@@ -531,6 +601,14 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
531
601
|
return (target.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
532
602
|
!PRIMITIVE_VALUE_GLOBALS.has(target.name));
|
|
533
603
|
};
|
|
604
|
+
/**
|
|
605
|
+
* The bindings a declaration node introduces, as the scope manager records
|
|
606
|
+
* them. The mutation walk resolves an alias declarator through this rather
|
|
607
|
+
* than looking its name up the scope chain: the scope manager already holds
|
|
608
|
+
* the exact answer, while a name lookup would have to guess which scope a
|
|
609
|
+
* `var` was hoisted into.
|
|
610
|
+
*/
|
|
611
|
+
const declaredVariablesOf = (node) => context.getDeclaredVariables(node);
|
|
534
612
|
const describeValueKind = (node) => {
|
|
535
613
|
const target = unwrapValueWrappers(node);
|
|
536
614
|
if (target.type === utils_1.AST_NODE_TYPES.ArrayExpression) {
|
|
@@ -722,7 +800,8 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
722
800
|
const declaredVariable = context
|
|
723
801
|
.getDeclaredVariables(declaration)
|
|
724
802
|
.find((variable) => variable.name === name);
|
|
725
|
-
return !declaredVariable ||
|
|
803
|
+
return (!declaredVariable ||
|
|
804
|
+
!isBindingMutated(declaredVariable, declaredVariablesOf));
|
|
726
805
|
};
|
|
727
806
|
if (shouldHaveAsConst(init)) {
|
|
728
807
|
context.report({
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
import { TSESLint } from '@typescript-eslint/utils';
|
|
2
|
+
export declare const noTryCatchAlreadyExistsInTransaction: TSESLint.RuleModule<"noAlreadyExistsCatchInTransaction", [], TSESLint.RuleListener>;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.noTryCatchAlreadyExistsInTransaction = void 0;
|
|
4
4
|
const utils_1 = require("@typescript-eslint/utils");
|
|
5
|
+
const ASTHelpers_1 = require("../utils/ASTHelpers");
|
|
5
6
|
const createRule_1 = require("../utils/createRule");
|
|
6
7
|
const ALREADY_EXISTS_STRINGS = new Set(['already-exists', 'ALREADY_EXISTS']);
|
|
7
8
|
const ALREADY_EXISTS_NUMBERS = new Set([6, '6']);
|
|
@@ -24,6 +25,118 @@ function isRunTransactionCall(node) {
|
|
|
24
25
|
}
|
|
25
26
|
return false;
|
|
26
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* The package surfaces whose `runTransaction` is the Firestore one.
|
|
30
|
+
*
|
|
31
|
+
* The bare name is not unique to Firestore: `firebase/database` exports a
|
|
32
|
+
* `runTransaction` for the Realtime Database, which re-applies its update
|
|
33
|
+
* function locally on conflict and carries no gRPC status codes, so
|
|
34
|
+
* `ALREADY_EXISTS` is not part of its error model and neither remedy this rule
|
|
35
|
+
* offers exists there — `runCreateForgivenessTransaction` is backend-Firestore
|
|
36
|
+
* only. Reporting an RTDB transaction leaves a developer with no way to comply.
|
|
37
|
+
*/
|
|
38
|
+
const FIRESTORE_MODULE_ROOTS = [
|
|
39
|
+
{ packageSegments: ['firebase'], product: 'firestore' },
|
|
40
|
+
{ packageSegments: ['firebase-admin'], product: 'firestore' },
|
|
41
|
+
{ packageSegments: ['@firebase'], product: 'firestore' },
|
|
42
|
+
{ packageSegments: ['@google-cloud'], product: 'firestore' },
|
|
43
|
+
];
|
|
44
|
+
/**
|
|
45
|
+
* Split a module source into path segments with any version suffix dropped, so
|
|
46
|
+
* a pinned specifier (`firebase@10/firestore`) reduces to the same root as the
|
|
47
|
+
* plain one. A `@` at the start of a segment marks a scope, not a version.
|
|
48
|
+
*/
|
|
49
|
+
function moduleSegments(source) {
|
|
50
|
+
return source.split('/').map((segment) => {
|
|
51
|
+
const versionIndex = segment.indexOf('@', 1);
|
|
52
|
+
return versionIndex === -1 ? segment : segment.slice(0, versionIndex);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Match the package root structurally rather than against one spelling: a deep
|
|
57
|
+
* entry point (`firebase/firestore/lite`), a build variant
|
|
58
|
+
* (`@firebase/firestore-compat`) and a pinned version all name the same
|
|
59
|
+
* product, and a trailing segment must not defeat the check.
|
|
60
|
+
*/
|
|
61
|
+
function isFirestoreModuleSource(source) {
|
|
62
|
+
const segments = moduleSegments(source);
|
|
63
|
+
return FIRESTORE_MODULE_ROOTS.some(({ packageSegments, product }) => {
|
|
64
|
+
if (!packageSegments.every((segment, index) => segments[index] === segment)) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
const productSegment = segments[packageSegments.length];
|
|
68
|
+
return (productSegment === product || !!productSegment?.startsWith(`${product}-`));
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The module `name` is imported from, or null when the file declares the name
|
|
73
|
+
* itself (a local helper, a parameter) or nothing declares it at all.
|
|
74
|
+
*/
|
|
75
|
+
function importedSourceOf(scope, name) {
|
|
76
|
+
const variable = ASTHelpers_1.ASTHelpers.findVariableInScope(scope, name);
|
|
77
|
+
if (!variable) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
for (const def of variable.defs) {
|
|
81
|
+
const specifier = def.node;
|
|
82
|
+
if (specifier.type !== utils_1.AST_NODE_TYPES.ImportSpecifier &&
|
|
83
|
+
specifier.type !== utils_1.AST_NODE_TYPES.ImportDefaultSpecifier &&
|
|
84
|
+
specifier.type !== utils_1.AST_NODE_TYPES.ImportNamespaceSpecifier) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const declaration = specifier.parent;
|
|
88
|
+
if (declaration?.type !== utils_1.AST_NODE_TYPES.ImportDeclaration ||
|
|
89
|
+
typeof declaration.source.value !== 'string') {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
return declaration.source.value;
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* The identifier whose binding carries the call's provenance: the callee for
|
|
98
|
+
* `runTransaction(...)`, and the root of the member chain for
|
|
99
|
+
* `database.runTransaction(...)`, since the receiver is what an import names
|
|
100
|
+
* and the property alone matches every `<anything>.runTransaction`.
|
|
101
|
+
*/
|
|
102
|
+
function provenanceIdentifier(callee) {
|
|
103
|
+
if (callee.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
104
|
+
return callee;
|
|
105
|
+
}
|
|
106
|
+
let current = callee;
|
|
107
|
+
while (current.type === utils_1.AST_NODE_TYPES.MemberExpression ||
|
|
108
|
+
current.type === utils_1.AST_NODE_TYPES.ChainExpression ||
|
|
109
|
+
current.type === utils_1.AST_NODE_TYPES.TSNonNullExpression) {
|
|
110
|
+
current =
|
|
111
|
+
current.type === utils_1.AST_NODE_TYPES.MemberExpression
|
|
112
|
+
? current.object
|
|
113
|
+
: current.expression;
|
|
114
|
+
}
|
|
115
|
+
return current.type === utils_1.AST_NODE_TYPES.Identifier ? current : null;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Whether a `runTransaction` call is the Firestore one this rule speaks about.
|
|
119
|
+
*
|
|
120
|
+
* The gate speaks only when it knows: a binding that resolves to an import is
|
|
121
|
+
* judged by its module source, and anything else — a bare call, a parameter, a
|
|
122
|
+
* local helper, a member call on an unresolvable receiver — keeps the rule's
|
|
123
|
+
* posture of reporting, since a name with no traceable origin is far more often
|
|
124
|
+
* Firestore (`db.runTransaction(...)`) than not.
|
|
125
|
+
*/
|
|
126
|
+
function isFirestoreTransactionCall(node, context) {
|
|
127
|
+
if (!isRunTransactionCall(node)) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
const carrier = provenanceIdentifier(unwrapChainExpression(node.callee));
|
|
131
|
+
if (!carrier) {
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
const source = importedSourceOf(ASTHelpers_1.ASTHelpers.getScope(context, node), carrier.name);
|
|
135
|
+
if (source === null) {
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
return isFirestoreModuleSource(source);
|
|
139
|
+
}
|
|
27
140
|
function getCallbackArgument(args) {
|
|
28
141
|
for (const arg of args) {
|
|
29
142
|
if (arg.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
@@ -301,7 +414,7 @@ exports.noTryCatchAlreadyExistsInTransaction = (0, createRule_1.createRule)({
|
|
|
301
414
|
}
|
|
302
415
|
return {
|
|
303
416
|
CallExpression(node) {
|
|
304
|
-
if (!
|
|
417
|
+
if (!isFirestoreTransactionCall(node, context)) {
|
|
305
418
|
return;
|
|
306
419
|
}
|
|
307
420
|
const callback = getCallbackArgument(node.arguments);
|
|
@@ -310,7 +423,7 @@ exports.noTryCatchAlreadyExistsInTransaction = (0, createRule_1.createRule)({
|
|
|
310
423
|
}
|
|
311
424
|
},
|
|
312
425
|
'CallExpression:exit'(node) {
|
|
313
|
-
if (!
|
|
426
|
+
if (!isFirestoreTransactionCall(node, context)) {
|
|
314
427
|
return;
|
|
315
428
|
}
|
|
316
429
|
const callback = getCallbackArgument(node.arguments);
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.preferSxPropOverSystemProps = void 0;
|
|
4
4
|
const utils_1 = require("@typescript-eslint/utils");
|
|
5
|
+
const ASTHelpers_1 = require("../utils/ASTHelpers");
|
|
5
6
|
const createRule_1 = require("../utils/createRule");
|
|
6
7
|
/**
|
|
7
8
|
* Matches Prettier's own default. The autofix rewrites JSX that a formatter
|
|
@@ -111,7 +112,12 @@ const MUI_SYSTEM_PROPS = new Set([
|
|
|
111
112
|
'textTransform',
|
|
112
113
|
]);
|
|
113
114
|
/**
|
|
114
|
-
*
|
|
115
|
+
* The MUI components this rule covers.
|
|
116
|
+
*
|
|
117
|
+
* The list narrows what provenance has already selected: an element is
|
|
118
|
+
* inspected only when it resolves to an `@mui/*` import AND names one of these,
|
|
119
|
+
* so a name here can never be the whole reason an element is rewritten. The
|
|
120
|
+
* `components` option replaces the list.
|
|
115
121
|
*/
|
|
116
122
|
const DEFAULT_MUI_COMPONENTS = new Set([
|
|
117
123
|
'Box',
|
|
@@ -229,6 +235,78 @@ function isUpperCase(name) {
|
|
|
229
235
|
name[0] === name[0].toUpperCase() &&
|
|
230
236
|
name[0] !== name[0].toLowerCase());
|
|
231
237
|
}
|
|
238
|
+
/**
|
|
239
|
+
* The package namespace every MUI distribution publishes under: `@mui/material`,
|
|
240
|
+
* `@mui/joy`, `@mui/system`, `@mui/lab` and their deep entry points
|
|
241
|
+
* (`@mui/material/Box`).
|
|
242
|
+
*/
|
|
243
|
+
const MUI_PACKAGE_PREFIX = '@mui/';
|
|
244
|
+
const isMuiSource = (source) => source.startsWith(MUI_PACKAGE_PREFIX);
|
|
245
|
+
/**
|
|
246
|
+
* The import that introduces `name`, or null when the file declares it itself
|
|
247
|
+
* (a local component, a parameter) or nothing declares it at all.
|
|
248
|
+
*/
|
|
249
|
+
function importBindingOf(scope, name) {
|
|
250
|
+
const variable = ASTHelpers_1.ASTHelpers.findVariableInScope(scope, name);
|
|
251
|
+
if (!variable) {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
for (const def of variable.defs) {
|
|
255
|
+
const specifier = def.node;
|
|
256
|
+
if (specifier.type !== utils_1.AST_NODE_TYPES.ImportSpecifier &&
|
|
257
|
+
specifier.type !== utils_1.AST_NODE_TYPES.ImportDefaultSpecifier &&
|
|
258
|
+
specifier.type !== utils_1.AST_NODE_TYPES.ImportNamespaceSpecifier) {
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
const declaration = specifier.parent;
|
|
262
|
+
if (declaration?.type !== utils_1.AST_NODE_TYPES.ImportDeclaration ||
|
|
263
|
+
typeof declaration.source.value !== 'string') {
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
return {
|
|
267
|
+
source: declaration.source.value,
|
|
268
|
+
// A default or namespace import has no exported name to read, so the
|
|
269
|
+
// local name is the only thing that names the component.
|
|
270
|
+
exportedName: specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier
|
|
271
|
+
? specifier.imported.name
|
|
272
|
+
: name,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* The MUI export a JSX element names, or null when the element does not come
|
|
279
|
+
* from MUI.
|
|
280
|
+
*
|
|
281
|
+
* Provenance, not spelling, is what makes an element MUI: `Box`, `Button`,
|
|
282
|
+
* `Card` and `Avatar` are ordinary words that design systems, third-party
|
|
283
|
+
* packages and first-party wrappers use too, and this rule ships a fixer that
|
|
284
|
+
* moves props into an `sx` slot a non-MUI component has no reading for. On a
|
|
285
|
+
* wrapper forwarding `width`/`height` to an `<img>`, that rewrite type-checks,
|
|
286
|
+
* lints clean and silently drops the attributes.
|
|
287
|
+
*
|
|
288
|
+
* `<Ns.Box>` resolves through `Ns`, the namespace: the object carries the
|
|
289
|
+
* provenance, so reading the property alone matches every `<Anything.Box>`.
|
|
290
|
+
*/
|
|
291
|
+
function muiExportOf(node, scope) {
|
|
292
|
+
const { name } = node;
|
|
293
|
+
if (name.type === utils_1.AST_NODE_TYPES.JSXIdentifier) {
|
|
294
|
+
const binding = importBindingOf(scope, name.name);
|
|
295
|
+
return binding && isMuiSource(binding.source) ? binding.exportedName : null;
|
|
296
|
+
}
|
|
297
|
+
if (name.type === utils_1.AST_NODE_TYPES.JSXMemberExpression) {
|
|
298
|
+
let object = name.object;
|
|
299
|
+
while (object.type === utils_1.AST_NODE_TYPES.JSXMemberExpression) {
|
|
300
|
+
object = object.object;
|
|
301
|
+
}
|
|
302
|
+
if (object.type !== utils_1.AST_NODE_TYPES.JSXIdentifier) {
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
const binding = importBindingOf(scope, object.name);
|
|
306
|
+
return binding && isMuiSource(binding.source) ? name.property.name : null;
|
|
307
|
+
}
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
232
310
|
/**
|
|
233
311
|
* Convert a string value to a single-quoted JS string literal.
|
|
234
312
|
* Used when building sx property values from JSX string attributes.
|
|
@@ -1023,9 +1101,14 @@ exports.preferSxPropOverSystemProps = (0, createRule_1.createRule)({
|
|
|
1023
1101
|
},
|
|
1024
1102
|
defaultOptions: [{}],
|
|
1025
1103
|
create(context, [options]) {
|
|
1026
|
-
|
|
1104
|
+
// Naming a component here is the documented opt-in for a first-party
|
|
1105
|
+
// wrapper that forwards its props to MUI. Such a wrapper is defined by
|
|
1106
|
+
// living outside `@mui/*`, so the names the user lists are honored whatever
|
|
1107
|
+
// introduced them.
|
|
1108
|
+
const explicitComponents = options.components
|
|
1027
1109
|
? new Set(options.components)
|
|
1028
|
-
:
|
|
1110
|
+
: null;
|
|
1111
|
+
const componentSet = explicitComponents ?? DEFAULT_MUI_COMPONENTS;
|
|
1029
1112
|
const extraAllowed = options.allowedProps
|
|
1030
1113
|
? new Set(options.allowedProps)
|
|
1031
1114
|
: new Set();
|
|
@@ -1052,14 +1135,31 @@ exports.preferSxPropOverSystemProps = (0, createRule_1.createRule)({
|
|
|
1052
1135
|
}
|
|
1053
1136
|
return MUI_SYSTEM_PROPS.has(name) && !isAllowedProp(name);
|
|
1054
1137
|
}
|
|
1138
|
+
/**
|
|
1139
|
+
* The MUI component this element is, or null when the rule leaves it alone.
|
|
1140
|
+
* An element qualifies on two counts: it resolves to a component MUI
|
|
1141
|
+
* exports, and that component is one the rule covers.
|
|
1142
|
+
*/
|
|
1143
|
+
function targetedComponentOf(node) {
|
|
1144
|
+
const writtenName = getComponentName(node);
|
|
1145
|
+
if (!writtenName || !isUpperCase(writtenName)) {
|
|
1146
|
+
return null;
|
|
1147
|
+
}
|
|
1148
|
+
if (explicitComponents?.has(writtenName)) {
|
|
1149
|
+
return writtenName;
|
|
1150
|
+
}
|
|
1151
|
+
const muiExport = muiExportOf(node, ASTHelpers_1.ASTHelpers.getScope(context, node));
|
|
1152
|
+
if (muiExport === null || !componentSet.has(muiExport)) {
|
|
1153
|
+
return null;
|
|
1154
|
+
}
|
|
1155
|
+
// The export name, not the local one: an aliased `Box as MuiBox` is still
|
|
1156
|
+
// MUI's `Box` for the covered-component and owned-prop lookups.
|
|
1157
|
+
return muiExport;
|
|
1158
|
+
}
|
|
1055
1159
|
return {
|
|
1056
1160
|
JSXOpeningElement(node) {
|
|
1057
|
-
const componentName =
|
|
1058
|
-
if (
|
|
1059
|
-
return;
|
|
1060
|
-
if (!isUpperCase(componentName))
|
|
1061
|
-
return;
|
|
1062
|
-
if (!componentSet.has(componentName))
|
|
1161
|
+
const componentName = targetedComponentOf(node);
|
|
1162
|
+
if (componentName === null)
|
|
1063
1163
|
return;
|
|
1064
1164
|
const systemPropAttrs = [];
|
|
1065
1165
|
let sxAttr = null;
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,56 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.21.6",
|
|
4
|
+
"date": "2026-09-04T20:52:21.747Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "global-const-style",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
2324
|
|
11
|
+
],
|
|
12
|
+
"summary": "follow alias chains when detecting mutation (closes #2324)"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "no-try-catch-already-exists-in-transaction",
|
|
16
|
+
"changeType": "fix",
|
|
17
|
+
"issues": [
|
|
18
|
+
2325
|
|
19
|
+
],
|
|
20
|
+
"summary": "gate on Firestore provenance (closes #2325)"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"name": "prefer-sx-prop-over-system-props",
|
|
24
|
+
"changeType": "fix",
|
|
25
|
+
"issues": [
|
|
26
|
+
2323
|
|
27
|
+
],
|
|
28
|
+
"summary": "gate the rewrite on MUI provenance (closes #2323)"
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"version": "1.21.5",
|
|
34
|
+
"date": "2026-09-04T15:01:41.191Z",
|
|
35
|
+
"rules": [
|
|
36
|
+
{
|
|
37
|
+
"name": "enforce-microdiff",
|
|
38
|
+
"changeType": "fix",
|
|
39
|
+
"issues": [
|
|
40
|
+
2322
|
|
41
|
+
],
|
|
42
|
+
"summary": "report jsdiff and fast-diff without rewriting them (closes #2322)"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"name": "flatten-push-calls",
|
|
46
|
+
"changeType": "fix",
|
|
47
|
+
"issues": [
|
|
48
|
+
2321
|
|
49
|
+
],
|
|
50
|
+
"summary": "gate the merge on syntactic array evidence (closes #2321)"
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
},
|
|
2
54
|
{
|
|
3
55
|
"version": "1.21.4",
|
|
4
56
|
"date": "2026-09-04T11:47:38.427Z",
|