@blumintinc/eslint-plugin-blumint 1.20.115 → 1.20.117
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 +129 -37
- package/lib/rules/enforce-firestore-set-merge.js +65 -19
- package/lib/rules/enforce-render-hits-memoization.js +77 -20
- package/lib/rules/enforce-transform-memoization.js +72 -1
- package/lib/rules/extract-global-constants.js +9 -4
- package/lib/rules/logical-top-to-bottom-grouping.js +85 -46
- package/lib/rules/no-curly-brackets-around-commented-properties.js +32 -16
- package/lib/rules/no-direct-function-state.js +76 -40
- package/lib/rules/no-explicit-return-type.js +145 -51
- package/lib/rules/no-firestore-object-arrays.js +190 -48
- package/lib/rules/no-inline-component-prop.js +44 -18
- package/lib/rules/prefer-batch-operations.js +49 -10
- package/lib/rules/prefer-spread-over-reassembly.js +82 -18
- package/lib/rules/prevent-children-clobber.js +104 -32
- package/lib/rules/require-hooks-default-params.js +58 -131
- package/lib/rules/require-server-timestamp-for-firestore-dates.js +16 -12
- package/lib/utils/ASTHelpers.js +77 -0
- package/package.json +1 -1
- package/release-manifest.json +149 -0
package/lib/index.js
CHANGED
|
@@ -11,6 +11,93 @@ 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
|
+
/** The Firestore reference types that carry a document-shape generic. */
|
|
15
|
+
const REFERENCE_TYPE_NAMES = new Set([
|
|
16
|
+
'DocumentReference',
|
|
17
|
+
'CollectionReference',
|
|
18
|
+
'CollectionGroup',
|
|
19
|
+
]);
|
|
20
|
+
/**
|
|
21
|
+
* The final segment of a type reference's name, so `FirebaseFirestore.
|
|
22
|
+
* DocumentReference` is recognized as the same type as `DocumentReference`.
|
|
23
|
+
*
|
|
24
|
+
* The rightmost segment is the right granularity because the namespace is
|
|
25
|
+
* arbitrary — `FirebaseFirestore.`, `admin.firestore.` and any
|
|
26
|
+
* `import * as fs from 'firebase-admin/firestore'` alias all name these types —
|
|
27
|
+
* while the names themselves are specific enough that an unrelated module's
|
|
28
|
+
* `DocumentReference` is not a realistic collision.
|
|
29
|
+
*/
|
|
30
|
+
const referenceTypeNameOf = (typeName) => {
|
|
31
|
+
if (typeName.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
32
|
+
return REFERENCE_TYPE_NAMES.has(typeName.name) ? typeName.name : undefined;
|
|
33
|
+
}
|
|
34
|
+
if (typeName.type === utils_1.AST_NODE_TYPES.TSQualifiedName &&
|
|
35
|
+
typeName.right.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
36
|
+
REFERENCE_TYPE_NAMES.has(typeName.right.name)) {
|
|
37
|
+
return typeName.right.name;
|
|
38
|
+
}
|
|
39
|
+
return undefined;
|
|
40
|
+
};
|
|
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
|
+
/**
|
|
56
|
+
* The type declaration a statement makes, looking through `export`.
|
|
57
|
+
*
|
|
58
|
+
* `export type User = ...` is the same declaration one AST node deeper, inside
|
|
59
|
+
* an `ExportNamedDeclaration`. Reading the statement without unwrapping makes
|
|
60
|
+
* the `export` keyword alone decide whether a schema is checked, which is not a
|
|
61
|
+
* distinction the document shape knows anything about.
|
|
62
|
+
*/
|
|
63
|
+
function typeDeclarationNamed(statement, name) {
|
|
64
|
+
const declared = statement.type === utils_1.AST_NODE_TYPES.ExportNamedDeclaration &&
|
|
65
|
+
statement.declaration
|
|
66
|
+
? statement.declaration
|
|
67
|
+
: statement;
|
|
68
|
+
if ((declared.type === utils_1.AST_NODE_TYPES.TSInterfaceDeclaration ||
|
|
69
|
+
declared.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration) &&
|
|
70
|
+
declared.id.name === name) {
|
|
71
|
+
return declared;
|
|
72
|
+
}
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Resolves a type name against every enclosing statement container, innermost
|
|
77
|
+
* outward, so the nearest declaration shadows a same-named outer one.
|
|
78
|
+
*
|
|
79
|
+
* Searching `Program.body` alone left the two commonest spellings unresolvable:
|
|
80
|
+
* an exported declaration sits inside its `export` statement, and a declaration
|
|
81
|
+
* written in a function body, block, or namespace sits inside that. Since an
|
|
82
|
+
* unresolved name is treated as carrying no readable members, the hole silently
|
|
83
|
+
* dropped the nested-`any` check rather than reporting anything.
|
|
84
|
+
*/
|
|
85
|
+
function declarationOfType(from, name) {
|
|
86
|
+
let current = from;
|
|
87
|
+
while (current) {
|
|
88
|
+
const statements = statementsOf(current);
|
|
89
|
+
if (statements) {
|
|
90
|
+
for (const statement of statements) {
|
|
91
|
+
const declaration = typeDeclarationNamed(statement, name);
|
|
92
|
+
if (declaration) {
|
|
93
|
+
return declaration;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
current = current.parent;
|
|
98
|
+
}
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
14
101
|
/**
|
|
15
102
|
* @type {import('eslint').Rule.RuleModule}
|
|
16
103
|
*/
|
|
@@ -48,7 +135,13 @@ exports.enforceFirestoreDocRefGeneric = (0, createRule_1.createRule)({
|
|
|
48
135
|
},
|
|
49
136
|
defaultOptions: [],
|
|
50
137
|
create(context) {
|
|
51
|
-
|
|
138
|
+
/**
|
|
139
|
+
* Keyed on the resolved declaration rather than on the name, because
|
|
140
|
+
* resolution is lexical: two scopes in one file may declare the same name
|
|
141
|
+
* with different fields, and a name-keyed answer would carry one scope's
|
|
142
|
+
* verdict into the other.
|
|
143
|
+
*/
|
|
144
|
+
const declarationCache = new WeakMap();
|
|
52
145
|
const nodeCache = new WeakMap();
|
|
53
146
|
function hasInvalidType(node) {
|
|
54
147
|
if (!node)
|
|
@@ -66,19 +159,7 @@ exports.enforceFirestoreDocRefGeneric = (0, createRule_1.createRule)({
|
|
|
66
159
|
return node.typeParameters.params.some(hasInvalidType);
|
|
67
160
|
}
|
|
68
161
|
if (node.typeName.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
69
|
-
|
|
70
|
-
if (typeCache.has(typeName)) {
|
|
71
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
72
|
-
return typeCache.get(typeName);
|
|
73
|
-
}
|
|
74
|
-
// Prevent infinite recursion
|
|
75
|
-
typeCache.set(typeName, false);
|
|
76
|
-
const members = declaredMembersOf(typeName);
|
|
77
|
-
if (members) {
|
|
78
|
-
const result = membersHaveInvalidType(members);
|
|
79
|
-
typeCache.set(typeName, result);
|
|
80
|
-
return result;
|
|
81
|
-
}
|
|
162
|
+
return declaredTypeHasInvalidType(node.typeName);
|
|
82
163
|
}
|
|
83
164
|
return false;
|
|
84
165
|
case utils_1.AST_NODE_TYPES.TSIntersectionType:
|
|
@@ -155,8 +236,8 @@ exports.enforceFirestoreDocRefGeneric = (0, createRule_1.createRule)({
|
|
|
155
236
|
: undefined;
|
|
156
237
|
}
|
|
157
238
|
/**
|
|
158
|
-
*
|
|
159
|
-
*
|
|
239
|
+
* The members a declaration lists, reading an interface and a type alias
|
|
240
|
+
* alike.
|
|
160
241
|
*
|
|
161
242
|
* The alias spelling is not an extra convenience: `prefer-type-over-interface`
|
|
162
243
|
* ships in the same recommended config and is fixable, so a single
|
|
@@ -165,18 +246,36 @@ exports.enforceFirestoreDocRefGeneric = (0, createRule_1.createRule)({
|
|
|
165
246
|
* has run the config, and a nested `any` in a document schema goes
|
|
166
247
|
* unreported.
|
|
167
248
|
*/
|
|
168
|
-
function declaredMembersOf(
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
249
|
+
function declaredMembersOf(declaration) {
|
|
250
|
+
return declaration.type === utils_1.AST_NODE_TYPES.TSInterfaceDeclaration
|
|
251
|
+
? declaration.body.body
|
|
252
|
+
: aliasedTypeLiteral(declaration.typeAnnotation)?.members;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Reports whether the type a name stands for declares a field this rule
|
|
256
|
+
* rejects, resolving the name from the reference site outward.
|
|
257
|
+
*
|
|
258
|
+
* The declaration doubles as the recursion guard: a self-referential schema
|
|
259
|
+
* such as `type Node = { child: Node }` reaches its own entry, which is
|
|
260
|
+
* seeded `false` before its members are read.
|
|
261
|
+
*/
|
|
262
|
+
function declaredTypeHasInvalidType(typeName) {
|
|
263
|
+
const declaration = declarationOfType(typeName, typeName.name);
|
|
264
|
+
if (!declaration) {
|
|
265
|
+
return false;
|
|
178
266
|
}
|
|
179
|
-
|
|
267
|
+
const cached = declarationCache.get(declaration);
|
|
268
|
+
if (cached !== undefined) {
|
|
269
|
+
return cached;
|
|
270
|
+
}
|
|
271
|
+
declarationCache.set(declaration, false);
|
|
272
|
+
const members = declaredMembersOf(declaration);
|
|
273
|
+
if (!members) {
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
const result = membersHaveInvalidType(members);
|
|
277
|
+
declarationCache.set(declaration, result);
|
|
278
|
+
return result;
|
|
180
279
|
}
|
|
181
280
|
function hasTypeAnnotation(node) {
|
|
182
281
|
if (nodeCache.has(node)) {
|
|
@@ -655,11 +754,7 @@ exports.enforceFirestoreDocRefGeneric = (0, createRule_1.createRule)({
|
|
|
655
754
|
}
|
|
656
755
|
function hasCollectionReferenceType(typeNode) {
|
|
657
756
|
if (typeNode.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
|
|
658
|
-
(
|
|
659
|
-
typeNode.typeName.name === 'CollectionReference') ||
|
|
660
|
-
(typeNode.typeName.type === utils_1.AST_NODE_TYPES.TSQualifiedName &&
|
|
661
|
-
typeNode.typeName.right.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
662
|
-
typeNode.typeName.right.name === 'CollectionReference')) &&
|
|
757
|
+
referenceTypeNameOf(typeNode.typeName) === 'CollectionReference' &&
|
|
663
758
|
typeNode.typeParameters &&
|
|
664
759
|
typeNode.typeParameters.params.length > 0) {
|
|
665
760
|
return true;
|
|
@@ -860,11 +955,8 @@ exports.enforceFirestoreDocRefGeneric = (0, createRule_1.createRule)({
|
|
|
860
955
|
}
|
|
861
956
|
return {
|
|
862
957
|
TSTypeReference(node) {
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
node.typeName.name === 'CollectionReference' ||
|
|
866
|
-
node.typeName.name === 'CollectionGroup')) {
|
|
867
|
-
const typeName = node.typeName.name;
|
|
958
|
+
const typeName = referenceTypeNameOf(node.typeName);
|
|
959
|
+
if (typeName) {
|
|
868
960
|
// Check if generic type argument is missing
|
|
869
961
|
if (!node.typeParameters || node.typeParameters.params.length === 0) {
|
|
870
962
|
context.report({
|
|
@@ -192,6 +192,68 @@ function isPrimitiveLiteral(node) {
|
|
|
192
192
|
typeof value === 'boolean' ||
|
|
193
193
|
typeof value === 'bigint');
|
|
194
194
|
}
|
|
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
|
+
/** Whether a declarator is initialized from a `<x>.firestore()` call. */
|
|
210
|
+
function initializesFirestore(declarator) {
|
|
211
|
+
const { init } = declarator;
|
|
212
|
+
return (init?.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
213
|
+
init.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
214
|
+
init.callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
215
|
+
init.callee.property.name === 'firestore');
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Whether a statement declares a Firestore instance, looking through `export`.
|
|
219
|
+
*
|
|
220
|
+
* `export const db = admin.firestore()` is the same declaration one AST node
|
|
221
|
+
* deeper, inside an `ExportNamedDeclaration`. Reading the statement without
|
|
222
|
+
* unwrapping makes the `export` keyword alone decide whether the file's
|
|
223
|
+
* Firestore evidence is visible, which is not a distinction a `db` handle knows
|
|
224
|
+
* anything about — `classBodiesByName()` already unwraps it for the same
|
|
225
|
+
* "find the in-file declaration that carries the evidence" purpose.
|
|
226
|
+
*/
|
|
227
|
+
function declaresFirestoreInstance(statement) {
|
|
228
|
+
const declaration = statement.type === utils_1.AST_NODE_TYPES.ExportNamedDeclaration &&
|
|
229
|
+
statement.declaration
|
|
230
|
+
? statement.declaration
|
|
231
|
+
: statement;
|
|
232
|
+
return (declaration.type === utils_1.AST_NODE_TYPES.VariableDeclaration &&
|
|
233
|
+
declaration.declarations.some(initializesFirestore));
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Whether the file itself proves Firestore is in play at the call site, by
|
|
237
|
+
* searching every enclosing statement container innermost outward.
|
|
238
|
+
*
|
|
239
|
+
* Scanning `Program.body` alone left both commonplace spellings invisible: a
|
|
240
|
+
* `const db = admin.firestore()` written inside the handler that uses it, and an
|
|
241
|
+
* exported one, which sits inside its `export` statement. Since this scan is the
|
|
242
|
+
* only detector left for a bare-identifier receiver, the hole silently dropped
|
|
243
|
+
* the report rather than producing a wrong one. A container without the evidence
|
|
244
|
+
* falls through to the next one out instead of answering for the whole chain.
|
|
245
|
+
*/
|
|
246
|
+
function hasFirestoreInstanceInScope(node) {
|
|
247
|
+
let current = node;
|
|
248
|
+
while (current) {
|
|
249
|
+
const statements = statementsOf(current);
|
|
250
|
+
if (statements?.some(declaresFirestoreInstance)) {
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
current = current.parent;
|
|
254
|
+
}
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
195
257
|
exports.enforceFirestoreSetMerge = (0, createRule_1.createRule)({
|
|
196
258
|
name: 'enforce-firestore-set-merge',
|
|
197
259
|
meta: {
|
|
@@ -397,25 +459,9 @@ exports.enforceFirestoreSetMerge = (0, createRule_1.createRule)({
|
|
|
397
459
|
object.name === 'transaction') {
|
|
398
460
|
return true;
|
|
399
461
|
}
|
|
400
|
-
// Check if it's a Firestore document reference by looking
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
for (const node of program.body) {
|
|
404
|
-
if (node.type === utils_1.AST_NODE_TYPES.VariableDeclaration) {
|
|
405
|
-
for (const decl of node.declarations) {
|
|
406
|
-
if (decl.init?.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
407
|
-
decl.init.callee.type ===
|
|
408
|
-
utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
409
|
-
decl.init.callee.property.type ===
|
|
410
|
-
utils_1.AST_NODE_TYPES.Identifier &&
|
|
411
|
-
decl.init.callee.property.name === 'firestore') {
|
|
412
|
-
return true;
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
return false;
|
|
462
|
+
// Check if it's a Firestore document reference by looking for the
|
|
463
|
+
// file's own `<x>.firestore()` handle, wherever it is declared.
|
|
464
|
+
return hasFirestoreInstanceInScope(node);
|
|
419
465
|
}
|
|
420
466
|
return false;
|
|
421
467
|
}
|
|
@@ -5,6 +5,34 @@ const utils_1 = require("@typescript-eslint/utils");
|
|
|
5
5
|
const createRule_1 = require("../utils/createRule");
|
|
6
6
|
const LATEST_CALLBACK_MODULE = 'use-latest-callback';
|
|
7
7
|
const LATEST_CALLBACK_HOOK = 'useLatestCallback';
|
|
8
|
+
function isFunctionNode(node) {
|
|
9
|
+
if (!node)
|
|
10
|
+
return false;
|
|
11
|
+
return (node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
12
|
+
node.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
13
|
+
node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Nearest enclosing function of a node, or `null` when the node sits at module
|
|
17
|
+
* scope. Class and object methods are reached through their `FunctionExpression`
|
|
18
|
+
* value, so no separate `MethodDefinition` case is needed.
|
|
19
|
+
*
|
|
20
|
+
* The walk starts at the parent, so a `FunctionDeclaration` passed in as the
|
|
21
|
+
* declaration site reports the function that CONTAINS it rather than itself.
|
|
22
|
+
*/
|
|
23
|
+
function getEnclosingFunction(node) {
|
|
24
|
+
let current = node.parent;
|
|
25
|
+
while (current) {
|
|
26
|
+
if (isFunctionNode(current)) {
|
|
27
|
+
return current;
|
|
28
|
+
}
|
|
29
|
+
if (current.type === utils_1.AST_NODE_TYPES.Program) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
current = current.parent;
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
8
36
|
/**
|
|
9
37
|
* Declaration forms whose binding is created once for the program's lifetime.
|
|
10
38
|
*
|
|
@@ -28,6 +56,32 @@ function isStableDeclaration(def) {
|
|
|
28
56
|
return false;
|
|
29
57
|
}
|
|
30
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* The hazard is identity churn measured against the CONSUMER, not absolute
|
|
61
|
+
* scope depth: `useRenderHits` only ever sees a new identity when the function
|
|
62
|
+
* that CALLS it re-runs and rebuilds the binding on the way. That holds exactly
|
|
63
|
+
* when the declaration and the call share a nearest enclosing function.
|
|
64
|
+
*
|
|
65
|
+
* When the declaration sits in a strictly outer function — a component factory,
|
|
66
|
+
* an HOC, a `describe` callback consumed from a nested `it` — the binding is
|
|
67
|
+
* created once per outer call and the calling function is created in that same
|
|
68
|
+
* call, so every run of the consumer sees the identical reference. The message's
|
|
69
|
+
* remedy is also unavailable there: `useCallback` cannot legally be called in a
|
|
70
|
+
* function that is neither a component nor a hook, and a helper closing over an
|
|
71
|
+
* outer parameter cannot be hoisted to module scope. Module scope is the
|
|
72
|
+
* degenerate case: the declaration has no enclosing function at all.
|
|
73
|
+
*
|
|
74
|
+
* Scope resolution guarantees the declaration's scope is on the consumer's scope
|
|
75
|
+
* chain, so "not the same function" and "strictly encloses" coincide here.
|
|
76
|
+
*
|
|
77
|
+
* A custom hook is deliberately NOT special-cased. A hook body does re-run per
|
|
78
|
+
* render, and when it also holds the `useRenderHits` call the two functions
|
|
79
|
+
* coincide, so the same predicate reports it.
|
|
80
|
+
*/
|
|
81
|
+
function isStableForConsumer(defNode, consumerFunction) {
|
|
82
|
+
const definitionFunction = getEnclosingFunction(defNode);
|
|
83
|
+
return definitionFunction === null || definitionFunction !== consumerFunction;
|
|
84
|
+
}
|
|
31
85
|
exports.enforceRenderHitsMemoization = (0, createRule_1.createRule)({
|
|
32
86
|
name: 'enforce-render-hits-memoization',
|
|
33
87
|
meta: {
|
|
@@ -116,21 +170,25 @@ exports.enforceRenderHitsMemoization = (0, createRule_1.createRule)({
|
|
|
116
170
|
return false;
|
|
117
171
|
};
|
|
118
172
|
/**
|
|
119
|
-
* A prop pointing at a declaration that lives outside
|
|
173
|
+
* A prop pointing at a declaration that lives outside the body of the
|
|
174
|
+
* function calling `useRenderHits`.
|
|
120
175
|
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
176
|
+
* Such a binding is created once per run of the enclosing scope, and the
|
|
177
|
+
* calling function is created in that same run, so its identity is fixed for
|
|
178
|
+
* the whole life of that closure — strictly more stable than anything a hook
|
|
179
|
+
* can hand back. Demanding a `useCallback` wrapper around it asks for work
|
|
180
|
+
* that can only make the reference less stable, never more, and in a factory
|
|
181
|
+
* the wrapper is not even legal (rules-of-hooks) while a helper closing over
|
|
182
|
+
* an outer parameter cannot be hoisted to module scope either.
|
|
125
183
|
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
* exists at all (issue #1578)
|
|
130
|
-
*
|
|
131
|
-
* parsing.
|
|
184
|
+
* Module and global scope are the degenerate case of "outside", and both
|
|
185
|
+
* count. Under `sourceType: 'script'` — the parser default, and what a
|
|
186
|
+
* consumer's config may well leave in place — a top-level declaration binds
|
|
187
|
+
* to the *global* scope and no module scope exists at all (issue #1578);
|
|
188
|
+
* measuring against the enclosing function rather than the scope type keeps
|
|
189
|
+
* the carve-out for consumers who never opted into module parsing.
|
|
132
190
|
*
|
|
133
|
-
* The shape is not hypothetical:
|
|
191
|
+
* The module-scope shape is not hypothetical:
|
|
134
192
|
* `no-empty-dependency-use-callbacks` — 'error' in the same recommended
|
|
135
193
|
* config, and fixable — hoists a dependency-free callback to module scope
|
|
136
194
|
* and drops the hook, so one `eslint --fix` run rewrites memoized code into
|
|
@@ -141,18 +199,17 @@ exports.enforceRenderHitsMemoization = (0, createRule_1.createRule)({
|
|
|
141
199
|
if (node.type !== utils_1.AST_NODE_TYPES.Identifier)
|
|
142
200
|
return false;
|
|
143
201
|
// The scope chain has to be walked rather than a single scope's variable
|
|
144
|
-
// list read: the useRenderHits call sits inside the component, so
|
|
145
|
-
//
|
|
202
|
+
// list read: the useRenderHits call sits inside the component, so an
|
|
203
|
+
// outer-scope declaration is never among the current scope's own
|
|
146
204
|
// variables.
|
|
147
205
|
const variable = utils_1.ASTUtils.findVariable(context.getScope(), node);
|
|
148
206
|
if (!variable)
|
|
149
207
|
return false;
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
return variable.defs.some(isStableDeclaration);
|
|
208
|
+
// The prop value sits lexically inside the useRenderHits call, so its
|
|
209
|
+
// nearest enclosing function is the consuming one.
|
|
210
|
+
const consumerFunction = getEnclosingFunction(node);
|
|
211
|
+
return variable.defs.some((def) => isStableDeclaration(def) &&
|
|
212
|
+
isStableForConsumer(def.node, consumerFunction));
|
|
156
213
|
};
|
|
157
214
|
const isInsideMemoizedCall = (node) => {
|
|
158
215
|
// Handle the case when node is already a memoized call
|
|
@@ -6,6 +6,62 @@ const createRule_1 = require("../utils/createRule");
|
|
|
6
6
|
const ASTHelpers_1 = require("../utils/ASTHelpers");
|
|
7
7
|
const LATEST_CALLBACK_MODULE = 'use-latest-callback';
|
|
8
8
|
const LATEST_CALLBACK_HOOK = 'useLatestCallback';
|
|
9
|
+
function isFunctionNode(node) {
|
|
10
|
+
if (!node)
|
|
11
|
+
return false;
|
|
12
|
+
return (node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
13
|
+
node.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
14
|
+
node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Nearest enclosing function of a node, or `null` when the node sits at module
|
|
18
|
+
* scope. Class and object methods are reached through their `FunctionExpression`
|
|
19
|
+
* value, so no separate `MethodDefinition` case is needed.
|
|
20
|
+
*
|
|
21
|
+
* The walk starts at the parent, so a `FunctionDeclaration` passed in as the
|
|
22
|
+
* declaration site reports the function that CONTAINS it rather than itself.
|
|
23
|
+
*/
|
|
24
|
+
function getEnclosingFunction(node) {
|
|
25
|
+
let current = node.parent;
|
|
26
|
+
while (current) {
|
|
27
|
+
if (isFunctionNode(current)) {
|
|
28
|
+
return current;
|
|
29
|
+
}
|
|
30
|
+
if (current.type === utils_1.AST_NODE_TYPES.Program) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
current = current.parent;
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The hazard is identity churn measured against the CONSUMER, not absolute scope
|
|
39
|
+
* depth: `adaptValue` only ever sees a new transform identity when the function
|
|
40
|
+
* that references it re-runs and rebuilds the binding on the way. That holds
|
|
41
|
+
* exactly when the declaration and the reference share a nearest enclosing
|
|
42
|
+
* function.
|
|
43
|
+
*
|
|
44
|
+
* When the declaration sits in a strictly outer function — a component factory,
|
|
45
|
+
* an HOC, a `describe` callback consumed from a nested `it`, a class-method
|
|
46
|
+
* factory, an IIFE — the binding is created once per outer call and the
|
|
47
|
+
* referencing function is created in that same call, so every render sees the
|
|
48
|
+
* identical reference. The message's remedy is also unavailable there: `useMemo`
|
|
49
|
+
* cannot legally be called in a factory that is neither a component nor a hook,
|
|
50
|
+
* and a helper closing over an outer parameter cannot be hoisted to module
|
|
51
|
+
* scope. Module scope is the degenerate case: the declaration has no enclosing
|
|
52
|
+
* function at all.
|
|
53
|
+
*
|
|
54
|
+
* Scope resolution guarantees the declaration's scope is on the reference's
|
|
55
|
+
* scope chain, so "not the same function" and "strictly encloses" coincide here.
|
|
56
|
+
*
|
|
57
|
+
* A custom hook is deliberately NOT special-cased, and neither is a plain helper
|
|
58
|
+
* called during render: both re-run per render, and when either also holds the
|
|
59
|
+
* reference the two functions coincide, so the same predicate reports it.
|
|
60
|
+
*/
|
|
61
|
+
function isStableForConsumer(defNode, consumerFunction) {
|
|
62
|
+
const definitionFunction = getEnclosingFunction(defNode);
|
|
63
|
+
return definitionFunction === null || definitionFunction !== consumerFunction;
|
|
64
|
+
}
|
|
9
65
|
exports.enforceTransformMemoization = (0, createRule_1.createRule)({
|
|
10
66
|
name: 'enforce-transform-memoization',
|
|
11
67
|
meta: {
|
|
@@ -69,6 +125,15 @@ exports.enforceTransformMemoization = (0, createRule_1.createRule)({
|
|
|
69
125
|
}
|
|
70
126
|
return '';
|
|
71
127
|
};
|
|
128
|
+
// Used ONLY by the dependency audit, where the question is "can this value
|
|
129
|
+
// differ between two renders", not "is this binding stable for its
|
|
130
|
+
// consumer". The consumer-relative predicate is deliberately not applied
|
|
131
|
+
// here: a hook call nested in a render callback (`keys.map((key) =>
|
|
132
|
+
// adaptValue({ transformValue: useMemo(...) }, Switch))`) has that callback
|
|
133
|
+
// as its nearest enclosing function, so measuring against it would drop the
|
|
134
|
+
// component's own props and state from the audit — a false negative. Naming
|
|
135
|
+
// one extra outer-scope value in the array, by contrast, is a legal and
|
|
136
|
+
// harmless remedy, so over-collection costs nothing here.
|
|
72
137
|
const isTopLevelScope = (scope) => scope.type === 'global' || scope.type === 'module';
|
|
73
138
|
const findVariableInScopeChain = (identifier) => {
|
|
74
139
|
if (!scopeManager)
|
|
@@ -352,8 +417,14 @@ exports.enforceTransformMemoization = (0, createRule_1.createRule)({
|
|
|
352
417
|
if (!variable) {
|
|
353
418
|
return { ok: true };
|
|
354
419
|
}
|
|
420
|
+
// `every` rather than `some` on the stability test: a name bound more
|
|
421
|
+
// than once is only as stable as its least stable binding. It also keeps
|
|
422
|
+
// a variable with NO definition — an ambient global such as `console`,
|
|
423
|
+
// resolved in the global scope with an empty `defs` — exempt, since such
|
|
424
|
+
// a binding is created once for the program's lifetime.
|
|
425
|
+
const consumerFunction = getEnclosingFunction(unwrapped);
|
|
355
426
|
if (variable.defs.some((def) => def.type === 'Parameter') ||
|
|
356
|
-
|
|
427
|
+
variable.defs.every((def) => isStableForConsumer(def.node, consumerFunction))) {
|
|
357
428
|
return { ok: true };
|
|
358
429
|
}
|
|
359
430
|
const init = getVariableInitializer(variable);
|
|
@@ -186,10 +186,15 @@ exports.extractGlobalConstants = (0, createRule_1.createRule)({
|
|
|
186
186
|
}
|
|
187
187
|
},
|
|
188
188
|
FunctionDeclaration(node) {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
189
|
+
/**
|
|
190
|
+
* The enclosing function, not the immediate parent. A
|
|
191
|
+
* FunctionDeclaration is a Statement, so its parent is always a
|
|
192
|
+
* statement container — Program, BlockStatement, StaticBlock,
|
|
193
|
+
* SwitchCase, an export, an IfStatement. It is never a direct child of
|
|
194
|
+
* a function node, which made the previous `node.parent.type` check
|
|
195
|
+
* unsatisfiable and this whole branch dead.
|
|
196
|
+
*/
|
|
197
|
+
if (node.parent && isInsideFunction(node.parent)) {
|
|
193
198
|
const scope = context.getScope();
|
|
194
199
|
const hasDependencies = ASTHelpers_1.ASTHelpers.blockIncludesIdentifier(node.body);
|
|
195
200
|
if (!hasDependencies && scope.type === 'function') {
|