@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 CHANGED
@@ -223,7 +223,7 @@ function noFrontendImportsFromFunctionsPatterns(pattern) {
223
223
  module.exports = {
224
224
  meta: {
225
225
  name: '@blumintinc/eslint-plugin-blumint',
226
- version: '1.20.115',
226
+ version: '1.20.117',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -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
- const typeCache = new Map();
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
- const typeName = node.typeName.name;
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
- * Resolves a named generic to the members its declaration lists, reading an
159
- * interface and a type alias alike.
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(typeName) {
169
- for (const statement of context.sourceCode.ast.body) {
170
- if (statement.type === utils_1.AST_NODE_TYPES.TSInterfaceDeclaration &&
171
- statement.id.name === typeName) {
172
- return statement.body.body;
173
- }
174
- if (statement.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration &&
175
- statement.id.name === typeName) {
176
- return aliasedTypeLiteral(statement.typeAnnotation)?.members;
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
- return undefined;
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
- ((typeNode.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
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
- if (node.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
864
- (node.typeName.name === 'DocumentReference' ||
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 at imports
401
- const program = ASTHelpers_1.ASTHelpers.getAncestors(context, node).find((node) => node.type === utils_1.AST_NODE_TYPES.Program);
402
- if (program) {
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 every component body.
173
+ * A prop pointing at a declaration that lives outside the body of the
174
+ * function calling `useRenderHits`.
120
175
  *
121
- * Module scope creates the binding once for the program's lifetime, so its
122
- * identity is strictly more stable than anything a hook can hand back:
123
- * demanding a `useCallback` wrapper around it asks for work that can only
124
- * make the reference less stable, never more.
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
- * Both `module` and `global` count. Under `sourceType: 'script'` the
127
- * parser default, and what a consumer's config may well leave in place a
128
- * top-level declaration binds to the *global* scope and no module scope
129
- * exists at all (issue #1578), so keying on `module` alone would silently
130
- * drop the carve-out for exactly the consumers who never opted into module
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 a
145
- // module-scope declaration is never among the current scope's own
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
- const scopeType = variable.scope.type;
151
- if (scopeType !== utils_1.TSESLint.Scope.ScopeType.module &&
152
- scopeType !== utils_1.TSESLint.Scope.ScopeType.global) {
153
- return false;
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
- isTopLevelScope(variable.scope)) {
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
- if (node.parent &&
190
- (node.parent.type === 'FunctionDeclaration' ||
191
- node.parent.type === 'FunctionExpression' ||
192
- node.parent.type === 'ArrowFunctionExpression')) {
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') {