@blumintinc/eslint-plugin-blumint 1.21.2 → 1.21.4
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-boolean-naming-prefixes.js +28 -5
- package/lib/rules/enforce-centralized-mock-firestore.d.ts +2 -1
- package/lib/rules/enforce-centralized-mock-firestore.js +173 -38
- package/lib/rules/enforce-fieldpath-syntax-in-docsetter.js +78 -13
- package/lib/rules/enforce-firestore-set-merge.js +155 -4
- package/lib/rules/enforce-object-literal-as-const.js +85 -4
- package/lib/rules/enforce-positive-naming.js +17 -13
- package/lib/rules/enforce-snapshot-state-narrowing.d.ts +1 -1
- package/lib/rules/enforce-snapshot-state-narrowing.js +39 -19
- package/lib/rules/memo-nested-react-components.js +12 -0
- package/lib/rules/no-entire-object-hook-deps.js +26 -3
- package/lib/rules/no-hungarian.js +14 -0
- package/lib/rules/no-inline-component-prop.js +83 -14
- package/lib/rules/no-restricted-properties-fix.js +53 -3
- package/lib/rules/no-useless-usememo-primitives.js +43 -11
- package/lib/rules/prefer-destructuring-no-class.js +65 -11
- package/lib/rules/prefer-document-flattening.js +71 -35
- package/lib/rules/prefer-map-over-conditional-dispatch.js +112 -5
- package/lib/rules/require-memoize-jsx-returners.js +18 -4
- package/lib/rules/use-custom-memo.js +4 -6
- package/lib/utils/memoModule.d.ts +26 -0
- package/lib/utils/memoModule.js +54 -0
- package/package.json +1 -1
- package/release-manifest.json +148 -0
|
@@ -35,6 +35,12 @@ const DEFAULT_OPTIONS = {
|
|
|
35
35
|
allowModuleScopeFactories: true,
|
|
36
36
|
};
|
|
37
37
|
const INLINE_COMPONENT_NAME = 'inline component';
|
|
38
|
+
/**
|
|
39
|
+
* Names followed through wrapper calls before declining. The shapes a fixer
|
|
40
|
+
* emits need two (`const X = memo(XUnmemoized)`, plus one wrapper above it);
|
|
41
|
+
* the bound is what keeps a cycle or a long chain from walking the scope graph.
|
|
42
|
+
*/
|
|
43
|
+
const MAX_ALIAS_HOPS = 3;
|
|
38
44
|
function isPascalCase(name) {
|
|
39
45
|
return /^[A-Z][A-Za-z0-9]*$/.test(name);
|
|
40
46
|
}
|
|
@@ -144,25 +150,42 @@ function getCalleeName(callee) {
|
|
|
144
150
|
}
|
|
145
151
|
return null;
|
|
146
152
|
}
|
|
147
|
-
|
|
153
|
+
const WRAPPER_CALLEE_NAMES = new Set([
|
|
154
|
+
'useCallback',
|
|
155
|
+
'React.useCallback',
|
|
156
|
+
'useMemo',
|
|
157
|
+
'React.useMemo',
|
|
158
|
+
'memo',
|
|
159
|
+
'React.memo',
|
|
160
|
+
'forwardRef',
|
|
161
|
+
'React.forwardRef',
|
|
162
|
+
]);
|
|
163
|
+
/**
|
|
164
|
+
* `resolveIdentifier` is what lets `memo(Inner)` be judged at all. `require-memo`
|
|
165
|
+
* rewrites a nested component into `function InnerUnmemoized() {...}` plus
|
|
166
|
+
* `const Inner = memo(InnerUnmemoized)`, so reading only an inline literal here
|
|
167
|
+
* made that rewrite silence this rule while the hazard survived: the renamed
|
|
168
|
+
* declaration is still recreated per render, and `memo` re-invoked on a fresh
|
|
169
|
+
* argument yields a fresh component type. Callers that cannot decide where the
|
|
170
|
+
* name is declared pass no resolver and keep the literal-only reading.
|
|
171
|
+
*/
|
|
172
|
+
function getFunctionFromCall(call, resolveIdentifier) {
|
|
148
173
|
const calleeName = getCalleeName(call.callee);
|
|
149
|
-
|
|
150
|
-
if (!firstArg || !isFunctionNode(firstArg)) {
|
|
174
|
+
if (!calleeName || !WRAPPER_CALLEE_NAMES.has(calleeName)) {
|
|
151
175
|
return undefined;
|
|
152
176
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
calleeName === 'memo' ||
|
|
158
|
-
calleeName === 'React.memo' ||
|
|
159
|
-
calleeName === 'forwardRef' ||
|
|
160
|
-
calleeName === 'React.forwardRef') {
|
|
177
|
+
const firstArg = unwrapExpression(call.arguments[0] ?? null);
|
|
178
|
+
if (!firstArg)
|
|
179
|
+
return undefined;
|
|
180
|
+
if (isFunctionNode(firstArg)) {
|
|
161
181
|
return firstArg;
|
|
162
182
|
}
|
|
183
|
+
if (firstArg.type === utils_1.AST_NODE_TYPES.Identifier && resolveIdentifier) {
|
|
184
|
+
return resolveIdentifier(firstArg);
|
|
185
|
+
}
|
|
163
186
|
return undefined;
|
|
164
187
|
}
|
|
165
|
-
function getFunctionFromInit(init) {
|
|
188
|
+
function getFunctionFromInit(init, resolveIdentifier) {
|
|
166
189
|
const unwrapped = unwrapExpression(init);
|
|
167
190
|
if (!unwrapped)
|
|
168
191
|
return undefined;
|
|
@@ -170,7 +193,7 @@ function getFunctionFromInit(init) {
|
|
|
170
193
|
return unwrapped;
|
|
171
194
|
}
|
|
172
195
|
if (unwrapped.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
173
|
-
return getFunctionFromCall(unwrapped);
|
|
196
|
+
return getFunctionFromCall(unwrapped, resolveIdentifier);
|
|
174
197
|
}
|
|
175
198
|
return undefined;
|
|
176
199
|
}
|
|
@@ -348,6 +371,52 @@ exports.noInlineComponentProp = (0, createRule_1.createRule)({
|
|
|
348
371
|
}
|
|
349
372
|
return false;
|
|
350
373
|
}
|
|
374
|
+
/**
|
|
375
|
+
* Follows an identifier in a wrapper call's argument position to the
|
|
376
|
+
* function it names, declining wherever the reference does not prove
|
|
377
|
+
* per-render churn.
|
|
378
|
+
*
|
|
379
|
+
* Every decline below is a name whose binding is NOT recreated by the
|
|
380
|
+
* consuming render, or one this rule cannot see the definition of at all:
|
|
381
|
+
* an unresolved name (import, global, ambient), a parameter, a catch or
|
|
382
|
+
* class binding, a name declared more than once, a binding assigned more
|
|
383
|
+
* than once (its identity is not fixed by any single declaration), and a
|
|
384
|
+
* declaration outside the consuming scope. Only a single-write `Variable`
|
|
385
|
+
* or `FunctionName` declared by the very function whose JSX passes it along
|
|
386
|
+
* churns per render.
|
|
387
|
+
*/
|
|
388
|
+
function resolveLocalFunction(identifier, consumerFunction, seen) {
|
|
389
|
+
// An alias chain is bounded so a cycle (`const A = memo(A)`) and a long
|
|
390
|
+
// re-export chain both terminate rather than recursing on the scope graph.
|
|
391
|
+
if (seen.size >= MAX_ALIAS_HOPS)
|
|
392
|
+
return undefined;
|
|
393
|
+
const variable = findVariableInScopes(context, identifier);
|
|
394
|
+
if (!variable || seen.has(variable))
|
|
395
|
+
return undefined;
|
|
396
|
+
seen.add(variable);
|
|
397
|
+
if (variable.defs.length !== 1)
|
|
398
|
+
return undefined;
|
|
399
|
+
const definition = variable.defs[0];
|
|
400
|
+
if (definition.type !== 'Variable' &&
|
|
401
|
+
definition.type !== 'FunctionName') {
|
|
402
|
+
return undefined;
|
|
403
|
+
}
|
|
404
|
+
const writeCount = variable.references.filter((reference) => reference.isWrite()).length;
|
|
405
|
+
if (writeCount > 1)
|
|
406
|
+
return undefined;
|
|
407
|
+
const defNode = definition.node;
|
|
408
|
+
if (resolvedOptions.allowModuleScopeFactories &&
|
|
409
|
+
isStableForConsumer(defNode, consumerFunction)) {
|
|
410
|
+
return undefined;
|
|
411
|
+
}
|
|
412
|
+
if (defNode.type === utils_1.AST_NODE_TYPES.FunctionDeclaration) {
|
|
413
|
+
return defNode;
|
|
414
|
+
}
|
|
415
|
+
if (defNode.type === utils_1.AST_NODE_TYPES.VariableDeclarator) {
|
|
416
|
+
return getFunctionFromInit(defNode.init, (next) => resolveLocalFunction(next, consumerFunction, seen));
|
|
417
|
+
}
|
|
418
|
+
return undefined;
|
|
419
|
+
}
|
|
351
420
|
function shouldReportDefinition(definition, displayName, consumerFunction) {
|
|
352
421
|
if (definition.type === 'ImportBinding' ||
|
|
353
422
|
definition.type === 'Parameter' ||
|
|
@@ -364,7 +433,7 @@ exports.noInlineComponentProp = (0, createRule_1.createRule)({
|
|
|
364
433
|
fnNode = defNode;
|
|
365
434
|
}
|
|
366
435
|
else if (defNode.type === utils_1.AST_NODE_TYPES.VariableDeclarator) {
|
|
367
|
-
fnNode = getFunctionFromInit(defNode.init);
|
|
436
|
+
fnNode = getFunctionFromInit(defNode.init, (identifier) => resolveLocalFunction(identifier, consumerFunction, new Set()));
|
|
368
437
|
}
|
|
369
438
|
else {
|
|
370
439
|
return false;
|
|
@@ -3,6 +3,47 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.noRestrictedPropertiesFix = void 0;
|
|
4
4
|
const createRule_1 = require("../utils/createRule");
|
|
5
5
|
const utils_1 = require("@typescript-eslint/utils");
|
|
6
|
+
/**
|
|
7
|
+
* Mirrors `global-const-style`'s own `toUpperSnakeCase`: both must agree on
|
|
8
|
+
* what a camelCase name becomes so that a config `object` string written
|
|
9
|
+
* before the sibling rule's rename still recognizes the code after it
|
|
10
|
+
* (Issue #2318 -- `global-const-style` renames a module-scope
|
|
11
|
+
* `disallowedObject` const to `DISALLOWED_OBJECT`, and a purely spelling-based
|
|
12
|
+
* match against `disallowedObject` goes silent on the renamed identifier even
|
|
13
|
+
* though the same restricted property is still being read off it). Splitting
|
|
14
|
+
* on case *boundaries* rather than just uppercasing keeps acronym runs intact
|
|
15
|
+
* and reproduces the sibling rule's output exactly.
|
|
16
|
+
*/
|
|
17
|
+
function toUpperSnakeCase(name) {
|
|
18
|
+
return name
|
|
19
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
|
20
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
|
|
21
|
+
.toUpperCase()
|
|
22
|
+
.replace(/^_/, '');
|
|
23
|
+
}
|
|
24
|
+
/** Whether `name` already has the exact shape `global-const-style` emits. */
|
|
25
|
+
function isUpperSnakeCase(name) {
|
|
26
|
+
return /^[A-Z][A-Z0-9_]*$/.test(name);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Matches a configured `object` name against an identifier, tolerating the
|
|
30
|
+
* one rewrite `global-const-style` performs on it: a module-scope const
|
|
31
|
+
* renamed from camelCase to UPPER_SNAKE_CASE. The match stays restricted to
|
|
32
|
+
* identifiers that are ALREADY in that exact UPPER_SNAKE_CASE shape --
|
|
33
|
+
* comparing case-insensitively across the board would also equate a
|
|
34
|
+
* configured `foo` with an unrelated PASCAL-cased `Foo` (a React component
|
|
35
|
+
* name, say), which `global-const-style` never rewrites and which the
|
|
36
|
+
* configured `foo` was never meant to reach. Restricting the tolerant branch
|
|
37
|
+
* to names `global-const-style` could plausibly have produced keeps the
|
|
38
|
+
* broadened match tied to the one rename it is compensating for, rather than
|
|
39
|
+
* a blanket case-insensitive comparison that would invite false positives on
|
|
40
|
+
* unrelated identifiers.
|
|
41
|
+
*/
|
|
42
|
+
function objectNameMatches(identifierName, configuredName) {
|
|
43
|
+
return (identifierName === configuredName ||
|
|
44
|
+
(isUpperSnakeCase(identifierName) &&
|
|
45
|
+
identifierName === toUpperSnakeCase(configuredName)));
|
|
46
|
+
}
|
|
6
47
|
/**
|
|
7
48
|
* This rule is a wrapper around the core ESLint no-restricted-properties rule
|
|
8
49
|
* that adds special handling for Object.keys() and Object.values() results.
|
|
@@ -98,7 +139,7 @@ exports.noRestrictedPropertiesFix = (0, createRule_1.createRule)({
|
|
|
98
139
|
for (const restrictedProp of restrictedProperties) {
|
|
99
140
|
const objectMatches = restrictedProp.object &&
|
|
100
141
|
node.object.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
101
|
-
node.object.name
|
|
142
|
+
objectNameMatches(node.object.name, restrictedProp.object);
|
|
102
143
|
const propertyMatches = restrictedProp.property &&
|
|
103
144
|
((node.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
104
145
|
node.property.name === restrictedProp.property) ||
|
|
@@ -124,9 +165,18 @@ exports.noRestrictedPropertiesFix = (0, createRule_1.createRule)({
|
|
|
124
165
|
restrictedProp.property &&
|
|
125
166
|
propertyMatches) {
|
|
126
167
|
// Check if the object is in the allowObjects list
|
|
127
|
-
|
|
168
|
+
const objectIdentifierName = node.object.type === utils_1.AST_NODE_TYPES.Identifier
|
|
169
|
+
? node.object.name
|
|
170
|
+
: '';
|
|
171
|
+
// `allowObjects` names a BINDING exactly as `object` does, so it
|
|
172
|
+
// must tolerate the same UPPER_SNAKE_CASE rewrite. Normalizing only
|
|
173
|
+
// the restrictive side would let `global-const-style`'s rename turn
|
|
174
|
+
// an explicitly allowed access (`router.push`) into a reported one
|
|
175
|
+
// (#2318).
|
|
176
|
+
const allowObjects = restrictedProp.allowObjects;
|
|
177
|
+
if (allowObjects &&
|
|
128
178
|
node.object.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
129
|
-
|
|
179
|
+
allowObjects.some((allowed) => objectNameMatches(objectIdentifierName, allowed))) {
|
|
130
180
|
continue;
|
|
131
181
|
}
|
|
132
182
|
const objectName = node.object.type === utils_1.AST_NODE_TYPES.Identifier
|
|
@@ -80,16 +80,44 @@ function isStringLikeWithoutTypes(expr) {
|
|
|
80
80
|
function rangesOverlap(a, b) {
|
|
81
81
|
return a[0] < b[1] && b[0] < a[1];
|
|
82
82
|
}
|
|
83
|
-
|
|
83
|
+
/**
|
|
84
|
+
* The memo hooks this rule governs, by the name they are called under.
|
|
85
|
+
*
|
|
86
|
+
* `useDeepCompareMemo` (from `@blumintinc/use-deep-compare`) mirrors `useMemo`
|
|
87
|
+
* argument for argument — a callback whose result is cached against a
|
|
88
|
+
* dependency array — so every judgement below reads the same call shape and
|
|
89
|
+
* every rewrite produces the same text. Memoizing a primitive is equally
|
|
90
|
+
* pointless under it: a value with no identity to preserve gains nothing from a
|
|
91
|
+
* cache, and deep comparison makes the bargain WORSE rather than acceptable,
|
|
92
|
+
* since each render now walks the dependency array by value to protect it.
|
|
93
|
+
*
|
|
94
|
+
* They are enumerated rather than matched by prefix because a fixable,
|
|
95
|
+
* recommended sibling rewrites the callee of a `useMemo` to the deep-compare
|
|
96
|
+
* spelling to repair a dependency-identity problem, leaving the callback
|
|
97
|
+
* byte-identical. Keyed on the literal `useMemo` alone, this rule went silent on
|
|
98
|
+
* the result while the useless memoization it objects to survived the rename
|
|
99
|
+
* untouched — renaming the callee by hand reproduces the blind spot with no
|
|
100
|
+
* fixer involved (#2312).
|
|
101
|
+
*/
|
|
102
|
+
const MEMO_HOOK_NAMES = new Set(['useMemo', 'useDeepCompareMemo']);
|
|
103
|
+
/**
|
|
104
|
+
* The hook a call invokes, or `null` where it is not a memo hook at all.
|
|
105
|
+
*
|
|
106
|
+
* A namespaced call answers with the bare hook name rather than the whole
|
|
107
|
+
* callee text, so `React.useMemo` and `useMemo` report identically: the
|
|
108
|
+
* namespace is the caller's import style, not part of what the report is about.
|
|
109
|
+
*/
|
|
110
|
+
function memoHookNameOf(callee) {
|
|
84
111
|
if (callee.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
85
|
-
return callee.name
|
|
112
|
+
return MEMO_HOOK_NAMES.has(callee.name) ? callee.name : null;
|
|
86
113
|
}
|
|
87
114
|
if (callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
88
115
|
!callee.computed &&
|
|
89
116
|
callee.property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
90
|
-
|
|
117
|
+
const { name } = callee.property;
|
|
118
|
+
return MEMO_HOOK_NAMES.has(name) ? name : null;
|
|
91
119
|
}
|
|
92
|
-
return
|
|
120
|
+
return null;
|
|
93
121
|
}
|
|
94
122
|
function getReturnedExpression(callback) {
|
|
95
123
|
if (callback.body.type !== utils_1.AST_NODE_TYPES.BlockStatement) {
|
|
@@ -320,7 +348,7 @@ exports.noUselessUsememoPrimitives = (0, createRule_1.createRule)({
|
|
|
320
348
|
},
|
|
321
349
|
],
|
|
322
350
|
messages: {
|
|
323
|
-
uselessUseMemoPrimitive: '
|
|
351
|
+
uselessUseMemoPrimitive: '{{hook}} wraps a primitive {{valueKind}}. → Primitives are pass-by-value and have no identity to preserve, so memoization provides zero referential-stability benefit and only adds unnecessary hook overhead. → Remove {{hook}} and inline the expression directly.',
|
|
324
352
|
},
|
|
325
353
|
},
|
|
326
354
|
defaultOptions: [DEFAULT_OPTIONS],
|
|
@@ -361,9 +389,9 @@ exports.noUselessUsememoPrimitives = (0, createRule_1.createRule)({
|
|
|
361
389
|
});
|
|
362
390
|
}
|
|
363
391
|
/**
|
|
364
|
-
* Every
|
|
392
|
+
* Every memo-hook call the rule reports, in traversal order.
|
|
365
393
|
*
|
|
366
|
-
* Reporting is deferred to `Program:exit` because the
|
|
394
|
+
* Reporting is deferred to `Program:exit` because the hook's import is
|
|
367
395
|
* unbound only once no surviving call references it. Judged one call at a
|
|
368
396
|
* time, a file with two of them never sees either as the binding's last
|
|
369
397
|
* use, and the pass that unwraps both resolves every report — so nothing
|
|
@@ -630,7 +658,8 @@ exports.noUselessUsememoPrimitives = (0, createRule_1.createRule)({
|
|
|
630
658
|
}
|
|
631
659
|
return {
|
|
632
660
|
CallExpression(node) {
|
|
633
|
-
|
|
661
|
+
const hook = memoHookNameOf(node.callee);
|
|
662
|
+
if (!hook) {
|
|
634
663
|
return;
|
|
635
664
|
}
|
|
636
665
|
if (node.arguments.length === 0) {
|
|
@@ -677,16 +706,18 @@ exports.noUselessUsememoPrimitives = (0, createRule_1.createRule)({
|
|
|
677
706
|
if (!isPrimitive) {
|
|
678
707
|
return;
|
|
679
708
|
}
|
|
680
|
-
violations.push({ node, returnedExpression, valueKind });
|
|
709
|
+
violations.push({ node, returnedExpression, valueKind, hook });
|
|
681
710
|
},
|
|
682
711
|
'Program:exit'() {
|
|
683
712
|
if (violations.length === 0)
|
|
684
713
|
return;
|
|
685
714
|
const planned = planViolations();
|
|
686
|
-
// One plan over every surviving rewrite: the
|
|
715
|
+
// One plan over every surviving rewrite: the hook's binding is left
|
|
687
716
|
// unreferenced by their union even when no single unwrap strips its
|
|
688
717
|
// last use, and the pass that applies them all resolves every report —
|
|
689
|
-
// so this is the only moment the stranded import is visible.
|
|
718
|
+
// so this is the only moment the stranded import is visible. Which
|
|
719
|
+
// binding that is comes from the deletions themselves, so the
|
|
720
|
+
// deep-compare import is unbound on the same terms as React's.
|
|
690
721
|
const importRemoval = planned.length > 0
|
|
691
722
|
? (0, importRemoval_1.planOrphanedImportRemoval)(sourceCode, planned.flatMap((entry) => entry.removed))
|
|
692
723
|
: null;
|
|
@@ -705,6 +736,7 @@ exports.noUselessUsememoPrimitives = (0, createRule_1.createRule)({
|
|
|
705
736
|
node: violation.node,
|
|
706
737
|
messageId: 'uselessUseMemoPrimitive',
|
|
707
738
|
data: {
|
|
739
|
+
hook: violation.hook,
|
|
708
740
|
valueKind: violation.valueKind,
|
|
709
741
|
},
|
|
710
742
|
fix: violation === carrier?.violation
|
|
@@ -114,16 +114,52 @@ function isStaticClassMember(node, context) {
|
|
|
114
114
|
return false;
|
|
115
115
|
}
|
|
116
116
|
/**
|
|
117
|
-
*
|
|
117
|
+
* Extracts the literal name a MemberExpression's property spells, when that
|
|
118
|
+
* name is comparable to a binding identifier (an Identifier's own name, or a
|
|
119
|
+
* string Literal's value). A non-string literal (e.g. `obj[0]`) can never
|
|
120
|
+
* equal an identifier name, so it is excluded here rather than at every
|
|
121
|
+
* caller.
|
|
118
122
|
*/
|
|
119
|
-
function
|
|
123
|
+
function getComparablePropertyName(propertyNode) {
|
|
120
124
|
if (propertyNode.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
121
|
-
return propertyNode.name
|
|
125
|
+
return propertyNode.name;
|
|
122
126
|
}
|
|
123
|
-
if (propertyNode.type === utils_1.AST_NODE_TYPES.Literal
|
|
124
|
-
|
|
127
|
+
if (propertyNode.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
128
|
+
typeof propertyNode.value === 'string') {
|
|
129
|
+
return propertyNode.value;
|
|
125
130
|
}
|
|
126
|
-
return
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Check if the property name matches the variable name in an assignment
|
|
135
|
+
*/
|
|
136
|
+
function isMatchingPropertyName(propertyNode, variableName) {
|
|
137
|
+
return getComparablePropertyName(propertyNode) === variableName;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Strips underscores and lowercases a name so a SCREAMING_SNAKE_CASE binding
|
|
141
|
+
* compares equal to the camelCase property it reads (`MY_VALUE` vs
|
|
142
|
+
* `myValue`), not merely a pure case shift (`FOO` vs `foo`). A rename tool
|
|
143
|
+
* such as `global-const-style` reshapes only the BINDING to fit a naming
|
|
144
|
+
* convention; it never touches the property expression on the right-hand
|
|
145
|
+
* side, so the two spellings diverge in casing and word separators without
|
|
146
|
+
* the assignment becoming a genuine rename of what the binding refers to.
|
|
147
|
+
*/
|
|
148
|
+
function normalizeForLooseNameMatch(name) {
|
|
149
|
+
return name.replace(/_/g, '').toLowerCase();
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Case/underscore-insensitive counterpart to `isMatchingPropertyName`, used
|
|
153
|
+
* only where `enforceForRenamedProperties` is off. The default gate must
|
|
154
|
+
* still recognize `const FOO = obj.foo;` as the same property access it
|
|
155
|
+
* recognizes for `const foo = obj.foo;` (#2316), while a genuinely different
|
|
156
|
+
* name (`TOTAL` vs `count`) keeps failing the match.
|
|
157
|
+
*/
|
|
158
|
+
function isMatchingPropertyNameIgnoringCase(propertyNode, variableName) {
|
|
159
|
+
const propertyName = getComparablePropertyName(propertyNode);
|
|
160
|
+
return (propertyName !== null &&
|
|
161
|
+
normalizeForLooseNameMatch(propertyName) ===
|
|
162
|
+
normalizeForLooseNameMatch(variableName));
|
|
127
163
|
}
|
|
128
164
|
/**
|
|
129
165
|
* Get the property text for destructuring
|
|
@@ -235,13 +271,20 @@ exports.preferDestructuringNoClass = (0, createRule_1.createRule)({
|
|
|
235
271
|
if (isPrivateIdentifierProperty(memberExpression.property)) {
|
|
236
272
|
return false;
|
|
237
273
|
}
|
|
274
|
+
// `super.x` has no destructurable form: `const { x } = super;` is a
|
|
275
|
+
// syntax error, since `super` must be followed by a call or a member
|
|
276
|
+
// access. Reachable for any binding whose name matches the property in
|
|
277
|
+
// any casing, so the guard belongs here rather than at one call site.
|
|
278
|
+
if (memberExpression.object.type === utils_1.AST_NODE_TYPES.Super) {
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
238
281
|
if (!options.object) {
|
|
239
282
|
return false;
|
|
240
283
|
}
|
|
241
284
|
if (options.enforceForRenamedProperties) {
|
|
242
285
|
return true;
|
|
243
286
|
}
|
|
244
|
-
return
|
|
287
|
+
return isMatchingPropertyNameIgnoringCase(memberExpression.property, identifier.name);
|
|
245
288
|
}
|
|
246
289
|
function getPatternKeyText(memberExpression, propertyText) {
|
|
247
290
|
if (memberExpression.computed) {
|
|
@@ -254,9 +297,15 @@ exports.preferDestructuringNoClass = (0, createRule_1.createRule)({
|
|
|
254
297
|
return null;
|
|
255
298
|
}
|
|
256
299
|
const patternKeyText = getPatternKeyText(memberExpression, propertyText);
|
|
300
|
+
// Alias whenever the destructured key would not itself spell the
|
|
301
|
+
// target binding: a computed key never can, and any literal spelling
|
|
302
|
+
// mismatch — an explicit rename under `enforceForRenamedProperties`,
|
|
303
|
+
// or a case/underscore-only difference the default gate now tolerates
|
|
304
|
+
// (#2316) — must keep `key: target`, or the emitted destructuring
|
|
305
|
+
// binds the wrong name (or, for `const { FOO } = OBJ;` where the
|
|
306
|
+
// property is `foo`, no name at all).
|
|
257
307
|
if (memberExpression.computed ||
|
|
258
|
-
(
|
|
259
|
-
!isMatchingPropertyName(memberExpression.property, targetName))) {
|
|
308
|
+
!isMatchingPropertyName(memberExpression.property, targetName)) {
|
|
260
309
|
return `${patternKeyText}: ${targetName}`;
|
|
261
310
|
}
|
|
262
311
|
return patternKeyText;
|
|
@@ -323,8 +372,13 @@ exports.preferDestructuringNoClass = (0, createRule_1.createRule)({
|
|
|
323
372
|
function buildReportDetails(memberExpr, targetName, examplePrefix, exampleSuffix) {
|
|
324
373
|
const objectText = sourceCode.getText(memberExpr.object);
|
|
325
374
|
const propertyText = getPropertyText(memberExpr.property, memberExpr.computed, sourceCode);
|
|
326
|
-
|
|
327
|
-
|
|
375
|
+
// Tracks whether the FIXED destructuring needs an alias — the same
|
|
376
|
+
// question `getDestructuringBindingText` answers — so the message
|
|
377
|
+
// wording ("with renaming", the target-name note) stays truthful for
|
|
378
|
+
// a case/underscore-only spelling difference the default gate now
|
|
379
|
+
// reports (#2316), not only for an explicit
|
|
380
|
+
// `enforceForRenamedProperties` rename.
|
|
381
|
+
const usesRenaming = !!targetName &&
|
|
328
382
|
!isMatchingPropertyName(memberExpr.property, targetName);
|
|
329
383
|
const aliasName = targetName ?? propertyText;
|
|
330
384
|
const patternKeyText = getPatternKeyText(memberExpr, propertyText);
|
|
@@ -20,6 +20,63 @@ function isObjectExpression(node) {
|
|
|
20
20
|
function isProperty(node) {
|
|
21
21
|
return node.type === utils_1.AST_NODE_TYPES.Property;
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Resolves the property name every static spelling of a key denotes:
|
|
25
|
+
* `shouldFlatten`, `'shouldFlatten'` and `['shouldFlatten']` all occupy the
|
|
26
|
+
* same slot, so a literal that writes one of them cannot gain another without
|
|
27
|
+
* duplicating the key. A key built from an expression denotes an unknown
|
|
28
|
+
* property and yields no name.
|
|
29
|
+
*/
|
|
30
|
+
function staticKeyName(property) {
|
|
31
|
+
const key = property.key;
|
|
32
|
+
if (!property.computed && isIdentifier(key)) {
|
|
33
|
+
return key.name;
|
|
34
|
+
}
|
|
35
|
+
if (key.type === utils_1.AST_NODE_TYPES.Literal && typeof key.value === 'string') {
|
|
36
|
+
return key.value;
|
|
37
|
+
}
|
|
38
|
+
if (key.type === utils_1.AST_NODE_TYPES.TemplateLiteral &&
|
|
39
|
+
key.expressions.length === 0 &&
|
|
40
|
+
key.quasis.length === 1) {
|
|
41
|
+
return key.quasis[0].value.cooked;
|
|
42
|
+
}
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The single answer to "does this options literal already write shouldFlatten".
|
|
47
|
+
* The reader that decides whether to report and the writer that builds the
|
|
48
|
+
* suggestion share it so they cannot disagree about which members exist: a
|
|
49
|
+
* writer that misses a member the reader sees appends a duplicate key.
|
|
50
|
+
*/
|
|
51
|
+
function findShouldFlattenProperty(options) {
|
|
52
|
+
for (const property of options.properties) {
|
|
53
|
+
if (!isProperty(property))
|
|
54
|
+
continue;
|
|
55
|
+
if (staticKeyName(property) === 'shouldFlatten') {
|
|
56
|
+
return property;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
function isLiteralBoolean(node, expected) {
|
|
62
|
+
return node.type === utils_1.AST_NODE_TYPES.Literal && node.value === expected;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Flattening counts as enabled only for a literal `true`. A variable, a
|
|
66
|
+
* ternary or a call may evaluate either way, so those are treated as unknown
|
|
67
|
+
* and the violation is still reported.
|
|
68
|
+
*/
|
|
69
|
+
function hasEnabledShouldFlatten(newExpr) {
|
|
70
|
+
if (newExpr.arguments.length < 2) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
const optionsArg = newExpr.arguments[1];
|
|
74
|
+
if (!isObjectExpression(optionsArg)) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
const property = findShouldFlattenProperty(optionsArg);
|
|
78
|
+
return !!property && isLiteralBoolean(property.value, true);
|
|
79
|
+
}
|
|
23
80
|
/**
|
|
24
81
|
* Appends an entry to a comma-separated list by anchoring on its last element
|
|
25
82
|
* and deriving the separator from whatever already follows that element.
|
|
@@ -105,6 +162,18 @@ exports.preferDocumentFlattening = (0, createRule_1.createRule)({
|
|
|
105
162
|
if (!isObjectExpression(optionsArg)) {
|
|
106
163
|
return null;
|
|
107
164
|
}
|
|
165
|
+
const existing = findShouldFlattenProperty(optionsArg);
|
|
166
|
+
if (existing) {
|
|
167
|
+
// Appending a second `shouldFlatten` member is never correct: the
|
|
168
|
+
// literal would carry the key twice (TS1117, and core no-dupe-keys).
|
|
169
|
+
// A literal `false` is rewritten in place; any other value — a
|
|
170
|
+
// variable, a ternary, a call, a shorthand reference, an accessor —
|
|
171
|
+
// may already be true, so the edit is declined rather than guessed.
|
|
172
|
+
if (isLiteralBoolean(existing.value, false)) {
|
|
173
|
+
return fixer.replaceText(existing.value, 'true');
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
108
177
|
const lastEntry = optionsArg.properties[optionsArg.properties.length - 1];
|
|
109
178
|
if (!lastEntry) {
|
|
110
179
|
// An empty object offers no entry to anchor on, so the opening brace
|
|
@@ -148,25 +217,8 @@ exports.preferDocumentFlattening = (0, createRule_1.createRule)({
|
|
|
148
217
|
// Only check DocSetter and DocSetterTransaction classes
|
|
149
218
|
if (className !== 'DocSetter' && className !== 'DocSetterTransaction')
|
|
150
219
|
return;
|
|
151
|
-
// Check if shouldFlatten option is provided
|
|
152
|
-
let hasShouldFlatten = false;
|
|
153
220
|
// The options object is typically the second argument
|
|
154
|
-
|
|
155
|
-
const optionsArg = node.arguments[1];
|
|
156
|
-
if (isObjectExpression(optionsArg)) {
|
|
157
|
-
for (const property of optionsArg.properties) {
|
|
158
|
-
if (!isProperty(property))
|
|
159
|
-
continue;
|
|
160
|
-
if (isIdentifier(property.key) &&
|
|
161
|
-
property.key.name === 'shouldFlatten' &&
|
|
162
|
-
property.value.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
163
|
-
property.value.value === true) {
|
|
164
|
-
hasShouldFlatten = true;
|
|
165
|
-
break;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
}
|
|
221
|
+
const hasShouldFlatten = hasEnabledShouldFlatten(node);
|
|
170
222
|
// Get variable name from parent node if it's a variable declaration
|
|
171
223
|
let instanceName = '';
|
|
172
224
|
if (node.parent &&
|
|
@@ -203,23 +255,7 @@ exports.preferDocumentFlattening = (0, createRule_1.createRule)({
|
|
|
203
255
|
const className = object.callee.name;
|
|
204
256
|
if (className === 'DocSetter' ||
|
|
205
257
|
className === 'DocSetterTransaction') {
|
|
206
|
-
|
|
207
|
-
if (object.arguments.length >= 2) {
|
|
208
|
-
const optionsArg = object.arguments[1];
|
|
209
|
-
if (isObjectExpression(optionsArg)) {
|
|
210
|
-
for (const property of optionsArg.properties) {
|
|
211
|
-
if (!isProperty(property))
|
|
212
|
-
continue;
|
|
213
|
-
if (isIdentifier(property.key) &&
|
|
214
|
-
property.key.name === 'shouldFlatten' &&
|
|
215
|
-
property.value.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
216
|
-
property.value.value === true) {
|
|
217
|
-
hasShouldFlatten = true;
|
|
218
|
-
break;
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
}
|
|
258
|
+
const hasShouldFlatten = hasEnabledShouldFlatten(object);
|
|
223
259
|
if (!hasShouldFlatten) {
|
|
224
260
|
instance = {
|
|
225
261
|
className,
|