@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.
@@ -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 FAST_DEEP_EQUAL_MODULES = new Set([
9
- 'fast-deep-equal',
10
- 'fast-deep-equal/es6',
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
- FAST_DEEP_EQUAL_MODULES.has(declaration.source.value));
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 `fast-deep-equal`.\n" +
56
- 'Why it matters: Without `fast-deep-equal`, equality checks keep building diff entries just to count them, adding overhead and obscuring intent.\n' +
57
- 'How to fix: Add a default import for `fast-deep-equal` as `{{fastEqualName}}` and use `{{fastEqualName}}(a, b)` for equality checks.',
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 single
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 most once per
419
- * file. Claiming the carrier slot is a side effect, so this runs last —
420
- * after every reason to decline the fix has been ruled out — otherwise a
421
- * declining violation takes the import down with it and leaves the
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 === 'microdiff');
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
- // Check for microdiff import
513
- if (importSource === 'microdiff') {
514
- hasMicrodiffImport = true;
515
- // Get the local name of the imported diff function
516
- node.specifiers.forEach((specifier) => {
517
- if (specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
518
- specifier.imported.name === 'diff') {
519
- microdiffImportName = specifier.local.name;
520
- }
521
- if (specifier.type === utils_1.AST_NODE_TYPES.ImportDefaultSpecifier) {
522
- microdiffImportName = specifier.local.name;
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
- // Check for fast-deep-equal import
527
- if (importSource === 'fast-deep-equal' ||
528
- importSource === 'fast-deep-equal/es6') {
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
- export declare const noClassInstanceDestructuring: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"noClassInstanceDestructuring", [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
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(initText, prop) {
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 `${initText}.${keyText}`;
135
+ return `${receiverText}.${keyText}`;
48
136
  }
49
- return `${initText}[${keyText}]`;
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, initText) {
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(initText, prop));
148
+ .map((prop) => buildAccessPath(receiverText, prop));
61
149
  if (accessPaths.length === 0) {
62
- return `\`${initText}.<member>\``;
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 initText = sourceCode.getText(node.init);
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: formatAccessExamples(objectPattern.properties, initText),
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
- // For single property, use simple replacement
112
- if (properties.length === 1) {
113
- const prop = properties[0];
114
- if (prop.type === utils_1.AST_NODE_TYPES.Property) {
115
- const value = prop.value.type === utils_1.AST_NODE_TYPES.Identifier
116
- ? prop.value.name
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
- // For multiple properties, create multiple declarations
124
- const declarations = properties
125
- .filter((prop) => prop.type === utils_1.AST_NODE_TYPES.Property)
126
- .map((prop) => {
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
- return fixer.replaceText(node, declarations);
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.20.49",
3
+ "version": "1.20.51",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",