@blumintinc/eslint-plugin-blumint 1.20.118 → 1.20.120
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-firestore-doc-ref-generic.js +9 -30
- package/lib/rules/enforce-firestore-set-merge.js +9 -45
- package/lib/rules/enforce-microdiff.js +124 -98
- package/lib/rules/no-direct-function-state.js +9 -30
- package/lib/rules/no-explicit-return-type.js +10 -37
- package/lib/rules/no-firestore-object-arrays.js +14 -65
- package/lib/rules/prefer-batch-operations.js +24 -45
- package/lib/rules/prefer-block-comments-for-declarations.js +99 -15
- package/lib/rules/prefer-spread-over-reassembly.js +3 -34
- package/lib/rules/prefer-use-base62-id.js +31 -2
- package/lib/rules/prevent-children-clobber.js +9 -38
- package/lib/rules/require-hooks-default-params.js +16 -20
- package/lib/rules/require-props-composition.js +35 -72
- package/lib/utils/fixtureCorpus.d.ts +2 -3
- package/lib/utils/fixtureCorpus.js +54 -5
- package/lib/utils/lexicalScope.d.ts +84 -0
- package/lib/utils/lexicalScope.js +131 -0
- package/package.json +1 -1
- package/release-manifest.json +45 -0
package/lib/index.js
CHANGED
|
@@ -11,6 +11,7 @@ exports.enforceFirestoreDocRefGeneric = void 0;
|
|
|
11
11
|
const utils_1 = require("@typescript-eslint/utils");
|
|
12
12
|
const createRule_1 = require("../utils/createRule");
|
|
13
13
|
const ASTHelpers_1 = require("../utils/ASTHelpers");
|
|
14
|
+
const lexicalScope_1 = require("../utils/lexicalScope");
|
|
14
15
|
/** The Firestore reference types that carry a document-shape generic. */
|
|
15
16
|
const REFERENCE_TYPE_NAMES = new Set([
|
|
16
17
|
'DocumentReference',
|
|
@@ -38,20 +39,6 @@ const referenceTypeNameOf = (typeName) => {
|
|
|
38
39
|
}
|
|
39
40
|
return undefined;
|
|
40
41
|
};
|
|
41
|
-
/** Statement containers a type declaration can be a direct child of. */
|
|
42
|
-
function statementsOf(node) {
|
|
43
|
-
switch (node.type) {
|
|
44
|
-
case utils_1.AST_NODE_TYPES.Program:
|
|
45
|
-
case utils_1.AST_NODE_TYPES.BlockStatement:
|
|
46
|
-
case utils_1.AST_NODE_TYPES.TSModuleBlock:
|
|
47
|
-
case utils_1.AST_NODE_TYPES.StaticBlock:
|
|
48
|
-
return node.body;
|
|
49
|
-
case utils_1.AST_NODE_TYPES.SwitchCase:
|
|
50
|
-
return node.consequent;
|
|
51
|
-
default:
|
|
52
|
-
return undefined;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
42
|
/**
|
|
56
43
|
* The type declaration a statement makes, looking through `export`.
|
|
57
44
|
*
|
|
@@ -61,10 +48,7 @@ function statementsOf(node) {
|
|
|
61
48
|
* distinction the document shape knows anything about.
|
|
62
49
|
*/
|
|
63
50
|
function typeDeclarationNamed(statement, name) {
|
|
64
|
-
const declared =
|
|
65
|
-
statement.declaration
|
|
66
|
-
? statement.declaration
|
|
67
|
-
: statement;
|
|
51
|
+
const declared = (0, lexicalScope_1.declarationOf)(statement);
|
|
68
52
|
if ((declared.type === utils_1.AST_NODE_TYPES.TSInterfaceDeclaration ||
|
|
69
53
|
declared.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration) &&
|
|
70
54
|
declared.id.name === name) {
|
|
@@ -83,20 +67,15 @@ function typeDeclarationNamed(statement, name) {
|
|
|
83
67
|
* dropped the nested-`any` check rather than reporting anything.
|
|
84
68
|
*/
|
|
85
69
|
function declarationOfType(from, name) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const declaration = typeDeclarationNamed(statement, name);
|
|
92
|
-
if (declaration) {
|
|
93
|
-
return declaration;
|
|
94
|
-
}
|
|
70
|
+
return (0, lexicalScope_1.resolveInEnclosingScopes)(from, (statements) => {
|
|
71
|
+
for (const statement of statements) {
|
|
72
|
+
const declaration = typeDeclarationNamed(statement, name);
|
|
73
|
+
if (declaration) {
|
|
74
|
+
return declaration;
|
|
95
75
|
}
|
|
96
76
|
}
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
return undefined;
|
|
77
|
+
return undefined;
|
|
78
|
+
});
|
|
100
79
|
}
|
|
101
80
|
/**
|
|
102
81
|
* @type {import('eslint').Rule.RuleModule}
|
|
@@ -5,6 +5,7 @@ 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 lexicalScope_1 = require("../utils/lexicalScope");
|
|
8
9
|
const FIRESTORE_MODULES = new Set(['firebase/firestore', 'firebase-admin']);
|
|
9
10
|
const UPDATE_DOC = 'updateDoc';
|
|
10
11
|
const SET_DOC = 'setDoc';
|
|
@@ -192,20 +193,6 @@ function isPrimitiveLiteral(node) {
|
|
|
192
193
|
typeof value === 'boolean' ||
|
|
193
194
|
typeof value === 'bigint');
|
|
194
195
|
}
|
|
195
|
-
/** Statement containers a declaration can be a direct child of. */
|
|
196
|
-
function statementsOf(node) {
|
|
197
|
-
switch (node.type) {
|
|
198
|
-
case utils_1.AST_NODE_TYPES.Program:
|
|
199
|
-
case utils_1.AST_NODE_TYPES.BlockStatement:
|
|
200
|
-
case utils_1.AST_NODE_TYPES.TSModuleBlock:
|
|
201
|
-
case utils_1.AST_NODE_TYPES.StaticBlock:
|
|
202
|
-
return node.body;
|
|
203
|
-
case utils_1.AST_NODE_TYPES.SwitchCase:
|
|
204
|
-
return node.consequent;
|
|
205
|
-
default:
|
|
206
|
-
return undefined;
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
196
|
/** Whether a declarator is initialized from a `<x>.firestore()` call. */
|
|
210
197
|
function initializesFirestore(declarator) {
|
|
211
198
|
const { init } = declarator;
|
|
@@ -225,10 +212,7 @@ function initializesFirestore(declarator) {
|
|
|
225
212
|
* "find the in-file declaration that carries the evidence" purpose.
|
|
226
213
|
*/
|
|
227
214
|
function declaresFirestoreInstance(statement) {
|
|
228
|
-
const declaration =
|
|
229
|
-
statement.declaration
|
|
230
|
-
? statement.declaration
|
|
231
|
-
: statement;
|
|
215
|
+
const declaration = (0, lexicalScope_1.declarationOf)(statement);
|
|
232
216
|
return (declaration.type === utils_1.AST_NODE_TYPES.VariableDeclaration &&
|
|
233
217
|
declaration.declarations.some(initializesFirestore));
|
|
234
218
|
}
|
|
@@ -244,15 +228,9 @@ function declaresFirestoreInstance(statement) {
|
|
|
244
228
|
* falls through to the next one out instead of answering for the whole chain.
|
|
245
229
|
*/
|
|
246
230
|
function hasFirestoreInstanceInScope(node) {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
if (statements?.some(declaresFirestoreInstance)) {
|
|
251
|
-
return true;
|
|
252
|
-
}
|
|
253
|
-
current = current.parent;
|
|
254
|
-
}
|
|
255
|
-
return false;
|
|
231
|
+
return ((0, lexicalScope_1.resolveInEnclosingScopes)(node, (statements) => statements.some((statement) => declaresFirestoreInstance(statement))
|
|
232
|
+
? true
|
|
233
|
+
: undefined) === true);
|
|
256
234
|
}
|
|
257
235
|
exports.enforceFirestoreSetMerge = (0, createRule_1.createRule)({
|
|
258
236
|
name: 'enforce-firestore-set-merge',
|
|
@@ -303,16 +281,13 @@ exports.enforceFirestoreSetMerge = (0, createRule_1.createRule)({
|
|
|
303
281
|
}
|
|
304
282
|
const classes = new Map();
|
|
305
283
|
for (const statement of statements) {
|
|
306
|
-
const declaration =
|
|
307
|
-
|
|
308
|
-
? statement.declaration
|
|
309
|
-
: statement;
|
|
310
|
-
if (declaration?.type === utils_1.AST_NODE_TYPES.ClassDeclaration &&
|
|
284
|
+
const declaration = (0, lexicalScope_1.declarationOf)(statement);
|
|
285
|
+
if (declaration.type === utils_1.AST_NODE_TYPES.ClassDeclaration &&
|
|
311
286
|
declaration.id) {
|
|
312
287
|
classes.set(declaration.id.name, declaration.body);
|
|
313
288
|
continue;
|
|
314
289
|
}
|
|
315
|
-
if (declaration
|
|
290
|
+
if (declaration.type === utils_1.AST_NODE_TYPES.VariableDeclaration) {
|
|
316
291
|
for (const declarator of declaration.declarations) {
|
|
317
292
|
if (declarator.id.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
318
293
|
declarator.init?.type === utils_1.AST_NODE_TYPES.ClassExpression) {
|
|
@@ -340,18 +315,7 @@ exports.enforceFirestoreSetMerge = (0, createRule_1.createRule)({
|
|
|
340
315
|
* the shadow.
|
|
341
316
|
*/
|
|
342
317
|
function resolveClassBody(name, reference) {
|
|
343
|
-
|
|
344
|
-
while (current) {
|
|
345
|
-
const statements = statementsOf(current);
|
|
346
|
-
if (statements) {
|
|
347
|
-
const found = containerClasses(current, statements).get(name);
|
|
348
|
-
if (found) {
|
|
349
|
-
return found;
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
current = current.parent;
|
|
353
|
-
}
|
|
354
|
-
return null;
|
|
318
|
+
return ((0, lexicalScope_1.resolveInEnclosingScopes)(reference, (statements, container) => containerClasses(container, statements).get(name)) ?? null);
|
|
355
319
|
}
|
|
356
320
|
function enclosingClassBody(node) {
|
|
357
321
|
let current = node.parent;
|
|
@@ -37,6 +37,58 @@ const DIFF_FUNCTION_NAMES = new Set([
|
|
|
37
37
|
'detailedDiff',
|
|
38
38
|
// 'fastDeepEqual' and 'isEqual' stay out: they are allowed alternatives.
|
|
39
39
|
]);
|
|
40
|
+
/**
|
|
41
|
+
* The names a hand-rolled comparison function is conventionally given. One
|
|
42
|
+
* shared set answers for both spellings of such a function — a `function`
|
|
43
|
+
* declaration and an arrow bound to a `const` — so neither can drift into
|
|
44
|
+
* recognising a name the other misses.
|
|
45
|
+
*/
|
|
46
|
+
const COMPARISON_FUNCTION_NAMES = new Set([
|
|
47
|
+
'detectChanges',
|
|
48
|
+
'hasConfigChanged',
|
|
49
|
+
'compareObjects',
|
|
50
|
+
'compareArrays',
|
|
51
|
+
'findChanges',
|
|
52
|
+
'detectDifferences',
|
|
53
|
+
'hasStateChanged',
|
|
54
|
+
'stateHasUpdated',
|
|
55
|
+
'arrayHasChanged',
|
|
56
|
+
'settingsChanged',
|
|
57
|
+
]);
|
|
58
|
+
/**
|
|
59
|
+
* The comparison function whose body the fix rewrites. A name alone does not
|
|
60
|
+
* fix what such a function returns — a boolean, the changed keys, the changes
|
|
61
|
+
* themselves — and a change list swapped in for the wrong one of those
|
|
62
|
+
* compiles, so the rest of the set is reported and left to its authors.
|
|
63
|
+
*/
|
|
64
|
+
const REWRITABLE_COMPARISON_NAME = 'hasConfigChanged';
|
|
65
|
+
/**
|
|
66
|
+
* The markers that make a body look like a hand-rolled comparison. Text is
|
|
67
|
+
* enough to raise the report because the report says only that the body should
|
|
68
|
+
* be using microdiff; nothing is rewritten off these.
|
|
69
|
+
*/
|
|
70
|
+
function hasComparisonMarkers(bodyText) {
|
|
71
|
+
return (bodyText.includes('JSON.stringify') ||
|
|
72
|
+
bodyText.includes('Object.keys') ||
|
|
73
|
+
bodyText.includes('for (') ||
|
|
74
|
+
bodyText.includes('.some(') ||
|
|
75
|
+
bodyText.includes('.every('));
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Whether a comparison function is one whose body the fix attempts. The gate is
|
|
79
|
+
* a text-level pre-filter over the name and the body: `collectStringifyComparisons`
|
|
80
|
+
* reads the AST afterwards and has the last word on whether a rewrite exists,
|
|
81
|
+
* so a body that clears this gate is still routinely left alone.
|
|
82
|
+
*
|
|
83
|
+
* The `!==` marker keeps the rewrite to bodies that phrase the question the way
|
|
84
|
+
* the name does — "has it changed?" — while an all-`===` body is reported and
|
|
85
|
+
* left for its author in either spelling.
|
|
86
|
+
*/
|
|
87
|
+
function isRewritableComparison(name, bodyText) {
|
|
88
|
+
return (name === REWRITABLE_COMPARISON_NAME &&
|
|
89
|
+
bodyText.includes('JSON.stringify') &&
|
|
90
|
+
bodyText.includes('!=='));
|
|
91
|
+
}
|
|
40
92
|
/**
|
|
41
93
|
* The exports of a competing library whose call sites this rule rewrites,
|
|
42
94
|
* whatever local name the import binds them to.
|
|
@@ -457,6 +509,62 @@ exports.enforceMicrodiff = (0, createRule_1.createRule)({
|
|
|
457
509
|
const emptiness = comparison.isEqual ? '.length === 0' : '.length > 0';
|
|
458
510
|
return `${DIFF_NAME}(${left}, ${right})${emptiness}`;
|
|
459
511
|
}
|
|
512
|
+
/**
|
|
513
|
+
* The rewrite of a comparison function's body, or null when the body offers
|
|
514
|
+
* no single expression to replace or the emitted `diff` would not reach
|
|
515
|
+
* microdiff at `fn`.
|
|
516
|
+
*
|
|
517
|
+
* Exactly one comparison is the condition for a fix. With none there is no
|
|
518
|
+
* expression to rewrite, and with several the rule cannot tell which one the
|
|
519
|
+
* function's answer turns on, so the report stands on its own.
|
|
520
|
+
*
|
|
521
|
+
* Only the comparison's own range is rewritten. The signature keeps its type
|
|
522
|
+
* annotations, its modifiers and any `export` in front of it, and the body
|
|
523
|
+
* keeps everything the comparison shares it with: side effects, guard
|
|
524
|
+
* clauses, locals, and the comments around them. Re-emitting the body as a
|
|
525
|
+
* single return drops all of that silently — the fix compiles, so nothing
|
|
526
|
+
* downstream flags the loss.
|
|
527
|
+
*
|
|
528
|
+
* Replacing the comparison rather than the statement holding it is also what
|
|
529
|
+
* lets one implementation serve every spelling of the function: an arrow's
|
|
530
|
+
* concise expression body takes no `return` and no semicolon, and it needs
|
|
531
|
+
* none, because the text around the comparison is never part of the range.
|
|
532
|
+
*/
|
|
533
|
+
function buildComparisonBodyFix(fixer, fn, body) {
|
|
534
|
+
const comparisons = collectStringifyComparisons(body);
|
|
535
|
+
if (comparisons.length !== 1 || !canEmitDiffAt(fn)) {
|
|
536
|
+
return null;
|
|
537
|
+
}
|
|
538
|
+
const bodyFix = fixer.replaceText(comparisons[0].node, buildDiffComparison(comparisons[0]));
|
|
539
|
+
const importFix = buildMicrodiffImportFix(fixer);
|
|
540
|
+
return importFix ? [importFix, bodyFix] : bodyFix;
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Reports a hand-rolled comparison function, carrying the rewrite whenever
|
|
544
|
+
* its body offers one. Both spellings route through here so an identical
|
|
545
|
+
* violation is auto-remediable however it is written: a report with a fix in
|
|
546
|
+
* one spelling and without it in the other leaves the same code manual to
|
|
547
|
+
* resolve for no reason the author can see.
|
|
548
|
+
*/
|
|
549
|
+
function reportComparisonFunction(node, name, body) {
|
|
550
|
+
const bodyText = sourceCode.getText(body);
|
|
551
|
+
if (isRewritableComparison(name, bodyText)) {
|
|
552
|
+
reportedNodes.add(node);
|
|
553
|
+
context.report({
|
|
554
|
+
node,
|
|
555
|
+
messageId: 'enforceMicrodiff',
|
|
556
|
+
fix: (fixer) => buildComparisonBodyFix(fixer, node, body),
|
|
557
|
+
});
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
if (hasComparisonMarkers(bodyText)) {
|
|
561
|
+
reportedNodes.add(node);
|
|
562
|
+
context.report({
|
|
563
|
+
node,
|
|
564
|
+
messageId: 'enforceMicrodiff',
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
}
|
|
460
568
|
// Add a specific set to track which import names are used
|
|
461
569
|
const usedImportNames = new Set();
|
|
462
570
|
// Check if a node is an object or array type
|
|
@@ -692,71 +800,14 @@ exports.enforceMicrodiff = (0, createRule_1.createRule)({
|
|
|
692
800
|
if (reportedNodes.has(node)) {
|
|
693
801
|
return;
|
|
694
802
|
}
|
|
695
|
-
//
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
'compareArrays',
|
|
702
|
-
'findChanges',
|
|
703
|
-
'detectDifferences',
|
|
704
|
-
'hasStateChanged',
|
|
705
|
-
'stateHasUpdated',
|
|
706
|
-
'arrayHasChanged',
|
|
707
|
-
'settingsChanged',
|
|
708
|
-
].includes(node.id.name)) {
|
|
709
|
-
// Check if function has two parameters that might be objects/arrays
|
|
710
|
-
if (node.params.length >= 2) {
|
|
711
|
-
const body = node.body;
|
|
712
|
-
const bodyText = sourceCode.getText(body);
|
|
713
|
-
// Check if the function body contains a JSON.stringify comparison
|
|
714
|
-
if (node.id.name === 'hasConfigChanged' &&
|
|
715
|
-
bodyText.includes('JSON.stringify') &&
|
|
716
|
-
bodyText.includes('!==')) {
|
|
717
|
-
reportedNodes.add(node);
|
|
718
|
-
// Exactly one comparison is the condition for a fix. With none
|
|
719
|
-
// there is no expression to rewrite, and with several the rule
|
|
720
|
-
// cannot tell which one the function's answer turns on, so the
|
|
721
|
-
// report stands on its own.
|
|
722
|
-
const comparisons = collectStringifyComparisons(body);
|
|
723
|
-
const comparison = comparisons.length === 1 ? comparisons[0] : null;
|
|
724
|
-
context.report({
|
|
725
|
-
node,
|
|
726
|
-
messageId: 'enforceMicrodiff',
|
|
727
|
-
fix(fixer) {
|
|
728
|
-
if (!comparison || !canEmitDiffAt(node)) {
|
|
729
|
-
return null;
|
|
730
|
-
}
|
|
731
|
-
// Only the comparison's own range is rewritten. The signature
|
|
732
|
-
// keeps its type annotations, its modifiers and any `export`
|
|
733
|
-
// in front of it, and the body keeps everything the
|
|
734
|
-
// comparison shares it with: side effects, guard clauses,
|
|
735
|
-
// locals, and the comments around them. Re-emitting the body
|
|
736
|
-
// as a single return drops all of that silently — the fix
|
|
737
|
-
// compiles, so nothing downstream flags the loss.
|
|
738
|
-
const bodyFix = fixer.replaceText(comparison.node, buildDiffComparison(comparison));
|
|
739
|
-
const importFix = buildMicrodiffImportFix(fixer);
|
|
740
|
-
return importFix ? [importFix, bodyFix] : bodyFix;
|
|
741
|
-
},
|
|
742
|
-
});
|
|
743
|
-
return;
|
|
744
|
-
}
|
|
745
|
-
// Look for patterns that suggest object/array comparison
|
|
746
|
-
const hasComparisonLogic = bodyText.includes('JSON.stringify') ||
|
|
747
|
-
bodyText.includes('Object.keys') ||
|
|
748
|
-
bodyText.includes('for (') ||
|
|
749
|
-
bodyText.includes('.some(') ||
|
|
750
|
-
bodyText.includes('.every(');
|
|
751
|
-
if (hasComparisonLogic) {
|
|
752
|
-
reportedNodes.add(node);
|
|
753
|
-
context.report({
|
|
754
|
-
node,
|
|
755
|
-
messageId: 'enforceMicrodiff',
|
|
756
|
-
});
|
|
757
|
-
}
|
|
758
|
-
}
|
|
803
|
+
// Two parameters are what a comparison function needs, and what the
|
|
804
|
+
// `diff(a, b)` it is rewritten to needs as well.
|
|
805
|
+
if (!node.id ||
|
|
806
|
+
!COMPARISON_FUNCTION_NAMES.has(node.id.name) ||
|
|
807
|
+
node.params.length < 2) {
|
|
808
|
+
return;
|
|
759
809
|
}
|
|
810
|
+
reportComparisonFunction(node, node.id.name, node.body);
|
|
760
811
|
},
|
|
761
812
|
// Check for custom deep comparison in arrow functions
|
|
762
813
|
ArrowFunctionExpression(node) {
|
|
@@ -764,42 +815,17 @@ exports.enforceMicrodiff = (0, createRule_1.createRule)({
|
|
|
764
815
|
if (reportedNodes.has(node)) {
|
|
765
816
|
return;
|
|
766
817
|
}
|
|
767
|
-
//
|
|
818
|
+
// The name an arrow answers to is the one its declarator binds, so an
|
|
819
|
+
// arrow passed straight to a call names nothing and is left alone.
|
|
768
820
|
const parent = node.parent;
|
|
769
|
-
if (parent
|
|
770
|
-
parent.type
|
|
771
|
-
parent.id.type
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
'compareObjects',
|
|
776
|
-
'compareArrays',
|
|
777
|
-
'findChanges',
|
|
778
|
-
'detectDifferences',
|
|
779
|
-
'hasStateChanged',
|
|
780
|
-
'stateHasUpdated',
|
|
781
|
-
'arrayHasChanged',
|
|
782
|
-
'settingsChanged',
|
|
783
|
-
].includes(parent.id.name)) {
|
|
784
|
-
// Check if function has two parameters that might be objects/arrays
|
|
785
|
-
if (node.params.length >= 2) {
|
|
786
|
-
const body = node.body;
|
|
787
|
-
// Look for patterns that suggest object/array comparison
|
|
788
|
-
const bodyText = sourceCode.getText(body);
|
|
789
|
-
const hasComparisonLogic = bodyText.includes('JSON.stringify') ||
|
|
790
|
-
bodyText.includes('Object.keys') ||
|
|
791
|
-
bodyText.includes('for (') ||
|
|
792
|
-
bodyText.includes('.some(') ||
|
|
793
|
-
bodyText.includes('.every(');
|
|
794
|
-
if (hasComparisonLogic) {
|
|
795
|
-
reportedNodes.add(node);
|
|
796
|
-
context.report({
|
|
797
|
-
node,
|
|
798
|
-
messageId: 'enforceMicrodiff',
|
|
799
|
-
});
|
|
800
|
-
}
|
|
801
|
-
}
|
|
821
|
+
if (!parent ||
|
|
822
|
+
parent.type !== utils_1.AST_NODE_TYPES.VariableDeclarator ||
|
|
823
|
+
parent.id.type !== utils_1.AST_NODE_TYPES.Identifier ||
|
|
824
|
+
!COMPARISON_FUNCTION_NAMES.has(parent.id.name) ||
|
|
825
|
+
node.params.length < 2) {
|
|
826
|
+
return;
|
|
802
827
|
}
|
|
828
|
+
reportComparisonFunction(node, parent.id.name, node.body);
|
|
803
829
|
},
|
|
804
830
|
};
|
|
805
831
|
},
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.noDirectFunctionState = void 0;
|
|
4
4
|
const utils_1 = require("@typescript-eslint/utils");
|
|
5
5
|
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
const lexicalScope_1 = require("../utils/lexicalScope");
|
|
6
7
|
const DEFAULT_FUNCTION_PATTERNS = [
|
|
7
8
|
'callback',
|
|
8
9
|
'handler',
|
|
@@ -10,20 +11,6 @@ const DEFAULT_FUNCTION_PATTERNS = [
|
|
|
10
11
|
'func',
|
|
11
12
|
'on[A-Z].*',
|
|
12
13
|
];
|
|
13
|
-
/** Statement containers a type alias can be a direct child of. */
|
|
14
|
-
function statementsOf(node) {
|
|
15
|
-
switch (node.type) {
|
|
16
|
-
case utils_1.AST_NODE_TYPES.Program:
|
|
17
|
-
case utils_1.AST_NODE_TYPES.BlockStatement:
|
|
18
|
-
case utils_1.AST_NODE_TYPES.TSModuleBlock:
|
|
19
|
-
case utils_1.AST_NODE_TYPES.StaticBlock:
|
|
20
|
-
return node.body;
|
|
21
|
-
case utils_1.AST_NODE_TYPES.SwitchCase:
|
|
22
|
-
return node.consequent;
|
|
23
|
-
default:
|
|
24
|
-
return undefined;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
14
|
/**
|
|
28
15
|
* The type alias a statement declares under `name`, looking through `export`.
|
|
29
16
|
*
|
|
@@ -33,10 +20,7 @@ function statementsOf(node) {
|
|
|
33
20
|
* readable, which says nothing about what the state holds.
|
|
34
21
|
*/
|
|
35
22
|
function typeAliasNamed(statement, name) {
|
|
36
|
-
const declaration =
|
|
37
|
-
statement.declaration
|
|
38
|
-
? statement.declaration
|
|
39
|
-
: statement;
|
|
23
|
+
const declaration = (0, lexicalScope_1.declarationOf)(statement);
|
|
40
24
|
return declaration.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration &&
|
|
41
25
|
declaration.id.name === name
|
|
42
26
|
? declaration
|
|
@@ -56,20 +40,15 @@ function typeAliasNamed(statement, name) {
|
|
|
56
40
|
* hoisting of type declarations.
|
|
57
41
|
*/
|
|
58
42
|
function resolveTypeAlias(from, name) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const alias = typeAliasNamed(statement, name);
|
|
65
|
-
if (alias) {
|
|
66
|
-
return alias;
|
|
67
|
-
}
|
|
43
|
+
return (0, lexicalScope_1.resolveInEnclosingScopes)(from, (statements) => {
|
|
44
|
+
for (const statement of statements) {
|
|
45
|
+
const alias = typeAliasNamed(statement, name);
|
|
46
|
+
if (alias) {
|
|
47
|
+
return alias;
|
|
68
48
|
}
|
|
69
49
|
}
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
return undefined;
|
|
50
|
+
return undefined;
|
|
51
|
+
});
|
|
73
52
|
}
|
|
74
53
|
/**
|
|
75
54
|
* Returns true if the TSTypeAnnotation node (from useState's type parameter)
|
|
@@ -5,6 +5,7 @@ const utils_1 = require("@typescript-eslint/utils");
|
|
|
5
5
|
const createRule_1 = require("../utils/createRule");
|
|
6
6
|
const disableDirectives_1 = require("../utils/disableDirectives");
|
|
7
7
|
const importRemoval_1 = require("../utils/importRemoval");
|
|
8
|
+
const lexicalScope_1 = require("../utils/lexicalScope");
|
|
8
9
|
const defaultOptions = {
|
|
9
10
|
allowRecursiveFunctions: true,
|
|
10
11
|
allowOverloadedFunctions: true,
|
|
@@ -401,24 +402,6 @@ function collectReturnIdentifierNames(fn, visitorKeys) {
|
|
|
401
402
|
}
|
|
402
403
|
return names;
|
|
403
404
|
}
|
|
404
|
-
/**
|
|
405
|
-
* The statement list a container holds, or undefined for a node that holds
|
|
406
|
-
* none. These are every place a function can be declared as a direct child, so
|
|
407
|
-
* walking them outward reproduces TypeScript's own scope chain.
|
|
408
|
-
*/
|
|
409
|
-
function statementsOf(node) {
|
|
410
|
-
switch (node.type) {
|
|
411
|
-
case utils_1.AST_NODE_TYPES.Program:
|
|
412
|
-
case utils_1.AST_NODE_TYPES.BlockStatement:
|
|
413
|
-
case utils_1.AST_NODE_TYPES.TSModuleBlock:
|
|
414
|
-
case utils_1.AST_NODE_TYPES.StaticBlock:
|
|
415
|
-
return node.body;
|
|
416
|
-
case utils_1.AST_NODE_TYPES.SwitchCase:
|
|
417
|
-
return node.consequent;
|
|
418
|
-
default:
|
|
419
|
-
return undefined;
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
405
|
/**
|
|
423
406
|
* What a statement list binds each of its names to: the function when the name
|
|
424
407
|
* denotes one, `null` when it denotes anything else.
|
|
@@ -437,11 +420,7 @@ function scopeBindings(statements) {
|
|
|
437
420
|
for (const rawStatement of statements) {
|
|
438
421
|
// `export function f() {}` is the same declaration one AST node deeper, and
|
|
439
422
|
// the `export` keyword alone cannot decide whether a name is resolvable.
|
|
440
|
-
const statement = (
|
|
441
|
-
rawStatement.type === utils_1.AST_NODE_TYPES.ExportDefaultDeclaration) &&
|
|
442
|
-
rawStatement.declaration
|
|
443
|
-
? rawStatement.declaration
|
|
444
|
-
: rawStatement;
|
|
423
|
+
const statement = (0, lexicalScope_1.declarationOf)(rawStatement);
|
|
445
424
|
if (statement.type === utils_1.AST_NODE_TYPES.FunctionDeclaration && statement.id) {
|
|
446
425
|
bind(statement.id.name, statement.body ? statement : null);
|
|
447
426
|
continue;
|
|
@@ -510,20 +489,14 @@ function createReturnCycleResolver(visitorKeys) {
|
|
|
510
489
|
bindings.set(container, computed);
|
|
511
490
|
return computed;
|
|
512
491
|
};
|
|
513
|
-
const resolveFunctionInScope = (from, name) => {
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
current = current.parent;
|
|
524
|
-
}
|
|
525
|
-
return undefined;
|
|
526
|
-
};
|
|
492
|
+
const resolveFunctionInScope = (from, name) => (0, lexicalScope_1.resolveInEnclosingScopes)(from, (statements, container) => {
|
|
493
|
+
const scope = bindingsOf(container, statements);
|
|
494
|
+
if (!scope.has(name))
|
|
495
|
+
return undefined;
|
|
496
|
+
// A non-function binding still shadows a same-named outer function, so
|
|
497
|
+
// it ends the search without yielding one.
|
|
498
|
+
return scope.get(name) ?? lexicalScope_1.BOUND_UNPROVABLE;
|
|
499
|
+
});
|
|
527
500
|
const edgesOf = (fn) => {
|
|
528
501
|
const cached = edges.get(fn);
|
|
529
502
|
if (cached)
|