@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 +1 -1
- package/lib/rules/enforce-dynamic-imports.js +3 -1
- package/lib/rules/enforce-m3-sentence-case.js +6 -1
- package/lib/rules/enforce-microdiff.js +169 -72
- package/lib/rules/enforce-timestamp-now.js +99 -25
- package/lib/rules/fast-deep-equal-over-microdiff.js +83 -38
- package/lib/rules/no-handler-suffix.js +8 -28
- package/lib/rules/no-render-function-components.js +5 -1
- package/lib/rules/no-separate-loading-state.js +13 -1
- package/lib/utils/compilePatternOption.d.ts +40 -0
- package/lib/utils/compilePatternOption.js +63 -0
- 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 +68 -0
package/lib/index.js
CHANGED
|
@@ -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',
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
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
|
|
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
|
-
|
|
236
|
-
|
|
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
|
|
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
|
|
300
|
-
//
|
|
301
|
-
|
|
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,
|
|
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 (!
|
|
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
|
-
//
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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
|
-
|
|
522
|
-
|
|
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
|
-
//
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
//
|
|
292
|
-
|
|
293
|
-
|
|
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
|
|
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) => {
|
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
package/release-manifest.json
CHANGED
|
@@ -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",
|