@blumintinc/eslint-plugin-blumint 1.20.115 → 1.20.116
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 +30 -10
- package/lib/rules/extract-global-constants.js +9 -4
- package/lib/rules/prefer-batch-operations.js +49 -10
- 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 +47 -0
package/lib/index.js
CHANGED
|
@@ -11,6 +11,33 @@ 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
|
+
};
|
|
14
41
|
/**
|
|
15
42
|
* @type {import('eslint').Rule.RuleModule}
|
|
16
43
|
*/
|
|
@@ -655,11 +682,7 @@ exports.enforceFirestoreDocRefGeneric = (0, createRule_1.createRule)({
|
|
|
655
682
|
}
|
|
656
683
|
function hasCollectionReferenceType(typeNode) {
|
|
657
684
|
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')) &&
|
|
685
|
+
referenceTypeNameOf(typeNode.typeName) === 'CollectionReference' &&
|
|
663
686
|
typeNode.typeParameters &&
|
|
664
687
|
typeNode.typeParameters.params.length > 0) {
|
|
665
688
|
return true;
|
|
@@ -860,11 +883,8 @@ exports.enforceFirestoreDocRefGeneric = (0, createRule_1.createRule)({
|
|
|
860
883
|
}
|
|
861
884
|
return {
|
|
862
885
|
TSTypeReference(node) {
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
node.typeName.name === 'CollectionReference' ||
|
|
866
|
-
node.typeName.name === 'CollectionGroup')) {
|
|
867
|
-
const typeName = node.typeName.name;
|
|
886
|
+
const typeName = referenceTypeNameOf(node.typeName);
|
|
887
|
+
if (typeName) {
|
|
868
888
|
// Check if generic type argument is missing
|
|
869
889
|
if (!node.typeParameters || node.typeParameters.params.length === 0) {
|
|
870
890
|
context.report({
|
|
@@ -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') {
|
|
@@ -261,17 +261,45 @@ function isMapInstance(node) {
|
|
|
261
261
|
}
|
|
262
262
|
return false;
|
|
263
263
|
}
|
|
264
|
+
/** Statements a declaration can be a direct child of, innermost outward. */
|
|
265
|
+
function statementsOf(node) {
|
|
266
|
+
switch (node.type) {
|
|
267
|
+
case utils_1.AST_NODE_TYPES.Program:
|
|
268
|
+
case utils_1.AST_NODE_TYPES.BlockStatement:
|
|
269
|
+
case utils_1.AST_NODE_TYPES.TSModuleBlock:
|
|
270
|
+
case utils_1.AST_NODE_TYPES.StaticBlock:
|
|
271
|
+
return node.body;
|
|
272
|
+
case utils_1.AST_NODE_TYPES.SwitchCase:
|
|
273
|
+
return node.consequent;
|
|
274
|
+
default:
|
|
275
|
+
return undefined;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Lexical lookup of a local declaration, innermost scope outward.
|
|
280
|
+
*
|
|
281
|
+
* The walk used to climb the parent chain but only inspect `Program.body`,
|
|
282
|
+
* which meant a setter constructed inside the function that uses it — how
|
|
283
|
+
* essentially all production code is written — was never found, so the caller
|
|
284
|
+
* bailed and the rule reported nothing. Every enclosing statement container is
|
|
285
|
+
* searched instead, and the first match wins, which is what shadowing requires.
|
|
286
|
+
*/
|
|
264
287
|
function findVariableDeclaration(node, varName) {
|
|
265
288
|
let current = node;
|
|
266
289
|
while (current) {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
290
|
+
const statements = statementsOf(current);
|
|
291
|
+
if (statements) {
|
|
292
|
+
for (const statement of statements) {
|
|
293
|
+
const declaration = statement.type === utils_1.AST_NODE_TYPES.ExportNamedDeclaration &&
|
|
294
|
+
statement.declaration
|
|
295
|
+
? statement.declaration
|
|
296
|
+
: statement;
|
|
297
|
+
if (declaration.type !== utils_1.AST_NODE_TYPES.VariableDeclaration)
|
|
298
|
+
continue;
|
|
299
|
+
for (const decl of declaration.declarations) {
|
|
300
|
+
if (decl.id.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
301
|
+
decl.id.name === varName) {
|
|
302
|
+
return decl;
|
|
275
303
|
}
|
|
276
304
|
}
|
|
277
305
|
}
|
|
@@ -403,8 +431,19 @@ exports.preferBatchOperations = (0, createRule_1.createRule)({
|
|
|
403
431
|
});
|
|
404
432
|
}
|
|
405
433
|
}
|
|
406
|
-
|
|
407
|
-
|
|
434
|
+
/**
|
|
435
|
+
* For array methods, report on the first occurrence — one syntactic
|
|
436
|
+
* call inside a `map` callback still runs once per element.
|
|
437
|
+
*
|
|
438
|
+
* A direct `Promise.all([...])` is not that: its elements are written
|
|
439
|
+
* out, so a lone `set()` is a single write and the docs call it
|
|
440
|
+
* valid. `findLoopNode` stamps `isArrayMethod: 'map'` onto the
|
|
441
|
+
* Promise.all result, which used to route it here and report on the
|
|
442
|
+
* first call — and, by adding the node to `reportedLoops`, made the
|
|
443
|
+
* deferred second-occurrence branch below unreachable for every
|
|
444
|
+
* input. Excluding it hands the decision back to that branch.
|
|
445
|
+
*/
|
|
446
|
+
else if (loopInfo.isArrayMethod && !loopInfo.isPromiseAll) {
|
|
408
447
|
if (!reportedLoops.has(loopInfo.node)) {
|
|
409
448
|
reportedLoops.add(loopInfo.node);
|
|
410
449
|
context.report({
|
|
@@ -22,6 +22,50 @@ exports.requireHooksDefaultParams = (0, createRule_1.createRule)({
|
|
|
22
22
|
function isHookName(name) {
|
|
23
23
|
return name.startsWith('use') && name[3]?.toUpperCase() === name[3];
|
|
24
24
|
}
|
|
25
|
+
/** The declaration a statement introduces, looking through `export`. */
|
|
26
|
+
function typeDeclarationNamed(statement, name) {
|
|
27
|
+
const declared = statement.type === utils_1.AST_NODE_TYPES.ExportNamedDeclaration &&
|
|
28
|
+
statement.declaration
|
|
29
|
+
? statement.declaration
|
|
30
|
+
: statement;
|
|
31
|
+
if ((declared.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration ||
|
|
32
|
+
declared.type === utils_1.AST_NODE_TYPES.TSInterfaceDeclaration) &&
|
|
33
|
+
declared.id.name === name) {
|
|
34
|
+
return declared;
|
|
35
|
+
}
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Lexical resolution of a type name, innermost scope outward.
|
|
40
|
+
*
|
|
41
|
+
* `context.getScope()` returns the scope of the node being visited and
|
|
42
|
+
* `scope.variables` is own-scope-only, so a type declared anywhere above
|
|
43
|
+
* the hook is invisible to it — and when a *value* of the same name is
|
|
44
|
+
* bound nearby, the lookup succeeds with a non-type definition and the
|
|
45
|
+
* whole check is abandoned. Walking the enclosing statement containers
|
|
46
|
+
* answers the question the rule actually asks, and a name that resolves to
|
|
47
|
+
* something other than a type declaration simply keeps searching.
|
|
48
|
+
*/
|
|
49
|
+
function resolveTypeDeclaration(from, name) {
|
|
50
|
+
let current = from;
|
|
51
|
+
while (current) {
|
|
52
|
+
const body = current.type === utils_1.AST_NODE_TYPES.Program ||
|
|
53
|
+
current.type === utils_1.AST_NODE_TYPES.BlockStatement ||
|
|
54
|
+
current.type === utils_1.AST_NODE_TYPES.TSModuleBlock
|
|
55
|
+
? current.body
|
|
56
|
+
: undefined;
|
|
57
|
+
if (body) {
|
|
58
|
+
for (const statement of body) {
|
|
59
|
+
const found = typeDeclarationNamed(statement, name);
|
|
60
|
+
if (found) {
|
|
61
|
+
return found;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
current = current.parent;
|
|
66
|
+
}
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
25
69
|
function hasAllOptionalProperties(typeNode) {
|
|
26
70
|
// Handle type literals directly
|
|
27
71
|
if (typeNode.type === utils_1.AST_NODE_TYPES.TSTypeLiteral) {
|
|
@@ -38,29 +82,10 @@ exports.requireHooksDefaultParams = (0, createRule_1.createRule)({
|
|
|
38
82
|
if (typeName.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
39
83
|
return false;
|
|
40
84
|
}
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
// This handles cases where the type is imported from another module
|
|
46
|
-
return false;
|
|
47
|
-
}
|
|
48
|
-
const def = variable.defs[0].node;
|
|
49
|
-
if (def.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration) {
|
|
50
|
-
return hasAllOptionalProperties(def.typeAnnotation);
|
|
51
|
-
}
|
|
52
|
-
else if (def.type === utils_1.AST_NODE_TYPES.TSInterfaceDeclaration) {
|
|
53
|
-
return def.body.body.every((member) => {
|
|
54
|
-
if (member.type !== utils_1.AST_NODE_TYPES.TSPropertySignature) {
|
|
55
|
-
return false;
|
|
56
|
-
}
|
|
57
|
-
return member.optional === true;
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
// If we found the type definition but it's not a type alias or interface declaration,
|
|
61
|
-
// assume it's a type with required properties
|
|
62
|
-
// This handles cases where the type is imported from another module
|
|
63
|
-
return false;
|
|
85
|
+
const declaration = resolveTypeDeclaration(typeNode, typeName.name);
|
|
86
|
+
// An unresolved name is an imported type whose shape is unknowable
|
|
87
|
+
// here, so it is treated as carrying required properties.
|
|
88
|
+
return declaration ? hasAllOptionalProperties(declaration) : false;
|
|
64
89
|
}
|
|
65
90
|
// Handle type alias declarations
|
|
66
91
|
if (typeNode.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration) {
|
|
@@ -113,114 +138,16 @@ exports.requireHooksDefaultParams = (0, createRule_1.createRule)({
|
|
|
113
138
|
if (param.type === utils_1.AST_NODE_TYPES.ObjectPattern &&
|
|
114
139
|
param.typeAnnotation) {
|
|
115
140
|
const typeAnnotation = param.typeAnnotation.typeAnnotation;
|
|
116
|
-
if (typeAnnotation
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
node: param,
|
|
127
|
-
messageId: 'requireDefaultParams',
|
|
128
|
-
data: messageData,
|
|
129
|
-
fix(fixer) {
|
|
130
|
-
const paramText = context.sourceCode.getText(param);
|
|
131
|
-
return fixer.replaceText(param, `${paramText} = {}`);
|
|
132
|
-
},
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
else if (def.type === utils_1.AST_NODE_TYPES.TSInterfaceDeclaration) {
|
|
137
|
-
if (def.body.body.every((member) => {
|
|
138
|
-
if (member.type !== utils_1.AST_NODE_TYPES.TSPropertySignature) {
|
|
139
|
-
return false;
|
|
140
|
-
}
|
|
141
|
-
return member.optional === true;
|
|
142
|
-
})) {
|
|
143
|
-
context.report({
|
|
144
|
-
node: param,
|
|
145
|
-
messageId: 'requireDefaultParams',
|
|
146
|
-
data: messageData,
|
|
147
|
-
fix(fixer) {
|
|
148
|
-
const paramText = context.sourceCode.getText(param);
|
|
149
|
-
return fixer.replaceText(param, `${paramText} = {}`);
|
|
150
|
-
},
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
else {
|
|
156
|
-
// If we can't find the type definition, check if it's defined in the same file
|
|
157
|
-
const program = context.sourceCode.ast;
|
|
158
|
-
const typeDefinitions = program.body.filter((node) => {
|
|
159
|
-
if (node.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration ||
|
|
160
|
-
node.type === utils_1.AST_NODE_TYPES.TSInterfaceDeclaration) {
|
|
161
|
-
if (node.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration) {
|
|
162
|
-
return node.id.name === typeName.name;
|
|
163
|
-
}
|
|
164
|
-
else {
|
|
165
|
-
return node.id.name === typeName.name;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
return false;
|
|
169
|
-
});
|
|
170
|
-
if (typeDefinitions.length > 0) {
|
|
171
|
-
const def = typeDefinitions[0];
|
|
172
|
-
if (def.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration) {
|
|
173
|
-
if (hasAllOptionalProperties(def.typeAnnotation)) {
|
|
174
|
-
context.report({
|
|
175
|
-
node: param,
|
|
176
|
-
messageId: 'requireDefaultParams',
|
|
177
|
-
data: messageData,
|
|
178
|
-
fix(fixer) {
|
|
179
|
-
const paramText = context.sourceCode.getText(param);
|
|
180
|
-
return fixer.replaceText(param, `${paramText} = {}`);
|
|
181
|
-
},
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
else if (def.type === utils_1.AST_NODE_TYPES.TSInterfaceDeclaration) {
|
|
186
|
-
if (def.body.body.every((member) => {
|
|
187
|
-
if (member.type !== utils_1.AST_NODE_TYPES.TSPropertySignature) {
|
|
188
|
-
return false;
|
|
189
|
-
}
|
|
190
|
-
return member.optional === true;
|
|
191
|
-
})) {
|
|
192
|
-
context.report({
|
|
193
|
-
node: param,
|
|
194
|
-
messageId: 'requireDefaultParams',
|
|
195
|
-
data: messageData,
|
|
196
|
-
fix(fixer) {
|
|
197
|
-
const paramText = context.sourceCode.getText(param);
|
|
198
|
-
return fixer.replaceText(param, `${paramText} = {}`);
|
|
199
|
-
},
|
|
200
|
-
});
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
else if (typeAnnotation.type === utils_1.AST_NODE_TYPES.TSTypeLiteral) {
|
|
208
|
-
if (typeAnnotation.members.every((member) => {
|
|
209
|
-
if (member.type !== utils_1.AST_NODE_TYPES.TSPropertySignature) {
|
|
210
|
-
return false;
|
|
211
|
-
}
|
|
212
|
-
return member.optional === true;
|
|
213
|
-
})) {
|
|
214
|
-
context.report({
|
|
215
|
-
node: param,
|
|
216
|
-
messageId: 'requireDefaultParams',
|
|
217
|
-
data: messageData,
|
|
218
|
-
fix(fixer) {
|
|
219
|
-
const paramText = context.sourceCode.getText(param);
|
|
220
|
-
return fixer.replaceText(param, `${paramText} = {}`);
|
|
221
|
-
},
|
|
222
|
-
});
|
|
223
|
-
}
|
|
141
|
+
if (hasAllOptionalProperties(typeAnnotation)) {
|
|
142
|
+
context.report({
|
|
143
|
+
node: param,
|
|
144
|
+
messageId: 'requireDefaultParams',
|
|
145
|
+
data: messageData,
|
|
146
|
+
fix(fixer) {
|
|
147
|
+
const paramText = context.sourceCode.getText(param);
|
|
148
|
+
return fixer.replaceText(param, `${paramText} = {}`);
|
|
149
|
+
},
|
|
150
|
+
});
|
|
224
151
|
}
|
|
225
152
|
}
|
|
226
153
|
},
|
|
@@ -95,8 +95,16 @@ function typeAnnotationReferencesFirestoreType(typeAnnotation, firestoreTypeName
|
|
|
95
95
|
* True for the assertion wrappers that leave the underlying expression intact:
|
|
96
96
|
* `x as T` and `x satisfies T`.
|
|
97
97
|
*/
|
|
98
|
+
/**
|
|
99
|
+
* Wrappers that change only the static type, never the runtime value. A
|
|
100
|
+
* non-null assertion belongs here for the same reason `as` does — `new Date()!`
|
|
101
|
+
* still constructs a client-clock Date — and it is exactly what a developer
|
|
102
|
+
* reaches for when silencing a nullability complaint, so leaving it wrapped
|
|
103
|
+
* turns an accident into a bypass.
|
|
104
|
+
*/
|
|
98
105
|
function isCastExpression(node) {
|
|
99
106
|
return (node.type === utils_1.AST_NODE_TYPES.TSAsExpression ||
|
|
107
|
+
node.type === utils_1.AST_NODE_TYPES.TSNonNullExpression ||
|
|
100
108
|
node.type ===
|
|
101
109
|
utils_1.AST_NODE_TYPES
|
|
102
110
|
.TSSatisfiesExpression);
|
|
@@ -175,19 +183,15 @@ context) {
|
|
|
175
183
|
else if (value.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
176
184
|
reportNewDatesInObject(value, context);
|
|
177
185
|
}
|
|
178
|
-
else if (value
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
186
|
+
else if (isCastExpression(value)) {
|
|
187
|
+
/**
|
|
188
|
+
* A cast can wrap an object literal that still needs recursion. It cannot
|
|
189
|
+
* wrap a `new Date()` that reaches here: `isNewDate` unwraps casts before
|
|
190
|
+
* answering, so the branch above has already reported that shape and this
|
|
191
|
+
* one is only ever entered when it did not.
|
|
192
|
+
*/
|
|
183
193
|
const inner = unwrapCast(value);
|
|
184
|
-
if (
|
|
185
|
-
context.report({
|
|
186
|
-
node: value,
|
|
187
|
-
messageId: 'useServerTimestamp',
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
else if (inner.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
194
|
+
if (inner.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
191
195
|
reportNewDatesInObject(inner, context);
|
|
192
196
|
}
|
|
193
197
|
}
|
package/lib/utils/ASTHelpers.js
CHANGED
|
@@ -186,6 +186,83 @@ class ASTHelpers {
|
|
|
186
186
|
case 'TSTypeLiteral':
|
|
187
187
|
// Handle type constraints and literals
|
|
188
188
|
return false;
|
|
189
|
+
/**
|
|
190
|
+
* Loops, switches and the remaining compound forms. Every node type this
|
|
191
|
+
* switch omits falls to `default: false` — "references nothing" — so an
|
|
192
|
+
* omission is not a missed detection but an inverted answer: a function
|
|
193
|
+
* whose only dependencies sit inside a `for` body reads as free-standing.
|
|
194
|
+
*/
|
|
195
|
+
case 'ForStatement':
|
|
196
|
+
return (this.declarationIncludesIdentifier(node.init) ||
|
|
197
|
+
this.declarationIncludesIdentifier(node.test) ||
|
|
198
|
+
this.declarationIncludesIdentifier(node.update) ||
|
|
199
|
+
this.declarationIncludesIdentifier(node.body));
|
|
200
|
+
case 'ForOfStatement':
|
|
201
|
+
case 'ForInStatement':
|
|
202
|
+
return (this.declarationIncludesIdentifier(node.left) ||
|
|
203
|
+
this.declarationIncludesIdentifier(node.right) ||
|
|
204
|
+
this.declarationIncludesIdentifier(node.body));
|
|
205
|
+
case 'WhileStatement':
|
|
206
|
+
case 'DoWhileStatement':
|
|
207
|
+
return (this.declarationIncludesIdentifier(node.test) ||
|
|
208
|
+
this.declarationIncludesIdentifier(node.body));
|
|
209
|
+
case 'SwitchStatement':
|
|
210
|
+
return (this.declarationIncludesIdentifier(node.discriminant) ||
|
|
211
|
+
node.cases.some((switchCase) => this.declarationIncludesIdentifier(switchCase)));
|
|
212
|
+
case 'SwitchCase':
|
|
213
|
+
return (this.declarationIncludesIdentifier(node.test) ||
|
|
214
|
+
node.consequent.some((statement) => this.declarationIncludesIdentifier(statement)));
|
|
215
|
+
case 'LabeledStatement':
|
|
216
|
+
return this.declarationIncludesIdentifier(node.body);
|
|
217
|
+
case 'SequenceExpression':
|
|
218
|
+
return node.expressions.some((expression) => this.declarationIncludesIdentifier(expression));
|
|
219
|
+
case 'TaggedTemplateExpression':
|
|
220
|
+
return (this.declarationIncludesIdentifier(node.tag) ||
|
|
221
|
+
this.declarationIncludesIdentifier(node.quasi));
|
|
222
|
+
case 'YieldExpression':
|
|
223
|
+
return this.declarationIncludesIdentifier(node.argument);
|
|
224
|
+
case 'ClassDeclaration':
|
|
225
|
+
case 'ClassExpression':
|
|
226
|
+
return (this.declarationIncludesIdentifier(node.superClass) ||
|
|
227
|
+
this.declarationIncludesIdentifier(node.body));
|
|
228
|
+
case 'ClassBody':
|
|
229
|
+
return node.body.some((member) => this.declarationIncludesIdentifier(member));
|
|
230
|
+
case 'MethodDefinition':
|
|
231
|
+
case 'PropertyDefinition':
|
|
232
|
+
return ((node.computed &&
|
|
233
|
+
this.declarationIncludesIdentifier(node.key)) ||
|
|
234
|
+
this.declarationIncludesIdentifier(node.value));
|
|
235
|
+
case 'ExportNamedDeclaration':
|
|
236
|
+
case 'ExportDefaultDeclaration':
|
|
237
|
+
return this.declarationIncludesIdentifier(node.declaration);
|
|
238
|
+
/**
|
|
239
|
+
* JSX subtrees carry references like any other expression. Without these
|
|
240
|
+
* cases a component that renders `<Component />` or `<div x={value} />`
|
|
241
|
+
* reads as depending on nothing, so a caller asking "can this be hoisted
|
|
242
|
+
* out of its enclosing scope?" gets `true` for a closure that cannot be.
|
|
243
|
+
*/
|
|
244
|
+
case 'JSXElement':
|
|
245
|
+
return (this.declarationIncludesIdentifier(node.openingElement) ||
|
|
246
|
+
node.children.some((child) => this.declarationIncludesIdentifier(child)));
|
|
247
|
+
case 'JSXFragment':
|
|
248
|
+
return node.children.some((child) => this.declarationIncludesIdentifier(child));
|
|
249
|
+
case 'JSXOpeningElement':
|
|
250
|
+
return (this.declarationIncludesIdentifier(node.name) ||
|
|
251
|
+
node.attributes.some((attribute) => this.declarationIncludesIdentifier(attribute)));
|
|
252
|
+
case 'JSXIdentifier':
|
|
253
|
+
// A lowercase tag is an intrinsic element (`div`), not a binding; an
|
|
254
|
+
// capitalized one resolves to a component in scope.
|
|
255
|
+
return !/^[a-z]/.test(node.name);
|
|
256
|
+
case 'JSXMemberExpression':
|
|
257
|
+
// `<Foo.Bar />` references `Foo`.
|
|
258
|
+
return true;
|
|
259
|
+
case 'JSXAttribute':
|
|
260
|
+
return this.declarationIncludesIdentifier(node.value);
|
|
261
|
+
case 'JSXSpreadAttribute':
|
|
262
|
+
case 'JSXSpreadChild':
|
|
263
|
+
return this.declarationIncludesIdentifier(node.argument ?? node.expression);
|
|
264
|
+
case 'JSXExpressionContainer':
|
|
265
|
+
return this.declarationIncludesIdentifier(node.expression);
|
|
189
266
|
default:
|
|
190
267
|
return false;
|
|
191
268
|
}
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,51 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.20.116",
|
|
4
|
+
"date": "2026-08-05T20:14:19.457Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "enforce-firestore-doc-ref-generic",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
1754
|
|
11
|
+
],
|
|
12
|
+
"summary": "detect namespaced reference types (closes #1754)"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "extract-global-constants",
|
|
16
|
+
"changeType": "fix",
|
|
17
|
+
"issues": [
|
|
18
|
+
1755
|
|
19
|
+
],
|
|
20
|
+
"summary": "report nested helper functions (closes #1755)"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"name": "prefer-batch-operations",
|
|
24
|
+
"changeType": "fix",
|
|
25
|
+
"issues": [
|
|
26
|
+
1757,
|
|
27
|
+
1759
|
|
28
|
+
],
|
|
29
|
+
"summary": "resolve the setter declaration lexically (closes #1759); stop flagging a lone set() in Promise.all (closes #1757)"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"name": "require-hooks-default-params",
|
|
33
|
+
"changeType": "fix",
|
|
34
|
+
"issues": [
|
|
35
|
+
1756
|
|
36
|
+
],
|
|
37
|
+
"summary": "resolve the options type lexically (closes #1756)"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"name": "require-server-timestamp-for-firestore-dates",
|
|
41
|
+
"changeType": "fix",
|
|
42
|
+
"issues": [
|
|
43
|
+
1758
|
|
44
|
+
],
|
|
45
|
+
"summary": "unwrap non-null assertions (closes #1758)"
|
|
46
|
+
}
|
|
47
|
+
]
|
|
48
|
+
},
|
|
2
49
|
{
|
|
3
50
|
"version": "1.20.115",
|
|
4
51
|
"date": "2026-08-05T18:41:41.153Z",
|