@blumintinc/eslint-plugin-blumint 1.20.49 → 1.20.51
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/consistent-callback-naming.js +13 -5
- package/lib/rules/enforce-dynamic-imports.js +3 -1
- package/lib/rules/enforce-early-destructuring.js +13 -3
- package/lib/rules/enforce-microdiff.js +169 -72
- package/lib/rules/enforce-object-literal-as-const.js +185 -4
- package/lib/rules/enforce-timestamp-now.d.ts +2 -1
- package/lib/rules/enforce-timestamp-now.js +226 -71
- package/lib/rules/fast-deep-equal-over-microdiff.js +83 -38
- package/lib/rules/no-class-instance-destructuring.d.ts +2 -1
- package/lib/rules/no-class-instance-destructuring.js +240 -32
- package/lib/utils/fastDeepEqualModules.d.ts +30 -0
- package/lib/utils/fastDeepEqualModules.js +40 -0
- package/lib/utils/microdiffModules.d.ts +15 -0
- package/lib/utils/microdiffModules.js +22 -0
- package/package.json +1 -1
- package/release-manifest.json +84 -0
|
@@ -5,10 +5,46 @@ const utils_1 = require("@typescript-eslint/utils");
|
|
|
5
5
|
const createRule_1 = require("../utils/createRule");
|
|
6
6
|
const ASTHelpers_1 = require("../utils/ASTHelpers");
|
|
7
7
|
const disableDirectives_1 = require("../utils/disableDirectives");
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
const microdiffModules_1 = require("../utils/microdiffModules");
|
|
9
|
+
const fastDeepEqualModules_1 = require("../utils/fastDeepEqualModules");
|
|
10
|
+
const DIFF_EXPORT_NAME = 'diff';
|
|
11
|
+
/**
|
|
12
|
+
* Whether a declaration imports values — a `import type ...` declaration binds
|
|
13
|
+
* nothing at runtime, so it neither supplies a callable `diff` nor satisfies
|
|
14
|
+
* the fast-deep-equal import the fix emits.
|
|
15
|
+
*/
|
|
16
|
+
function isValueImport(declaration) {
|
|
17
|
+
return declaration.importKind !== 'type';
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Whether a specifier binds microdiff's diff function: the module default, or
|
|
21
|
+
* its named `diff` export under whatever local name. Type-only specifiers are
|
|
22
|
+
* excluded for the same reason a type-only declaration is.
|
|
23
|
+
*
|
|
24
|
+
* The check matters because microdiff's other exports are types — a file
|
|
25
|
+
* importing only `Difference` binds no `diff` at all, and treating that import
|
|
26
|
+
* as proof of one turns any local `diff(...)` in the file into a violation.
|
|
27
|
+
*/
|
|
28
|
+
function bindsMicrodiffDiff(specifier) {
|
|
29
|
+
if (specifier.type === utils_1.AST_NODE_TYPES.ImportDefaultSpecifier) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
return (specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
|
|
33
|
+
specifier.importKind !== 'type' &&
|
|
34
|
+
specifier.imported.name === DIFF_EXPORT_NAME);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Whether a specifier binds a callable equality function rather than the module
|
|
38
|
+
* object: the default export the package is, or a named specifier of it. A
|
|
39
|
+
* namespace import binds the module, and a bare `import '...'` binds nothing at
|
|
40
|
+
* all — reading either as the file's equality function leaves the emitted
|
|
41
|
+
* `isEqual(...)` call with no declaration behind it.
|
|
42
|
+
*/
|
|
43
|
+
function bindsFastDeepEqualFunction(specifier) {
|
|
44
|
+
return (specifier.type === utils_1.AST_NODE_TYPES.ImportDefaultSpecifier ||
|
|
45
|
+
(specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
|
|
46
|
+
specifier.importKind !== 'type'));
|
|
47
|
+
}
|
|
12
48
|
/**
|
|
13
49
|
* Whether every declaration of a visible binding is a fast-deep-equal import,
|
|
14
50
|
* i.e. the name already means the comparison function the fix wants to call.
|
|
@@ -26,7 +62,8 @@ function bindsFastDeepEqual(variable) {
|
|
|
26
62
|
}
|
|
27
63
|
const declaration = specifier.parent;
|
|
28
64
|
return (declaration?.type === utils_1.AST_NODE_TYPES.ImportDeclaration &&
|
|
29
|
-
|
|
65
|
+
isValueImport(declaration) &&
|
|
66
|
+
fastDeepEqualModules_1.FAST_DEEP_EQUAL_MODULES.has(String(declaration.source.value)));
|
|
30
67
|
}));
|
|
31
68
|
}
|
|
32
69
|
/**
|
|
@@ -52,9 +89,9 @@ exports.fastDeepEqualOverMicrodiff = (0, createRule_1.createRule)({
|
|
|
52
89
|
useFastDeepEqual: "What's wrong: This code uses `{{diffName}}(...).length` as a deep equality check.\n" +
|
|
53
90
|
'Why it matters: `{{diffName}}` allocates a full change list (paths, types, values) before you compare it to zero, which hides the boolean intent and wastes memory/time.\n' +
|
|
54
91
|
'How to fix: Call `{{fastEqualName}}(left, right)` for equality (or prefix with `!` for inequality) using the same two arguments instead of counting diff length.',
|
|
55
|
-
addFastDeepEqualImport: "What's wrong: This file checks equality via `{{diffName}}(...).length` but does not import
|
|
56
|
-
'Why it matters: Without
|
|
57
|
-
'How to fix: Add a default import
|
|
92
|
+
addFastDeepEqualImport: "What's wrong: This file checks equality via `{{diffName}}(...).length` but does not import a deep-equality function.\n" +
|
|
93
|
+
'Why it matters: Without `@blumintinc/fast-deep-equal`, equality checks keep building diff entries just to count them, adding overhead and obscuring intent.\n' +
|
|
94
|
+
'How to fix: Add a default import from `@blumintinc/fast-deep-equal` as `{{fastEqualName}}` and use `{{fastEqualName}}(a, b)` for equality checks.',
|
|
58
95
|
},
|
|
59
96
|
},
|
|
60
97
|
defaultOptions: [],
|
|
@@ -67,8 +104,8 @@ exports.fastDeepEqualOverMicrodiff = (0, createRule_1.createRule)({
|
|
|
67
104
|
const reportedNodes = new Set();
|
|
68
105
|
let plannedFastDeepEqualImport = false;
|
|
69
106
|
/**
|
|
70
|
-
* The `import ... from 'fast-deep-equal'` statement rides on a
|
|
71
|
-
* violation's fix, so that violation is the file's import carrier. A
|
|
107
|
+
* The `import ... from '@blumintinc/fast-deep-equal'` statement rides on a
|
|
108
|
+
* single violation's fix, so that violation is the file's import carrier. A
|
|
72
109
|
* suppressed carrier would take the import down with it while the surviving
|
|
73
110
|
* violations still emit `isEqual(...)` calls, leaving them unbound.
|
|
74
111
|
*/
|
|
@@ -415,28 +452,29 @@ exports.fastDeepEqualOverMicrodiff = (0, createRule_1.createRule)({
|
|
|
415
452
|
return fixes;
|
|
416
453
|
}
|
|
417
454
|
/**
|
|
418
|
-
* The `import ... from 'fast-deep-equal'` edit, scheduled at
|
|
419
|
-
* file. Claiming the carrier slot is a side effect, so this
|
|
420
|
-
* after every reason to decline the fix has been ruled out —
|
|
421
|
-
* declining violation takes the import down with it and leaves
|
|
422
|
-
* surviving violations' `isEqual(...)` calls unbound.
|
|
455
|
+
* The `import ... from '@blumintinc/fast-deep-equal'` edit, scheduled at
|
|
456
|
+
* most once per file. Claiming the carrier slot is a side effect, so this
|
|
457
|
+
* runs last — after every reason to decline the fix has been ruled out —
|
|
458
|
+
* otherwise a declining violation takes the import down with it and leaves
|
|
459
|
+
* the surviving violations' `isEqual(...)` calls unbound.
|
|
460
|
+
*
|
|
461
|
+
* The specifier is the scoped fork: it is the dependency this codebase
|
|
462
|
+
* declares, so any other name would be written as an import that resolves
|
|
463
|
+
* nowhere.
|
|
423
464
|
*/
|
|
424
465
|
function planFastDeepEqualImport(fixer) {
|
|
425
466
|
if (hasFastDeepEqualImport || plannedFastDeepEqualImport) {
|
|
426
467
|
return [];
|
|
427
468
|
}
|
|
428
469
|
plannedFastDeepEqualImport = true;
|
|
470
|
+
const importStatement = (0, fastDeepEqualModules_1.fastDeepEqualImport)(fastDeepEqualImportName);
|
|
429
471
|
const importDeclarations = sourceCode.ast.body.filter((statement) => statement.type === utils_1.AST_NODE_TYPES.ImportDeclaration);
|
|
430
|
-
const microdiffImport = importDeclarations.find((declaration) => declaration.source.value
|
|
472
|
+
const microdiffImport = importDeclarations.find((declaration) => microdiffModules_1.MICRODIFF_MODULES.has(String(declaration.source.value)));
|
|
431
473
|
const anchor = microdiffImport ?? importDeclarations[importDeclarations.length - 1];
|
|
432
474
|
if (anchor) {
|
|
433
|
-
return [
|
|
434
|
-
fixer.insertTextAfter(anchor, `\nimport ${fastDeepEqualImportName} from 'fast-deep-equal';`),
|
|
435
|
-
];
|
|
475
|
+
return [fixer.insertTextAfter(anchor, `\n${importStatement}`)];
|
|
436
476
|
}
|
|
437
|
-
return [
|
|
438
|
-
fixer.insertTextBeforeRange([0, 0], `import ${fastDeepEqualImportName} from 'fast-deep-equal';\n`),
|
|
439
|
-
];
|
|
477
|
+
return [fixer.insertTextBeforeRange([0, 0], `${importStatement}\n`)];
|
|
440
478
|
}
|
|
441
479
|
/**
|
|
442
480
|
* Create a fix for replacing microdiff equality check with fast-deep-equal
|
|
@@ -508,24 +546,31 @@ exports.fastDeepEqualOverMicrodiff = (0, createRule_1.createRule)({
|
|
|
508
546
|
return {
|
|
509
547
|
// Track imports of microdiff and fast-deep-equal
|
|
510
548
|
ImportDeclaration(node) {
|
|
511
|
-
const importSource = node.source.value;
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
549
|
+
const importSource = String(node.source.value);
|
|
550
|
+
if (!isValueImport(node)) {
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
// A microdiff import counts only once it binds the diff function
|
|
554
|
+
// itself. Every other export of the package is a type, so
|
|
555
|
+
// `import { Difference } from '@blumintinc/microdiff'` brings in
|
|
556
|
+
// nothing callable — reading it as microdiff's presence turns an
|
|
557
|
+
// unrelated local `diff(...)` in that file into a violation whose fix
|
|
558
|
+
// rewrites it to an equality check.
|
|
559
|
+
if (microdiffModules_1.MICRODIFF_MODULES.has(importSource)) {
|
|
560
|
+
node.specifiers.filter(bindsMicrodiffDiff).forEach((specifier) => {
|
|
561
|
+
hasMicrodiffImport = true;
|
|
562
|
+
microdiffImportName = specifier.local.name;
|
|
524
563
|
});
|
|
525
564
|
}
|
|
526
|
-
//
|
|
527
|
-
|
|
528
|
-
|
|
565
|
+
// An equality function the file already has, under any of the
|
|
566
|
+
// specifiers that resolve to one — the scoped fork this codebase
|
|
567
|
+
// depends on, its React entry point, and upstream. Missing one of them
|
|
568
|
+
// costs the fix twice over: a second import of a function already in
|
|
569
|
+
// scope, and, when that import's local name is the one the fix emits, a
|
|
570
|
+
// collision with it that makes the fix decline outright and leaves the
|
|
571
|
+
// report with no remedy.
|
|
572
|
+
if (fastDeepEqualModules_1.FAST_DEEP_EQUAL_MODULES.has(importSource) &&
|
|
573
|
+
node.specifiers.some(bindsFastDeepEqualFunction)) {
|
|
529
574
|
hasFastDeepEqualImport = true;
|
|
530
575
|
// Get the local name of the imported isEqual function
|
|
531
576
|
node.specifiers.forEach((specifier) => {
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
import { TSESLint } from '@typescript-eslint/utils';
|
|
2
|
+
export declare const noClassInstanceDestructuring: TSESLint.RuleModule<"noClassInstanceDestructuring", [], TSESLint.RuleListener>;
|
|
@@ -3,6 +3,94 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.noClassInstanceDestructuring = void 0;
|
|
4
4
|
const utils_1 = require("@typescript-eslint/utils");
|
|
5
5
|
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
/**
|
|
7
|
+
* A synthesized binding may not be a keyword, and may not be one of the
|
|
8
|
+
* bare-word globals whose rebinding would be legal but ruinous.
|
|
9
|
+
*/
|
|
10
|
+
const UNUSABLE_NAMES = new Set([
|
|
11
|
+
'break',
|
|
12
|
+
'case',
|
|
13
|
+
'catch',
|
|
14
|
+
'class',
|
|
15
|
+
'const',
|
|
16
|
+
'continue',
|
|
17
|
+
'debugger',
|
|
18
|
+
'default',
|
|
19
|
+
'delete',
|
|
20
|
+
'do',
|
|
21
|
+
'else',
|
|
22
|
+
'enum',
|
|
23
|
+
'export',
|
|
24
|
+
'extends',
|
|
25
|
+
'false',
|
|
26
|
+
'finally',
|
|
27
|
+
'for',
|
|
28
|
+
'function',
|
|
29
|
+
'if',
|
|
30
|
+
'implements',
|
|
31
|
+
'import',
|
|
32
|
+
'in',
|
|
33
|
+
'instanceof',
|
|
34
|
+
'interface',
|
|
35
|
+
'let',
|
|
36
|
+
'new',
|
|
37
|
+
'null',
|
|
38
|
+
'package',
|
|
39
|
+
'private',
|
|
40
|
+
'protected',
|
|
41
|
+
'public',
|
|
42
|
+
'return',
|
|
43
|
+
'static',
|
|
44
|
+
'super',
|
|
45
|
+
'switch',
|
|
46
|
+
'this',
|
|
47
|
+
'throw',
|
|
48
|
+
'true',
|
|
49
|
+
'try',
|
|
50
|
+
'typeof',
|
|
51
|
+
'var',
|
|
52
|
+
'void',
|
|
53
|
+
'while',
|
|
54
|
+
'with',
|
|
55
|
+
'yield',
|
|
56
|
+
'await',
|
|
57
|
+
'arguments',
|
|
58
|
+
'eval',
|
|
59
|
+
'undefined',
|
|
60
|
+
'NaN',
|
|
61
|
+
'Infinity',
|
|
62
|
+
]);
|
|
63
|
+
/**
|
|
64
|
+
* Node types whose children are statements, so a single statement may be
|
|
65
|
+
* replaced by several. Anything else (a `for` initializer, an unbraced `if`
|
|
66
|
+
* body, a label) can hold exactly one.
|
|
67
|
+
*/
|
|
68
|
+
const STATEMENT_CONTAINERS = new Set([
|
|
69
|
+
utils_1.AST_NODE_TYPES.Program,
|
|
70
|
+
utils_1.AST_NODE_TYPES.BlockStatement,
|
|
71
|
+
utils_1.AST_NODE_TYPES.StaticBlock,
|
|
72
|
+
utils_1.AST_NODE_TYPES.SwitchCase,
|
|
73
|
+
utils_1.AST_NODE_TYPES.TSModuleBlock,
|
|
74
|
+
]);
|
|
75
|
+
const IDENTIFIER_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
76
|
+
function isUsableName(name) {
|
|
77
|
+
return IDENTIFIER_PATTERN.test(name) && !UNUSABLE_NAMES.has(name);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* `Person` -> `person`, `URL` -> `url`, `URLParser` -> `urlParser`. The leading
|
|
81
|
+
* run of capitals is treated as one word so an acronym does not become `uRL`.
|
|
82
|
+
*/
|
|
83
|
+
function lowerFirstWord(name) {
|
|
84
|
+
const leadingCapitals = /^[A-Z]+/.exec(name);
|
|
85
|
+
if (!leadingCapitals)
|
|
86
|
+
return name;
|
|
87
|
+
const run = leadingCapitals[0];
|
|
88
|
+
if (run.length === name.length)
|
|
89
|
+
return name.toLowerCase();
|
|
90
|
+
if (run.length === 1)
|
|
91
|
+
return name[0].toLowerCase() + name.slice(1);
|
|
92
|
+
return run.slice(0, -1).toLowerCase() + name.slice(run.length - 1);
|
|
93
|
+
}
|
|
6
94
|
exports.noClassInstanceDestructuring = (0, createRule_1.createRule)({
|
|
7
95
|
name: 'no-class-instance-destructuring',
|
|
8
96
|
meta: {
|
|
@@ -41,12 +129,12 @@ exports.noClassInstanceDestructuring = (0, createRule_1.createRule)({
|
|
|
41
129
|
}
|
|
42
130
|
return 'member';
|
|
43
131
|
}
|
|
44
|
-
function buildAccessPath(
|
|
132
|
+
function buildAccessPath(receiverText, prop) {
|
|
45
133
|
const keyText = sourceCode.getText(prop.key);
|
|
46
134
|
if (prop.key.type === utils_1.AST_NODE_TYPES.Identifier && !prop.computed) {
|
|
47
|
-
return `${
|
|
135
|
+
return `${receiverText}.${keyText}`;
|
|
48
136
|
}
|
|
49
|
-
return `${
|
|
137
|
+
return `${receiverText}[${keyText}]`;
|
|
50
138
|
}
|
|
51
139
|
function formatMembers(properties) {
|
|
52
140
|
const memberNames = properties.map(describeMember).filter(Boolean);
|
|
@@ -54,12 +142,12 @@ exports.noClassInstanceDestructuring = (0, createRule_1.createRule)({
|
|
|
54
142
|
return '`<members>`';
|
|
55
143
|
return memberNames.map((name) => `\`${name}\``).join(', ');
|
|
56
144
|
}
|
|
57
|
-
function formatAccessExamples(properties,
|
|
145
|
+
function formatAccessExamples(properties, receiverText) {
|
|
58
146
|
const accessPaths = properties
|
|
59
147
|
.filter((prop) => prop.type === utils_1.AST_NODE_TYPES.Property)
|
|
60
|
-
.map((prop) => buildAccessPath(
|
|
148
|
+
.map((prop) => buildAccessPath(receiverText, prop));
|
|
61
149
|
if (accessPaths.length === 0) {
|
|
62
|
-
return `\`${
|
|
150
|
+
return `\`${receiverText}.<member>\``;
|
|
63
151
|
}
|
|
64
152
|
return accessPaths.map((path) => `\`${path}\``).join(', ');
|
|
65
153
|
}
|
|
@@ -81,20 +169,134 @@ exports.noClassInstanceDestructuring = (0, createRule_1.createRule)({
|
|
|
81
169
|
}
|
|
82
170
|
return false;
|
|
83
171
|
}
|
|
172
|
+
/**
|
|
173
|
+
* `new Person` and `new Person()` construct alike, but only the latter can
|
|
174
|
+
* carry a member access: `new Person.name` reads `name` off the class and
|
|
175
|
+
* constructs that instead.
|
|
176
|
+
*/
|
|
177
|
+
function callableInitText(init) {
|
|
178
|
+
const text = sourceCode.getText(init);
|
|
179
|
+
if (init.type === utils_1.AST_NODE_TYPES.NewExpression && init.arguments) {
|
|
180
|
+
const lastToken = sourceCode.getLastToken(init);
|
|
181
|
+
if (lastToken?.value !== ')')
|
|
182
|
+
return `${text}()`;
|
|
183
|
+
}
|
|
184
|
+
return text;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Every name that is visible where the temp binding would be inserted,
|
|
188
|
+
* including names only referenced (globals, imports resolved elsewhere) and
|
|
189
|
+
* names bound by nested closures that read through this scope. Reusing any
|
|
190
|
+
* of them would either redeclare or shadow a live binding — see the
|
|
191
|
+
* shadow-capture guard in `src/tests/fixer-shadow-capture.test.ts`.
|
|
192
|
+
*/
|
|
193
|
+
function collectVisibleNames(scope) {
|
|
194
|
+
const names = new Set();
|
|
195
|
+
for (let current = scope; current; current = current.upper) {
|
|
196
|
+
for (const variable of current.variables) {
|
|
197
|
+
names.add(variable.name);
|
|
198
|
+
}
|
|
199
|
+
for (const reference of current.through) {
|
|
200
|
+
names.add(reference.identifier.name);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return names;
|
|
204
|
+
}
|
|
205
|
+
function calleeName(init) {
|
|
206
|
+
const { callee } = init;
|
|
207
|
+
if (callee.type === utils_1.AST_NODE_TYPES.Identifier)
|
|
208
|
+
return callee.name;
|
|
209
|
+
if (callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
210
|
+
!callee.computed &&
|
|
211
|
+
callee.property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
212
|
+
return callee.property.name;
|
|
213
|
+
}
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
function baseNameFor(init) {
|
|
217
|
+
if (init.type !== utils_1.AST_NODE_TYPES.NewExpression)
|
|
218
|
+
return 'instance';
|
|
219
|
+
const raw = calleeName(init);
|
|
220
|
+
if (!raw)
|
|
221
|
+
return 'instance';
|
|
222
|
+
const lowered = lowerFirstWord(raw);
|
|
223
|
+
// An already-lowercase callee would have the temp shadow the class itself.
|
|
224
|
+
if (lowered !== raw && isUsableName(lowered))
|
|
225
|
+
return lowered;
|
|
226
|
+
const suffixed = `${lowered}Instance`;
|
|
227
|
+
return isUsableName(suffixed) ? suffixed : 'instance';
|
|
228
|
+
}
|
|
229
|
+
function uniqueName(base, taken) {
|
|
230
|
+
if (!taken.has(base))
|
|
231
|
+
return base;
|
|
232
|
+
for (let suffix = 2; suffix <= taken.size + 2; suffix++) {
|
|
233
|
+
const candidate = `${base}${suffix}`;
|
|
234
|
+
if (!taken.has(candidate))
|
|
235
|
+
return candidate;
|
|
236
|
+
}
|
|
237
|
+
return base;
|
|
238
|
+
}
|
|
239
|
+
function indentOf(node) {
|
|
240
|
+
const line = sourceCode.lines[node.loc.start.line - 1] ?? '';
|
|
241
|
+
const leading = /^[\t ]*/.exec(line);
|
|
242
|
+
return leading ? leading[0] : '';
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* The statement that may be swapped for several declarations, or `null`
|
|
246
|
+
* when the declarator does not own a whole statement of its own.
|
|
247
|
+
*/
|
|
248
|
+
function resolveStatement(node) {
|
|
249
|
+
const declaration = node.parent;
|
|
250
|
+
if (declaration?.type !== utils_1.AST_NODE_TYPES.VariableDeclaration)
|
|
251
|
+
return null;
|
|
252
|
+
// Splitting one declarator out of `const {a, b} = inst, c = 1;` would have
|
|
253
|
+
// to hoist emitted lines past declarators this fix does not own.
|
|
254
|
+
if (declaration.declarations.length !== 1)
|
|
255
|
+
return null;
|
|
256
|
+
const exportDeclaration = declaration.parent?.type === utils_1.AST_NODE_TYPES.ExportNamedDeclaration
|
|
257
|
+
? declaration.parent
|
|
258
|
+
: null;
|
|
259
|
+
const statement = exportDeclaration ?? declaration;
|
|
260
|
+
if (!STATEMENT_CONTAINERS.has(statement.parent?.type ?? ''))
|
|
261
|
+
return null;
|
|
262
|
+
const text = sourceCode.getText(statement);
|
|
263
|
+
return {
|
|
264
|
+
statement,
|
|
265
|
+
kind: declaration.kind,
|
|
266
|
+
// Re-exporting each extracted member keeps the module's public surface,
|
|
267
|
+
// while the temp binding stays private.
|
|
268
|
+
exportPrefix: exportDeclaration ? 'export ' : '',
|
|
269
|
+
indent: indentOf(statement),
|
|
270
|
+
// Matching the source's own terminator keeps semicolon-free files intact.
|
|
271
|
+
terminator: text.endsWith(';') ? ';' : '',
|
|
272
|
+
};
|
|
273
|
+
}
|
|
84
274
|
return {
|
|
85
275
|
VariableDeclarator(node) {
|
|
86
276
|
if (node.id.type === utils_1.AST_NODE_TYPES.ObjectPattern &&
|
|
87
277
|
node.init &&
|
|
88
278
|
isClassInstance(node.init)) {
|
|
89
279
|
const objectPattern = node.id;
|
|
90
|
-
const
|
|
280
|
+
const init = node.init;
|
|
281
|
+
const initText = sourceCode.getText(init);
|
|
282
|
+
const propertyCount = objectPattern.properties.filter((prop) => prop.type === utils_1.AST_NODE_TYPES.Property).length;
|
|
283
|
+
// Reading two members off `new Person()` twice would construct twice,
|
|
284
|
+
// so the instance is bound once and read from. A plain identifier is
|
|
285
|
+
// already such a binding, and a lone member read constructs once.
|
|
286
|
+
const needsTempBinding = init.type !== utils_1.AST_NODE_TYPES.Identifier && propertyCount > 1;
|
|
287
|
+
const tempName = needsTempBinding
|
|
288
|
+
? uniqueName(baseNameFor(init), collectVisibleNames(context.getScope()))
|
|
289
|
+
: null;
|
|
290
|
+
const receiverText = tempName ?? callableInitText(init);
|
|
91
291
|
context.report({
|
|
92
292
|
node,
|
|
93
293
|
messageId: 'noClassInstanceDestructuring',
|
|
94
294
|
data: {
|
|
95
295
|
members: formatMembers(objectPattern.properties),
|
|
96
296
|
instance: `\`${initText}\``,
|
|
97
|
-
suggestion:
|
|
297
|
+
suggestion: tempName
|
|
298
|
+
? `\`const ${tempName} = ${initText};\` then ${formatAccessExamples(objectPattern.properties, tempName)}`
|
|
299
|
+
: formatAccessExamples(objectPattern.properties, receiverText),
|
|
98
300
|
},
|
|
99
301
|
fix(fixer) {
|
|
100
302
|
const properties = objectPattern.properties;
|
|
@@ -108,33 +310,39 @@ exports.noClassInstanceDestructuring = (0, createRule_1.createRule)({
|
|
|
108
310
|
// the code to something more weakly typed than the author wrote.
|
|
109
311
|
if (objectPattern.typeAnnotation)
|
|
110
312
|
return null;
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
: sourceCode.getText(prop.value);
|
|
118
|
-
const accessPath = buildAccessPath(initText, prop);
|
|
119
|
-
return fixer.replaceText(node, `${value} = ${accessPath}`);
|
|
120
|
-
}
|
|
313
|
+
if (properties.length === 0)
|
|
314
|
+
return null;
|
|
315
|
+
// A rest element collects the members no other property names, a
|
|
316
|
+
// set only the type checker knows, so it cannot become a member
|
|
317
|
+
// read. Rewriting the siblings alone would delete the binding.
|
|
318
|
+
if (properties.some((prop) => prop.type !== utils_1.AST_NODE_TYPES.Property)) {
|
|
121
319
|
return null;
|
|
122
320
|
}
|
|
123
|
-
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const value = prop.value.type === utils_1.AST_NODE_TYPES.Identifier
|
|
128
|
-
? prop.value.name
|
|
129
|
-
: sourceCode.getText(prop.value);
|
|
130
|
-
const accessPath = buildAccessPath(initText, prop);
|
|
131
|
-
return `${value} = ${accessPath}`;
|
|
132
|
-
})
|
|
133
|
-
.join(';\nconst ');
|
|
134
|
-
// Only apply the fix if we have valid declarations
|
|
135
|
-
if (!declarations)
|
|
321
|
+
const props = properties;
|
|
322
|
+
// `const {a = 1} = inst` applies the default only when the member
|
|
323
|
+
// is `undefined`; a plain member read cannot express that.
|
|
324
|
+
if (props.some((prop) => prop.value.type === utils_1.AST_NODE_TYPES.AssignmentPattern)) {
|
|
136
325
|
return null;
|
|
137
|
-
|
|
326
|
+
}
|
|
327
|
+
const targetOf = (prop) => prop.value.type === utils_1.AST_NODE_TYPES.Identifier
|
|
328
|
+
? prop.value.name
|
|
329
|
+
: sourceCode.getText(prop.value);
|
|
330
|
+
// A single binding stays a single declarator, so it can be
|
|
331
|
+
// rewritten in place wherever the declaration sits.
|
|
332
|
+
if (props.length === 1) {
|
|
333
|
+
return fixer.replaceText(node, `${targetOf(props[0])} = ${buildAccessPath(receiverText, props[0])}`);
|
|
334
|
+
}
|
|
335
|
+
const target = resolveStatement(node);
|
|
336
|
+
if (!target)
|
|
337
|
+
return null;
|
|
338
|
+
const lines = tempName ? [`const ${tempName} = ${initText}`] : [];
|
|
339
|
+
for (const prop of props) {
|
|
340
|
+
lines.push(`${target.exportPrefix}${target.kind} ${targetOf(prop)} = ${buildAccessPath(receiverText, prop)}`);
|
|
341
|
+
}
|
|
342
|
+
// One replacement, so the whole rewrite lands or none of it does.
|
|
343
|
+
return fixer.replaceText(target.statement, lines
|
|
344
|
+
.map((line) => `${line}${target.terminator}`)
|
|
345
|
+
.join(`\n${target.indent}`));
|
|
138
346
|
},
|
|
139
347
|
});
|
|
140
348
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BluMint's fast-deep-equal fork: the dependency this codebase declares, and
|
|
3
|
+
* therefore the only specifier a fixer may emit. An unscoped `fast-deep-equal`
|
|
4
|
+
* import resolves nowhere here (TS2307).
|
|
5
|
+
*/
|
|
6
|
+
export declare const FAST_DEEP_EQUAL_MODULE = "@blumintinc/fast-deep-equal";
|
|
7
|
+
/**
|
|
8
|
+
* Every specifier that resolves to a deep-equality function of the same shape:
|
|
9
|
+
* the fork, its React entry point, upstream, and upstream's ESM build. A file on
|
|
10
|
+
* any of them already has the comparison this rule asks for, so all four have to
|
|
11
|
+
* be read identically when deciding whether an import is still needed.
|
|
12
|
+
*
|
|
13
|
+
* The literals live here rather than in each rule because the microdiff pair
|
|
14
|
+
* drifted twice: a rule carrying only the unscoped name is inert on every call
|
|
15
|
+
* site in a codebase that depends on the fork, and a fixer emitting the unscoped
|
|
16
|
+
* name writes an import that does not resolve.
|
|
17
|
+
*/
|
|
18
|
+
export declare const FAST_DEEP_EQUAL_MODULES: ReadonlySet<string>;
|
|
19
|
+
/**
|
|
20
|
+
* The import statement a fixer writes to bind the equality function to
|
|
21
|
+
* `localName`.
|
|
22
|
+
*
|
|
23
|
+
* It is assembled here, beside the literal, rather than in the rule that emits
|
|
24
|
+
* it: `src/tests/enforce-dynamic-imports.test.ts` derives the modules this
|
|
25
|
+
* plugin's fixers inject by resolving module-scope string constants *within each
|
|
26
|
+
* source file*, so a specifier interpolated from an imported constant reads as
|
|
27
|
+
* unresolved and drops out of the check that every injected module is one the
|
|
28
|
+
* recommended config accepts.
|
|
29
|
+
*/
|
|
30
|
+
export declare const fastDeepEqualImport: (localName: string) => string;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.fastDeepEqualImport = exports.FAST_DEEP_EQUAL_MODULES = exports.FAST_DEEP_EQUAL_MODULE = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* BluMint's fast-deep-equal fork: the dependency this codebase declares, and
|
|
6
|
+
* therefore the only specifier a fixer may emit. An unscoped `fast-deep-equal`
|
|
7
|
+
* import resolves nowhere here (TS2307).
|
|
8
|
+
*/
|
|
9
|
+
exports.FAST_DEEP_EQUAL_MODULE = '@blumintinc/fast-deep-equal';
|
|
10
|
+
/**
|
|
11
|
+
* Every specifier that resolves to a deep-equality function of the same shape:
|
|
12
|
+
* the fork, its React entry point, upstream, and upstream's ESM build. A file on
|
|
13
|
+
* any of them already has the comparison this rule asks for, so all four have to
|
|
14
|
+
* be read identically when deciding whether an import is still needed.
|
|
15
|
+
*
|
|
16
|
+
* The literals live here rather than in each rule because the microdiff pair
|
|
17
|
+
* drifted twice: a rule carrying only the unscoped name is inert on every call
|
|
18
|
+
* site in a codebase that depends on the fork, and a fixer emitting the unscoped
|
|
19
|
+
* name writes an import that does not resolve.
|
|
20
|
+
*/
|
|
21
|
+
exports.FAST_DEEP_EQUAL_MODULES = new Set([
|
|
22
|
+
exports.FAST_DEEP_EQUAL_MODULE,
|
|
23
|
+
'@blumintinc/fast-deep-equal/react',
|
|
24
|
+
'fast-deep-equal',
|
|
25
|
+
'fast-deep-equal/es6',
|
|
26
|
+
]);
|
|
27
|
+
/**
|
|
28
|
+
* The import statement a fixer writes to bind the equality function to
|
|
29
|
+
* `localName`.
|
|
30
|
+
*
|
|
31
|
+
* It is assembled here, beside the literal, rather than in the rule that emits
|
|
32
|
+
* it: `src/tests/enforce-dynamic-imports.test.ts` derives the modules this
|
|
33
|
+
* plugin's fixers inject by resolving module-scope string constants *within each
|
|
34
|
+
* source file*, so a specifier interpolated from an imported constant reads as
|
|
35
|
+
* unresolved and drops out of the check that every injected module is one the
|
|
36
|
+
* recommended config accepts.
|
|
37
|
+
*/
|
|
38
|
+
const fastDeepEqualImport = (localName) => `import ${localName} from '${exports.FAST_DEEP_EQUAL_MODULE}';`;
|
|
39
|
+
exports.fastDeepEqualImport = fastDeepEqualImport;
|
|
40
|
+
//# sourceMappingURL=fastDeepEqualModules.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BluMint's microdiff fork: the dependency this codebase declares, and the
|
|
3
|
+
* specifier every real call site resolves against.
|
|
4
|
+
*/
|
|
5
|
+
export declare const MICRODIFF_MODULE = "@blumintinc/microdiff";
|
|
6
|
+
/**
|
|
7
|
+
* Every specifier that resolves to microdiff. The fork and upstream are the
|
|
8
|
+
* same library under two names, so a file on either one is diffing
|
|
9
|
+
* structurally and has to be matched identically.
|
|
10
|
+
*
|
|
11
|
+
* The literal lives here rather than in each rule because the pair drifted
|
|
12
|
+
* twice: a rule carrying only the unscoped name is inert on every call site in
|
|
13
|
+
* a codebase that depends on the fork.
|
|
14
|
+
*/
|
|
15
|
+
export declare const MICRODIFF_MODULES: ReadonlySet<string>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MICRODIFF_MODULES = exports.MICRODIFF_MODULE = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* BluMint's microdiff fork: the dependency this codebase declares, and the
|
|
6
|
+
* specifier every real call site resolves against.
|
|
7
|
+
*/
|
|
8
|
+
exports.MICRODIFF_MODULE = '@blumintinc/microdiff';
|
|
9
|
+
/**
|
|
10
|
+
* Every specifier that resolves to microdiff. The fork and upstream are the
|
|
11
|
+
* same library under two names, so a file on either one is diffing
|
|
12
|
+
* structurally and has to be matched identically.
|
|
13
|
+
*
|
|
14
|
+
* The literal lives here rather than in each rule because the pair drifted
|
|
15
|
+
* twice: a rule carrying only the unscoped name is inert on every call site in
|
|
16
|
+
* a codebase that depends on the fork.
|
|
17
|
+
*/
|
|
18
|
+
exports.MICRODIFF_MODULES = new Set([
|
|
19
|
+
exports.MICRODIFF_MODULE,
|
|
20
|
+
'microdiff',
|
|
21
|
+
]);
|
|
22
|
+
//# sourceMappingURL=microdiffModules.js.map
|