@blumintinc/eslint-plugin-blumint 1.20.50 → 1.20.52

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 CHANGED
@@ -223,7 +223,7 @@ function noFrontendImportsFromFunctionsPatterns(pattern) {
223
223
  module.exports = {
224
224
  meta: {
225
225
  name: '@blumintinc/eslint-plugin-blumint',
226
- version: '1.20.50',
226
+ version: '1.20.52',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -36,9 +36,11 @@ exports.DEFAULT_IGNORED_LIBRARIES = [
36
36
  'use-latest-callback',
37
37
  '@blumintinc/typescript-memoize',
38
38
  '@blumintinc/use-deep-compare',
39
+ '@blumintinc/microdiff',
39
40
  'microdiff',
40
41
  'safe-stable-stringify',
41
- 'fast-deep-equal', // fast-deep-equal-over-microdiff
42
+ '@blumintinc/fast-deep-equal',
43
+ 'fast-deep-equal', // fast-deep-equal-over-microdiff, for files already on upstream
42
44
  ];
43
45
  exports.DEFAULT_INTERNAL_PREFIXES = ['src/', 'functions/'];
44
46
  // Pre-built set of Node.js core module names for O(1) lookup.
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.enforceM3SentenceCase = void 0;
4
4
  const utils_1 = require("@typescript-eslint/utils");
5
+ const compilePatternOption_1 = require("../utils/compilePatternOption");
5
6
  const createRule_1 = require("../utils/createRule");
6
7
  /**
7
8
  * Default props that carry user-facing label text, per the issue spec.
@@ -452,7 +453,11 @@ exports.enforceM3SentenceCase = (0, createRule_1.createRule)({
452
453
  ...DEFAULT_IGNORED_WORDS,
453
454
  ...(options.ignoredWords ?? []),
454
455
  ]);
455
- const ignorePatternRegexes = (options.ignorePatterns ?? []).map((p) => new RegExp(p));
456
+ // Rejecting a malformed `ignorePatterns` entry rather than dropping it keeps
457
+ // the consumer's exception list honest: a silently discarded pattern would
458
+ // make text they deliberately excluded start getting reported with no
459
+ // indication why.
460
+ const ignorePatternRegexes = (0, compilePatternOption_1.compilePatternOption)('enforce-m3-sentence-case', 'ignorePatterns', options.ignorePatterns ?? []);
456
461
  const allowListSet = new Set(options.allowList ?? []);
457
462
  const checkJsxText = options.checkJsxText !== false;
458
463
  /**
@@ -5,7 +5,25 @@ 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 DIFF_NAME = 'diff';
8
- const MICRODIFF_MODULE = 'microdiff';
8
+ /**
9
+ * The package the fix imports from. BluMint's fork is the dependency this
10
+ * codebase declares, and every call site resolves against it.
11
+ */
12
+ const MICRODIFF_MODULE = '@blumintinc/microdiff';
13
+ /**
14
+ * The specifiers that already satisfy this rule. The fix emits the fork, but a
15
+ * file importing upstream `microdiff` is diffing structurally all the same, so
16
+ * both are recognised on the way in: an unrecognised one would earn a second
17
+ * import of the same binding (TS2300).
18
+ */
19
+ const MICRODIFF_MODULES = new Set([MICRODIFF_MODULE, 'microdiff']);
20
+ /**
21
+ * The import the fix emits. Both packages export their diff function as the
22
+ * module *default* — `Difference`, `MicrodiffOptions` and the `default*`
23
+ * predicates are the only named exports — so a `{ diff }` specifier binds
24
+ * nothing (TS2305) however the module resolves.
25
+ */
26
+ const MICRODIFF_IMPORT = `import ${DIFF_NAME} from '${MICRODIFF_MODULE}';`;
9
27
  /**
10
28
  * The names a competing library's diff export is conventionally bound to. They
11
29
  * are candidates for a report, never proof of one: what a call resolves to
@@ -68,7 +86,7 @@ function bindsMicrodiffDiff(specifier) {
68
86
  */
69
87
  function findMicrodiffImport(program) {
70
88
  return program.body.find((statement) => statement.type === utils_1.AST_NODE_TYPES.ImportDeclaration &&
71
- statement.source.value === MICRODIFF_MODULE &&
89
+ MICRODIFF_MODULES.has(String(statement.source.value)) &&
72
90
  statement.importKind !== 'type' &&
73
91
  statement.specifiers.some(bindsMicrodiffDiff));
74
92
  }
@@ -85,7 +103,7 @@ function collectClaimableSpecifiers(program) {
85
103
  return;
86
104
  }
87
105
  const source = statement.source.value;
88
- if (source === MICRODIFF_MODULE) {
106
+ if (MICRODIFF_MODULES.has(source)) {
89
107
  statement.specifiers
90
108
  .filter(bindsMicrodiffDiff)
91
109
  .forEach((specifier) => claimable.add(specifier));
@@ -166,6 +184,28 @@ function toImportedDiffSource(variable, importedSpecifiers) {
166
184
  }
167
185
  return null;
168
186
  }
187
+ /**
188
+ * Whether a call carries operands `diff(obj, newObj, options?)` accepts.
189
+ * microdiff needs both sides of the comparison, so a call supplying fewer is
190
+ * reported but left alone: rewriting it would trade an unresolved name for a
191
+ * call that does not type-check (TS2554).
192
+ */
193
+ function hasRewritableArity(call) {
194
+ return call.arguments.length >= 2;
195
+ }
196
+ /**
197
+ * Whether a reference sits where the call fix rewrites it — as the callee of a
198
+ * convertible call. A reference in any other position (passed as a value,
199
+ * assigned to a variable, re-exported) has no rewrite of its own, so retiring
200
+ * the import that binds it would leave it unresolved (TS2304).
201
+ */
202
+ function isRewrittenCallee(identifier) {
203
+ const parent = identifier.parent;
204
+ return (!!parent &&
205
+ parent.type === utils_1.AST_NODE_TYPES.CallExpression &&
206
+ parent.callee === identifier &&
207
+ hasRewritableArity(parent));
208
+ }
169
209
  /**
170
210
  * Whether a bare `diff` written at `scope` reaches microdiff's function.
171
211
  * Resolving through the scope chain catches both failure modes: a module-scope
@@ -221,21 +261,86 @@ exports.enforceMicrodiff = (0, createRule_1.createRule)({
221
261
  return canEmitDiff(ASTHelpers_1.ASTHelpers.getScope(context, node), collectClaimableSpecifiers(sourceCode.ast));
222
262
  }
223
263
  /**
224
- * Whether retiring `declaration` leaves every reference it binds able to
225
- * take the name `diff`. The import rewrite lands at module scope while the
226
- * references it serves sit in nested scopes: one standing where `diff` is
227
- * shadowed keeps the old name, because its own fix declines, so rewriting
228
- * the import would strand it without a binding (TS2304).
264
+ * Whether the call handler rewrites the calls a specifier's references
265
+ * make. It fires on a specifier the import handler tracked, or on a local
266
+ * name a competing library conventionally binds; a specifier matching
267
+ * neither `applyChange` from `deep-diff`, say — keeps its call sites, so
268
+ * its references survive only if the import that binds them does.
229
269
  */
230
- function canRenameReferencesOf(declaration) {
270
+ function isRewrittenSpecifier(specifier) {
271
+ return (importedDiffSpecifiers.has(specifier) ||
272
+ DIFF_FUNCTION_NAMES.has(specifier.local.name));
273
+ }
274
+ /**
275
+ * Whether retiring `declaration` leaves every name it binds accounted for.
276
+ * The fix removes the whole declaration, so each reference it serves has to
277
+ * be one the call fixes rewrite to `diff` in the same pass — an import swap
278
+ * that strands a reference behind is the defect this guards.
279
+ *
280
+ * Two things have to hold at every reference. `diff` must reach microdiff
281
+ * there: the import rewrite lands at module scope while the references it
282
+ * serves sit in nested scopes, and one standing where `diff` is shadowed
283
+ * would resolve to the shadow. And the reference has to sit where a rewrite
284
+ * exists at all, which is the callee position of a convertible call.
285
+ *
286
+ * A specifier nothing references is vacuously safe: retiring it removes an
287
+ * import no code reads.
288
+ */
289
+ function canRetireImport(declaration) {
231
290
  const claimable = collectClaimableSpecifiers(sourceCode.ast);
232
291
  const declarationScope = ASTHelpers_1.ASTHelpers.getScope(context, declaration);
233
292
  return declaration.specifiers.every((specifier) => {
234
293
  const variable = ASTHelpers_1.ASTHelpers.findVariableInScope(declarationScope, specifier.local.name);
235
- return (!variable ||
236
- variable.references.every((reference) => canEmitDiff(reference.from, claimable)));
294
+ if (!variable) {
295
+ return true;
296
+ }
297
+ const rewritten = isRewrittenSpecifier(specifier);
298
+ return variable.references.every((reference) => canEmitDiff(reference.from, claimable) &&
299
+ rewritten &&
300
+ isRewrittenCallee(reference.identifier));
237
301
  });
238
302
  }
303
+ /**
304
+ * Whether the import fix retires `declaration` in this same pass.
305
+ */
306
+ function willRetireImport(declaration) {
307
+ return canEmitDiffAt(declaration) && canRetireImport(declaration);
308
+ }
309
+ /**
310
+ * Whether the call at `node`, whose callee `declaration` binds, may be
311
+ * rewritten to `diff(...)`. Separate from the decision to report: a call
312
+ * this rule cannot convert is still a use of a competing library, so it is
313
+ * reported and left for the author.
314
+ *
315
+ * The rename needs something to bind the `diff` it writes — an import the
316
+ * file already has, or the one that replaces `declaration` in this same
317
+ * pass. Renaming a callee whose import survives strands the rewritten call
318
+ * exactly as retiring an import whose callee survives strands that one.
319
+ */
320
+ function canRewriteCall(node, declaration) {
321
+ if (!canEmitDiffAt(node) || !hasRewritableArity(node)) {
322
+ return false;
323
+ }
324
+ if (findMicrodiffImport(sourceCode.ast)) {
325
+ return true;
326
+ }
327
+ return !!declaration && willRetireImport(declaration);
328
+ }
329
+ /**
330
+ * The fix element that puts microdiff's import at the top of the file, or
331
+ * null when the file already has one.
332
+ *
333
+ * It is its own element rather than text spliced into the replacement for
334
+ * the reported node, because the reported node is rarely at module scope: a
335
+ * comparison inside a function body, or a function behind an `export`,
336
+ * would otherwise take the import somewhere the grammar forbids it.
337
+ */
338
+ function buildMicrodiffImportFix(fixer) {
339
+ if (findMicrodiffImport(sourceCode.ast)) {
340
+ return null;
341
+ }
342
+ return fixer.insertTextBeforeRange([0, 0], `${MICRODIFF_IMPORT}\n\n`);
343
+ }
239
344
  // Add a specific set to track which import names are used
240
345
  const usedImportNames = new Set();
241
346
  // Check if a node is an object or array type
@@ -272,7 +377,7 @@ exports.enforceMicrodiff = (0, createRule_1.createRule)({
272
377
  ImportDeclaration(node) {
273
378
  const importSource = node.source.value;
274
379
  // Check for microdiff import
275
- if (importSource === MICRODIFF_MODULE) {
380
+ if (MICRODIFF_MODULES.has(importSource)) {
276
381
  return;
277
382
  }
278
383
  // Track other diffing libraries
@@ -296,9 +401,10 @@ exports.enforceMicrodiff = (0, createRule_1.createRule)({
296
401
  },
297
402
  fix(fixer) {
298
403
  // Decline rather than duplicate or shadow a `diff` this file
299
- // already binds to something else. The report stands so the
300
- // author resolves the name clash deliberately.
301
- if (!canEmitDiffAt(node) || !canRenameReferencesOf(node)) {
404
+ // already binds to something else, and rather than retire an
405
+ // import whose references no call fix rewrites. The report stands
406
+ // either way, so the author resolves it deliberately.
407
+ if (!canEmitDiffAt(node) || !canRetireImport(node)) {
302
408
  return null;
303
409
  }
304
410
  // If we already have a microdiff import, just remove this import
@@ -306,7 +412,7 @@ exports.enforceMicrodiff = (0, createRule_1.createRule)({
306
412
  return fixer.remove(node);
307
413
  }
308
414
  // Otherwise, replace with microdiff import
309
- return fixer.replaceText(node, `import { ${DIFF_NAME} } from '${MICRODIFF_MODULE}';`);
415
+ return fixer.replaceText(node, MICRODIFF_IMPORT);
310
416
  },
311
417
  });
312
418
  // Check if importing a diff function or a known equality library
@@ -346,6 +452,9 @@ exports.enforceMicrodiff = (0, createRule_1.createRule)({
346
452
  // Check if this call resolves to a function we specifically
347
453
  // imported from a diff library
348
454
  const importSource = toImportedDiffSource(calleeVariable, importedDiffSpecifiers);
455
+ // The declaration the rename depends on: retiring it is what frees
456
+ // the name `diff` and imports something under it.
457
+ const competingImport = toCompetingDiffImport(calleeVariable);
349
458
  if (importSource) {
350
459
  usedImportNames.add(name);
351
460
  // Skip reporting if it's from fast-deep-equal
@@ -358,7 +467,7 @@ exports.enforceMicrodiff = (0, createRule_1.createRule)({
358
467
  node,
359
468
  messageId: 'enforceMicrodiff',
360
469
  fix(fixer) {
361
- if (!canEmitDiffAt(node)) {
470
+ if (!canRewriteCall(node, competingImport)) {
362
471
  return null;
363
472
  }
364
473
  return fixer.replaceText(callee, DIFF_NAME);
@@ -370,29 +479,28 @@ exports.enforceMicrodiff = (0, createRule_1.createRule)({
370
479
  // The name is only a candidate until the scope chain says what it
371
480
  // binds: a local function, variable, parameter, or an import from
372
481
  // anywhere but a competing diff library keeps its call untouched.
373
- const competingImport = toCompetingDiffImport(calleeVariable);
374
482
  if (!competingImport) {
375
483
  return;
376
484
  }
377
485
  // Track this import name as used
378
486
  usedImportNames.add(name);
379
- // Check if we have at least 2 arguments that are objects or arrays
380
- if (node.arguments.length >= 2 &&
381
- isObjectOrArrayType(node.arguments[0]) &&
382
- isObjectOrArrayType(node.arguments[1])) {
383
- reportedNodes.add(node);
384
- context.report({
385
- node,
386
- messageId: 'enforceMicrodiff',
387
- fix(fixer) {
388
- if (!canEmitDiffAt(node)) {
389
- return null;
390
- }
391
- // When handling fast-diff and similar libraries, need to ensure the function name is replaced
392
- return fixer.replaceText(callee, DIFF_NAME);
393
- },
394
- });
395
- }
487
+ // What the callee resolves to already settles this: the argument
488
+ // shapes say nothing a competing library's own import has not.
489
+ // Gating the report on them left the call behind while the import
490
+ // handler retired the declaration binding it, so `deepDiff(a, b)`
491
+ // came out of a fix unresolved.
492
+ reportedNodes.add(node);
493
+ context.report({
494
+ node,
495
+ messageId: 'enforceMicrodiff',
496
+ fix(fixer) {
497
+ if (!canRewriteCall(node, competingImport)) {
498
+ return null;
499
+ }
500
+ // When handling fast-diff and similar libraries, need to ensure the function name is replaced
501
+ return fixer.replaceText(callee, DIFF_NAME);
502
+ },
503
+ });
396
504
  }
397
505
  }
398
506
  }
@@ -461,28 +569,13 @@ exports.enforceMicrodiff = (0, createRule_1.createRule)({
461
569
  if (!canEmitDiffAt(node)) {
462
570
  return null;
463
571
  }
464
- // Find the containing function to add the import
465
- let functionNode = node;
466
- while (functionNode &&
467
- functionNode.type !== utils_1.AST_NODE_TYPES.FunctionDeclaration &&
468
- functionNode.type !==
469
- utils_1.AST_NODE_TYPES.ArrowFunctionExpression &&
470
- functionNode.type !== utils_1.AST_NODE_TYPES.Program) {
471
- functionNode = functionNode.parent;
472
- }
473
- // If we found a program node and microdiff isn't imported,
474
- // we'll need to add the import manually
475
- if (functionNode &&
476
- functionNode.type === utils_1.AST_NODE_TYPES.Program &&
477
- !findMicrodiffImport(sourceCode.ast)) {
478
- // Need to add an import
479
- const importFix = fixer.insertTextBeforeRange([0, 0], `import { ${DIFF_NAME} } from '${MICRODIFF_MODULE}';\n\n`);
480
- // Replace JSON.stringify comparison
481
- const compareFix = fixer.replaceText(node, `${DIFF_NAME}(${sourceCode.getText(leftArg)}, ${sourceCode.getText(rightArg)})${isEqual ? '.length === 0' : '.length > 0'}`);
482
- return [importFix, compareFix];
483
- }
484
- // Otherwise just replace the comparison
485
- return fixer.replaceText(node, `${DIFF_NAME}(${sourceCode.getText(leftArg)}, ${sourceCode.getText(rightArg)})${isEqual ? '.length === 0' : '.length > 0'}`);
572
+ const compareFix = fixer.replaceText(node, `${DIFF_NAME}(${sourceCode.getText(leftArg)}, ${sourceCode.getText(rightArg)})${isEqual ? '.length === 0' : '.length > 0'}`);
573
+ // The comparison this rewrites almost always sits inside a
574
+ // function, and the `diff` it emits needs an import whatever
575
+ // encloses it. Deciding on the enclosing node left every
576
+ // nested comparison calling a `diff` nothing bound.
577
+ const importFix = buildMicrodiffImportFix(fixer);
578
+ return importFix ? [importFix, compareFix] : compareFix;
486
579
  },
487
580
  });
488
581
  }
@@ -518,27 +611,31 @@ exports.enforceMicrodiff = (0, createRule_1.createRule)({
518
611
  bodyText.includes('JSON.stringify') &&
519
612
  bodyText.includes('!==')) {
520
613
  reportedNodes.add(node);
521
- const param1 = sourceCode.getText(node.params[0]);
522
- const param2 = sourceCode.getText(node.params[1]);
614
+ // The operands are the parameter *names*, not the text of the
615
+ // parameters: a typed parameter's text carries its annotation,
616
+ // and `diff(oldConfig: Config, ...)` does not parse. A parameter
617
+ // that binds no single name — a destructuring or rest pattern —
618
+ // has no operand to pass, so the report stands without a fix.
619
+ const [firstParam, secondParam] = node.params;
620
+ const operands = firstParam.type === utils_1.AST_NODE_TYPES.Identifier &&
621
+ secondParam.type === utils_1.AST_NODE_TYPES.Identifier
622
+ ? [firstParam.name, secondParam.name]
623
+ : null;
523
624
  context.report({
524
625
  node,
525
626
  messageId: 'enforceMicrodiff',
526
627
  fix(fixer) {
527
- if (!canEmitDiffAt(node)) {
628
+ if (!operands || !canEmitDiffAt(node)) {
528
629
  return null;
529
630
  }
530
- // Create a new version of the function with microdiff
531
- const newFunctionBody = `{
532
- return ${DIFF_NAME}(${param1}, ${param2}).length > 0;
533
- }`;
534
- if (!findMicrodiffImport(sourceCode.ast)) {
535
- // Create a new import statement
536
- return fixer.replaceText(node, `import { ${DIFF_NAME} } from '${MICRODIFF_MODULE}';\n\nfunction ${node.id?.name}(${param1}, ${param2}) ${newFunctionBody}`);
537
- }
538
- else {
539
- // Just replace the function body
540
- return fixer.replaceText(body, newFunctionBody);
541
- }
631
+ // Only the body is rewritten, so the signature keeps its type
632
+ // annotations, its modifiers, and any `export` in front of
633
+ // it. Replacing the declaration wholesale used to drop those
634
+ // and, when it prefixed the import, put an `import` inside
635
+ // whatever enclosed the function.
636
+ const bodyFix = fixer.replaceText(body, `{\n return ${DIFF_NAME}(${operands[0]}, ${operands[1]}).length > 0;\n}`);
637
+ const importFix = buildMicrodiffImportFix(fixer);
638
+ return importFix ? [importFix, bodyFix] : bodyFix;
542
639
  },
543
640
  });
544
641
  return;
@@ -7,6 +7,27 @@ const FIRESTORE_MODULES = new Set([
7
7
  'firebase-admin/firestore',
8
8
  'firebase/firestore',
9
9
  ]);
10
+ /**
11
+ * Members whose meaning survives the `new Date()` → `Timestamp.now()` rewrite,
12
+ * taken from the shipped typings rather than from `Date`'s surface:
13
+ * `@google-cloud/firestore` (what `firebase-admin/firestore` re-exports)
14
+ * declares `seconds`, `nanoseconds`, `toDate()`, `toMillis()`, `isEqual()` and
15
+ * `valueOf()`, and `@firebase/firestore` adds `toString()` and `toJSON()`.
16
+ *
17
+ * `valueOf`, `toString` and `toJSON` are excluded on purpose even though
18
+ * `Timestamp` declares them, because their contracts differ from `Date`'s:
19
+ * `Timestamp#valueOf()` returns an encoded `string` where `Date#valueOf()`
20
+ * returns a `number`, and the string forms render `Timestamp(seconds=…,
21
+ * nanoseconds=…)` instead of a date. Those call sites keep compiling while the
22
+ * value silently changes, which is harder to catch than a type error.
23
+ */
24
+ const TIMESTAMP_COMPATIBLE_MEMBERS = new Set([
25
+ 'toDate',
26
+ 'toMillis',
27
+ 'isEqual',
28
+ 'seconds',
29
+ 'nanoseconds',
30
+ ]);
10
31
  exports.enforceTimestampNow = (0, createRule_1.createRule)({
11
32
  name: 'enforce-timestamp-now',
12
33
  meta: {
@@ -58,14 +79,27 @@ exports.enforceTimestampNow = (0, createRule_1.createRule)({
58
79
  importedTimestampAliases.push(localName);
59
80
  }
60
81
  }
61
- /** Local names a static Firestore import binds to `Timestamp`. */
82
+ /**
83
+ * Local names a static Firestore import binds to `Timestamp` as a *value*.
84
+ *
85
+ * A type-only binding is erased at emit, so referencing it from the
86
+ * synthesized `Timestamp.now()` turns compiling code into TS1361 (issue
87
+ * #1530). This is the same shape as #1521: the gate has to prove not just
88
+ * that a binding exists but that it is the kind of binding the emitted code
89
+ * requires. Both spellings are type-only and neither implies the other —
90
+ * `import type { Timestamp }` marks the declaration while leaving its
91
+ * specifier `value`, and `import { type Timestamp }` marks the specifier
92
+ * while leaving the declaration `value`.
93
+ */
62
94
  function staticTimestampAliases(node) {
63
95
  if (typeof node.source.value !== 'string' ||
64
- !FIRESTORE_MODULES.has(node.source.value)) {
96
+ !FIRESTORE_MODULES.has(node.source.value) ||
97
+ node.importKind === 'type') {
65
98
  return [];
66
99
  }
67
100
  return node.specifiers
68
101
  .filter((specifier) => specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
102
+ specifier.importKind !== 'type' &&
69
103
  specifier.imported.type === utils_1.AST_NODE_TYPES.Identifier &&
70
104
  specifier.imported.name === 'Timestamp')
71
105
  .map((specifier) => specifier.local.name);
@@ -73,6 +107,12 @@ exports.enforceTimestampNow = (0, createRule_1.createRule)({
73
107
  /**
74
108
  * Local names a `const { Timestamp } = await import(...)` declarator binds
75
109
  * to `Timestamp`.
110
+ *
111
+ * No type-only screen is needed here the way the static path needs one
112
+ * (#1530): a destructured `await import(...)` is a runtime value binding by
113
+ * construction. TypeScript spells a type-only dynamic import as the
114
+ * `import('...').Timestamp` type node, which never appears as a variable
115
+ * initializer, and `ImportExpression` carries no `importKind` to check.
76
116
  */
77
117
  function dynamicTimestampAliases(node) {
78
118
  if (node.init?.type !== utils_1.AST_NODE_TYPES.AwaitExpression ||
@@ -193,27 +233,61 @@ exports.enforceTimestampNow = (0, createRule_1.createRule)({
193
233
  node.callee.name === 'Date' &&
194
234
  node.arguments.length === 0);
195
235
  }
196
- // Check if a Date object is being modified (e.g., futureDate.setDate())
197
- function isDateBeingModified(dateVar) {
198
- // Look through the scope to find if this variable is modified
199
- const scope = context.getScope();
200
- const variable = scope.variables.find((v) => v.name === dateVar);
201
- if (!variable)
236
+ /**
237
+ * Whether a single use of the rewritten binding would still type-check and
238
+ * mean the same thing once its initializer is a `Timestamp`.
239
+ *
240
+ * Only a read of a member that `Timestamp` shares with `Date` qualifies.
241
+ * Every other shape — a write, an argument, a return, a comparison, an
242
+ * interpolation, a computed access — hands the value to a position whose
243
+ * expected type this rule cannot see, so it cannot be shown safe.
244
+ */
245
+ function isTimestampCompatibleReference(reference) {
246
+ // The declaration's own initializer write is the site being rewritten,
247
+ // not a use of the resulting value.
248
+ if (reference.init) {
249
+ return true;
250
+ }
251
+ // A later assignment rebinds the variable to a value typed elsewhere,
252
+ // which the rewritten initializer no longer matches.
253
+ if (!reference.isReadOnly()) {
202
254
  return false;
203
- // Check if any references to this variable are followed by property access and modification
204
- return variable.references.some((ref) => {
205
- const id = ref.identifier;
206
- const parent = id.parent;
207
- // Check for patterns like dateVar.setDate(), dateVar.setHours(), etc.
208
- return (parent &&
209
- parent.type === utils_1.AST_NODE_TYPES.MemberExpression &&
210
- parent.object === id &&
211
- parent.property.type === utils_1.AST_NODE_TYPES.Identifier &&
212
- (parent.property.name.startsWith('set') ||
213
- parent.property.name === 'toISOString' ||
214
- parent.property.name === 'toLocaleString' ||
215
- parent.property.name === 'toString'));
216
- });
255
+ }
256
+ const identifier = reference.identifier;
257
+ const parent = identifier.parent;
258
+ if (!parent ||
259
+ parent.type !== utils_1.AST_NODE_TYPES.MemberExpression ||
260
+ parent.object !== identifier ||
261
+ parent.computed ||
262
+ parent.property.type !== utils_1.AST_NODE_TYPES.Identifier) {
263
+ return false;
264
+ }
265
+ return TIMESTAMP_COMPATIBLE_MEMBERS.has(parent.property.name);
266
+ }
267
+ /**
268
+ * Whether every use of a `new Date()` binding survives the rewrite to
269
+ * `Timestamp.now()`.
270
+ *
271
+ * `Timestamp` shares almost none of `Date`'s surface, so rewriting the
272
+ * initializer turns each `getX`/`setX`/`toLocaleX` call on the binding into
273
+ * TS2339 (issue #1528). A denylist of the `Date` members `Timestamp` lacks
274
+ * can only ever be incomplete, so the question is inverted: the fix is
275
+ * offered only when the whole use set is provably part of the `Timestamp`
276
+ * API. Declining on an unrecognized use trades a missed rewrite for never
277
+ * breaking the build, which is the trade this repo prefers.
278
+ */
279
+ function usesOnlyTimestampCompatibleMembers(declarator) {
280
+ // An exported binding is read by files this rule never sees, so its use
281
+ // set cannot be enumerated and the rewrite would break importers instead.
282
+ if (declarator.parent?.parent?.type ===
283
+ utils_1.AST_NODE_TYPES.ExportNamedDeclaration) {
284
+ return false;
285
+ }
286
+ const [variable] = context.getDeclaredVariables(declarator);
287
+ if (!variable) {
288
+ return false;
289
+ }
290
+ return variable.references.every(isTimestampCompatibleReference);
217
291
  }
218
292
  return {
219
293
  Program(node) {
@@ -288,9 +362,9 @@ exports.enforceTimestampNow = (0, createRule_1.createRule)({
288
362
  varName.includes('date') ||
289
363
  varName.includes('created') ||
290
364
  varName.includes('updated')) {
291
- // Check if the Date object is being modified
292
- if (isDateBeingModified(parent.id.name)) {
293
- // If the Date is being modified, don't flag it
365
+ // Stay silent when any use of the binding relies on the `Date`
366
+ // API, since the rewrite would strip it (issue #1528).
367
+ if (!usesOnlyTimestampCompatibleMembers(parent)) {
294
368
  return;
295
369
  }
296
370
  // Stay silent unless a real `Timestamp` binding is in scope. The
@@ -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) => {
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.noHandlerSuffix = void 0;
4
4
  const utils_1 = require("@typescript-eslint/utils");
5
5
  const minimatch_1 = require("minimatch");
6
+ const compilePatternOption_1 = require("../utils/compilePatternOption");
6
7
  const createRule_1 = require("../utils/createRule");
7
8
  const DEFAULT_OPTIONS = {
8
9
  ignoreClassMethods: false,
@@ -26,6 +27,12 @@ function isUnsafeAllowPattern(pattern) {
26
27
  const nestedQuantifierPattern = /\((?:[^()\\]|\\.)*[+*{][^)]*\)\s*[+*{]/;
27
28
  return nestedQuantifierPattern.test(pattern);
28
29
  }
30
+ // A pattern that compiles can still hang the linter, so allowlist sources are
31
+ // refused for catastrophic-backtracking risk as well as for syntax.
32
+ const UNSAFE_ALLOW_PATTERN_REJECTION = {
33
+ isRejected: isUnsafeAllowPattern,
34
+ describe: (optionName) => `unsafe ${optionName} (avoid nested quantifiers that risk catastrophic backtracking)`,
35
+ };
29
36
  function getStaticKeyName(key) {
30
37
  if (key.type === utils_1.AST_NODE_TYPES.Identifier) {
31
38
  return key.name;
@@ -131,34 +138,7 @@ exports.noHandlerSuffix = (0, createRule_1.createRule)({
131
138
  const resolvedOptions = { ...DEFAULT_OPTIONS, ...(options ?? {}) };
132
139
  const allowNames = new Set(resolvedOptions.allowNames);
133
140
  const interfaceAllowlist = new Set(resolvedOptions.interfaceAllowlist);
134
- const invalidAllowPatterns = [];
135
- const unsafeAllowPatterns = [];
136
- const allowPatterns = (resolvedOptions.allowPatterns ?? []).flatMap((pattern) => {
137
- try {
138
- if (isUnsafeAllowPattern(pattern)) {
139
- unsafeAllowPatterns.push(pattern);
140
- return [];
141
- }
142
- return [new RegExp(pattern)];
143
- }
144
- catch (error) {
145
- const reason = error && typeof error === 'object' && 'message' in error
146
- ? ` (${String(error.message)})`
147
- : '';
148
- invalidAllowPatterns.push(`${pattern}${reason}`);
149
- return [];
150
- }
151
- });
152
- if (invalidAllowPatterns.length > 0 || unsafeAllowPatterns.length > 0) {
153
- const errorParts = [];
154
- if (invalidAllowPatterns.length > 0) {
155
- errorParts.push(`invalid allowPatterns: ${invalidAllowPatterns.join(', ')}`);
156
- }
157
- if (unsafeAllowPatterns.length > 0) {
158
- errorParts.push(`unsafe allowPatterns (avoid nested quantifiers that risk catastrophic backtracking): ${unsafeAllowPatterns.join(', ')}`);
159
- }
160
- throw new Error(`no-handler-suffix: ${errorParts.join('; ')}`);
161
- }
141
+ const allowPatterns = (0, compilePatternOption_1.compilePatternOption)('no-handler-suffix', 'allowPatterns', resolvedOptions.allowPatterns ?? [], undefined, UNSAFE_ALLOW_PATTERN_REJECTION);
162
142
  if (isInAllowedFile(filename, resolvedOptions.allowFilePatterns ?? [])) {
163
143
  return {};
164
144
  }
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.noRenderFunctionComponents = void 0;
4
4
  const utils_1 = require("@typescript-eslint/utils");
5
+ const compilePatternOption_1 = require("../utils/compilePatternOption");
5
6
  const createRule_1 = require("../utils/createRule");
6
7
  const ASTHelpers_1 = require("../utils/ASTHelpers");
7
8
  /**
@@ -95,7 +96,10 @@ exports.noRenderFunctionComponents = (0, createRule_1.createRule)({
95
96
  ...DEFAULT_RENDER_PROP_NAMES,
96
97
  ...userRenderPropNames,
97
98
  ]);
98
- const allowNamePatterns = (options?.allowNames ?? []).map((pattern) => new RegExp(pattern));
99
+ // Rejecting a malformed `allowNames` entry rather than dropping it keeps the
100
+ // consumer's allowlist honest: a silently discarded pattern would report the
101
+ // functions they deliberately exempted with no indication why.
102
+ const allowNamePatterns = (0, compilePatternOption_1.compilePatternOption)('no-render-function-components', 'allowNames', options?.allowNames ?? []);
99
103
  const candidates = [];
100
104
  function isAllowed(name) {
101
105
  return allowNamePatterns.some((pattern) => pattern.test(name));
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.noSeparateLoadingState = void 0;
4
4
  const utils_1 = require("@typescript-eslint/utils");
5
+ const compilePatternOption_1 = require("../utils/compilePatternOption");
5
6
  const createRule_1 = require("../utils/createRule");
6
7
  const LOADING_PATTERNS = [
7
8
  /^is.*Loading$/i,
@@ -34,7 +35,18 @@ exports.noSeparateLoadingState = (0, createRule_1.createRule)({
34
35
  },
35
36
  defaultOptions: [{}],
36
37
  create(context, [options]) {
37
- const effectivePatterns = options?.patterns?.map((p) => new RegExp(p, 'i')) ?? LOADING_PATTERNS;
38
+ // Rejecting a malformed `patterns` entry rather than falling back to
39
+ // `LOADING_PATTERNS` keeps the consumer's detection list honest: a silent
40
+ // fallback would look configured while leaving the names they meant to flag
41
+ // unreported.
42
+ //
43
+ // The `undefined` check is load-bearing: an absent `patterns` falls back to
44
+ // the built-ins, while an explicit empty list stays empty, so the option can
45
+ // disable name matching entirely.
46
+ const configuredPatterns = options?.patterns === undefined
47
+ ? undefined
48
+ : (0, compilePatternOption_1.compilePatternOption)('no-separate-loading-state', 'patterns', options.patterns, 'i');
49
+ const effectivePatterns = configuredPatterns ?? LOADING_PATTERNS;
38
50
  const setterTrackers = [];
39
51
  function isLoadingPattern(name) {
40
52
  return effectivePatterns.some((pattern) => pattern.test(name));
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Pattern options are declared as bare `string[]` because JSON Schema cannot
3
+ * express "is a compilable regex". Schema validation therefore hands any string
4
+ * straight to `new RegExp`, and an exception raised while building a rule aborts
5
+ * the entire lint run — every file, every other rule — with an opaque
6
+ * `Error while loading rule …` naming neither the option nor the offending
7
+ * value.
8
+ *
9
+ * Rejecting the configuration is the right response: silently dropping a
10
+ * pattern would leave the consumer's allowlist inert, so the code they
11
+ * deliberately excluded would be reported anyway with no indication why. This
12
+ * helper makes the rejection actionable and uniform — every failure is
13
+ * collected and rethrown as a single error naming the rule, the option and each
14
+ * bad pattern alongside the underlying regex error.
15
+ */
16
+ export type PatternRejection = {
17
+ /**
18
+ * Refuses a pattern that compiles but is still unacceptable — a source with
19
+ * nested quantifiers, say, which risks catastrophic backtracking. Checked
20
+ * before compilation, so a refused pattern is never also reported as invalid.
21
+ */
22
+ isRejected: (pattern: string) => boolean;
23
+ /**
24
+ * Builds the clause head for refused patterns, e.g.
25
+ * `unsafe allowPatterns (avoid nested quantifiers…)`. Receives the option name
26
+ * so callers need not repeat it.
27
+ */
28
+ describe: (optionName: string) => string;
29
+ };
30
+ /**
31
+ * Compiles a user-supplied list of regex sources, throwing one actionable
32
+ * configuration error listing every pattern that failed.
33
+ *
34
+ * @param ruleName Rule id used to prefix the thrown message.
35
+ * @param optionName Option the patterns came from, named in the thrown message.
36
+ * @param patterns Regex source strings supplied by the consumer.
37
+ * @param flags Flags applied to every compiled pattern (`'i'`, say).
38
+ * @param rejection Optional extra admissibility check applied before compiling.
39
+ */
40
+ export declare function compilePatternOption(ruleName: string, optionName: string, patterns: readonly string[], flags?: string, rejection?: PatternRejection): RegExp[];
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ /**
3
+ * Pattern options are declared as bare `string[]` because JSON Schema cannot
4
+ * express "is a compilable regex". Schema validation therefore hands any string
5
+ * straight to `new RegExp`, and an exception raised while building a rule aborts
6
+ * the entire lint run — every file, every other rule — with an opaque
7
+ * `Error while loading rule …` naming neither the option nor the offending
8
+ * value.
9
+ *
10
+ * Rejecting the configuration is the right response: silently dropping a
11
+ * pattern would leave the consumer's allowlist inert, so the code they
12
+ * deliberately excluded would be reported anyway with no indication why. This
13
+ * helper makes the rejection actionable and uniform — every failure is
14
+ * collected and rethrown as a single error naming the rule, the option and each
15
+ * bad pattern alongside the underlying regex error.
16
+ */
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.compilePatternOption = void 0;
19
+ function describeError(error) {
20
+ return error && typeof error === 'object' && 'message' in error
21
+ ? ` (${String(error.message)})`
22
+ : '';
23
+ }
24
+ /**
25
+ * Compiles a user-supplied list of regex sources, throwing one actionable
26
+ * configuration error listing every pattern that failed.
27
+ *
28
+ * @param ruleName Rule id used to prefix the thrown message.
29
+ * @param optionName Option the patterns came from, named in the thrown message.
30
+ * @param patterns Regex source strings supplied by the consumer.
31
+ * @param flags Flags applied to every compiled pattern (`'i'`, say).
32
+ * @param rejection Optional extra admissibility check applied before compiling.
33
+ */
34
+ function compilePatternOption(ruleName, optionName, patterns, flags, rejection) {
35
+ const invalid = [];
36
+ const rejected = [];
37
+ const compiled = patterns.flatMap((pattern) => {
38
+ try {
39
+ if (rejection?.isRejected(pattern)) {
40
+ rejected.push(pattern);
41
+ return [];
42
+ }
43
+ return [new RegExp(pattern, flags)];
44
+ }
45
+ catch (error) {
46
+ invalid.push(`${pattern}${describeError(error)}`);
47
+ return [];
48
+ }
49
+ });
50
+ if (invalid.length === 0 && rejected.length === 0) {
51
+ return compiled;
52
+ }
53
+ const clauses = [];
54
+ if (invalid.length > 0) {
55
+ clauses.push(`invalid ${optionName}: ${invalid.join(', ')}`);
56
+ }
57
+ if (rejected.length > 0 && rejection) {
58
+ clauses.push(`${rejection.describe(optionName)}: ${rejected.join(', ')}`);
59
+ }
60
+ throw new Error(`${ruleName}: ${clauses.join('; ')}`);
61
+ }
62
+ exports.compilePatternOption = compilePatternOption;
63
+ //# sourceMappingURL=compilePatternOption.js.map
@@ -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.50",
3
+ "version": "1.20.52",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -1,4 +1,72 @@
1
1
  [
2
+ {
3
+ "version": "1.20.52",
4
+ "date": "2026-07-31T19:23:46.059Z",
5
+ "rules": [
6
+ {
7
+ "name": "enforce-m3-sentence-case",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 1534
11
+ ],
12
+ "summary": "validate ignorePatterns regexes with an actionable error (closes #1534)"
13
+ },
14
+ {
15
+ "name": "no-render-function-components",
16
+ "changeType": "fix",
17
+ "issues": [
18
+ 1536
19
+ ],
20
+ "summary": "validate allowNames regexes with an actionable error (closes #1536)"
21
+ },
22
+ {
23
+ "name": "no-separate-loading-state",
24
+ "changeType": "fix",
25
+ "issues": [
26
+ 1535
27
+ ],
28
+ "summary": "validate patterns regexes with an actionable error (closes #1535)"
29
+ }
30
+ ]
31
+ },
32
+ {
33
+ "version": "1.20.51",
34
+ "date": "2026-07-31T15:20:05.562Z",
35
+ "rules": [
36
+ {
37
+ "name": "enforce-dynamic-imports",
38
+ "changeType": "fix",
39
+ "issues": [],
40
+ "summary": "allow the scoped @blumintinc/fast-deep-equal specifier; allow the scoped @blumintinc/microdiff specifier"
41
+ },
42
+ {
43
+ "name": "enforce-microdiff",
44
+ "changeType": "fix",
45
+ "issues": [
46
+ 1531
47
+ ],
48
+ "summary": "emit a default import from @blumintinc/microdiff and stop stranding the call site (closes #1531)"
49
+ },
50
+ {
51
+ "name": "enforce-timestamp-now",
52
+ "changeType": "fix",
53
+ "issues": [
54
+ 1528,
55
+ 1530
56
+ ],
57
+ "summary": "reject type-only imports as evidence of a value binding (closes #1530); only rewrite new Date() when every use is Timestamp-compatible (closes #1528)"
58
+ },
59
+ {
60
+ "name": "fast-deep-equal-over-microdiff",
61
+ "changeType": "fix",
62
+ "issues": [
63
+ 1532,
64
+ 1533
65
+ ],
66
+ "summary": "emit and recognize @blumintinc/fast-deep-equal (closes #1533); recognize the scoped @blumintinc/microdiff fork (closes #1532)"
67
+ }
68
+ ]
69
+ },
2
70
  {
3
71
  "version": "1.20.50",
4
72
  "date": "2026-07-31T12:53:09.792Z",