@blumintinc/eslint-plugin-blumint 1.20.46 → 1.20.48
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-props-naming-consistency.js +13 -0
- package/lib/rules/no-explicit-return-type.js +344 -4
- package/lib/rules/no-mock-firebase-admin.js +71 -0
- package/lib/rules/no-unnecessary-verb-suffix.js +50 -6
- package/lib/rules/prefer-nullish-coalescing-boolean-props.js +13 -1
- package/package.json +1 -1
- package/release-manifest.json +52 -0
package/lib/index.js
CHANGED
|
@@ -165,6 +165,19 @@ exports.enforcePropsNamingConsistency = (0, createRule_1.createRule)({
|
|
|
165
165
|
}
|
|
166
166
|
// Check function parameters
|
|
167
167
|
function checkFunctionParams(node) {
|
|
168
|
+
// A constructor is a `MethodDefinition` wrapping a `FunctionExpression`,
|
|
169
|
+
// so both this visitor and `checkClassConstructor` would otherwise fire on
|
|
170
|
+
// the same parameter and emit two identical reports — each carrying a fix
|
|
171
|
+
// over the same range, which ESLint deduplicates by discarding one, so a
|
|
172
|
+
// single `--fix` pass could not converge (Issue #1514). The constructor
|
|
173
|
+
// handler owns the case because it is a strict superset: it also reports
|
|
174
|
+
// parameter properties, counts them toward the multi-Props deferral, and
|
|
175
|
+
// gates the rename on `this.<name>` safety.
|
|
176
|
+
if (node.type === utils_1.AST_NODE_TYPES.FunctionExpression &&
|
|
177
|
+
node.parent?.type === utils_1.AST_NODE_TYPES.MethodDefinition &&
|
|
178
|
+
node.parent.kind === 'constructor') {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
168
181
|
// Skip functions with multiple parameters that have Props types
|
|
169
182
|
const propsTypeParams = node.params.filter((param) => {
|
|
170
183
|
if (param.type !== utils_1.AST_NODE_TYPES.Identifier)
|
|
@@ -148,6 +148,306 @@ function isRecursiveFunction(node) {
|
|
|
148
148
|
checkNode(node.body);
|
|
149
149
|
return hasRecursiveCall;
|
|
150
150
|
}
|
|
151
|
+
const THIS_OWNER = 'this';
|
|
152
|
+
const FUNCTION_NODE_TYPES = new Set([
|
|
153
|
+
utils_1.AST_NODE_TYPES.FunctionDeclaration,
|
|
154
|
+
utils_1.AST_NODE_TYPES.FunctionExpression,
|
|
155
|
+
utils_1.AST_NODE_TYPES.ArrowFunctionExpression,
|
|
156
|
+
]);
|
|
157
|
+
function pushChildren(node, visitorKeys, stack) {
|
|
158
|
+
for (const key of visitorKeys[node.type] ?? []) {
|
|
159
|
+
const value = node[key];
|
|
160
|
+
const children = Array.isArray(value) ? value : [value];
|
|
161
|
+
for (const child of children) {
|
|
162
|
+
if (child && typeof child === 'object' && 'type' in child) {
|
|
163
|
+
stack.push(child);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Return expressions belonging to `fn` itself: the concise arrow body, or the
|
|
170
|
+
* arguments of every `return` whose nearest enclosing function is `fn`.
|
|
171
|
+
* Returns of nested functions belong to those functions, not to `fn`.
|
|
172
|
+
*/
|
|
173
|
+
function collectOwnReturnExpressions(fn, visitorKeys) {
|
|
174
|
+
if (fn.body.type !== utils_1.AST_NODE_TYPES.BlockStatement) {
|
|
175
|
+
return [fn.body];
|
|
176
|
+
}
|
|
177
|
+
const returnExpressions = [];
|
|
178
|
+
const stack = [...fn.body.body];
|
|
179
|
+
while (stack.length > 0) {
|
|
180
|
+
const current = stack.pop();
|
|
181
|
+
if (FUNCTION_NODE_TYPES.has(current.type))
|
|
182
|
+
continue;
|
|
183
|
+
if (current.type === utils_1.AST_NODE_TYPES.ReturnStatement) {
|
|
184
|
+
if (current.argument) {
|
|
185
|
+
returnExpressions.push(current.argument);
|
|
186
|
+
}
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
pushChildren(current, visitorKeys, stack);
|
|
190
|
+
}
|
|
191
|
+
return returnExpressions;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Identifiers that name something (object keys, member property names) are not
|
|
195
|
+
* references to the binding of the same name.
|
|
196
|
+
*/
|
|
197
|
+
function isReferencePosition(node) {
|
|
198
|
+
const parent = node.parent;
|
|
199
|
+
if (!parent)
|
|
200
|
+
return true;
|
|
201
|
+
if (parent.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
202
|
+
!parent.computed &&
|
|
203
|
+
parent.property === node) {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
if ((parent.type === utils_1.AST_NODE_TYPES.Property ||
|
|
207
|
+
parent.type === utils_1.AST_NODE_TYPES.PropertyDefinition ||
|
|
208
|
+
parent.type === utils_1.AST_NODE_TYPES.MethodDefinition) &&
|
|
209
|
+
!parent.computed &&
|
|
210
|
+
parent.key === node) {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
function ownerMatches(object, owners) {
|
|
216
|
+
if (object.type === utils_1.AST_NODE_TYPES.ThisExpression) {
|
|
217
|
+
return owners.has(THIS_OWNER);
|
|
218
|
+
}
|
|
219
|
+
if (object.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
220
|
+
return owners.has(object.name);
|
|
221
|
+
}
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Searches a whole expression subtree, nested functions included. A closure in
|
|
226
|
+
* the returned value is part of the return type, so a self-reference inside it
|
|
227
|
+
* can be what defeats inference: `return { orderBy: () => buildQuery(p) }
|
|
228
|
+
* satisfies FakeQuery` is TS7023. Whether TypeScript manages to break such a
|
|
229
|
+
* cycle depends on type information this rule does not have, so every reference
|
|
230
|
+
* in a return expression counts — erring toward silence, per the repo's
|
|
231
|
+
* preference for false negatives over false positives.
|
|
232
|
+
*/
|
|
233
|
+
function subtreeReferences(root, selfReferences, visitorKeys) {
|
|
234
|
+
const stack = [root];
|
|
235
|
+
while (stack.length > 0) {
|
|
236
|
+
const current = stack.pop();
|
|
237
|
+
if (current.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
238
|
+
isReferencePosition(current) &&
|
|
239
|
+
selfReferences.some((reference) => reference.kind === 'identifier' && reference.name === current.name)) {
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
if (current.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
243
|
+
!current.computed &&
|
|
244
|
+
current.property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
245
|
+
const propertyName = current.property.name;
|
|
246
|
+
const matchesMember = selfReferences.some((reference) => reference.kind === 'member' &&
|
|
247
|
+
reference.name === propertyName &&
|
|
248
|
+
ownerMatches(current.object, reference.owners));
|
|
249
|
+
if (matchesMember)
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
pushChildren(current, visitorKeys, stack);
|
|
253
|
+
}
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
function ownerNamesOfObjectExpression(objectExpression) {
|
|
257
|
+
const owners = new Set([THIS_OWNER]);
|
|
258
|
+
const parent = objectExpression.parent;
|
|
259
|
+
if (parent?.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
260
|
+
parent.id.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
261
|
+
owners.add(parent.id.name);
|
|
262
|
+
}
|
|
263
|
+
return owners;
|
|
264
|
+
}
|
|
265
|
+
function ownerNamesOfClassMember(member) {
|
|
266
|
+
const owners = new Set([THIS_OWNER]);
|
|
267
|
+
const classBody = member.parent;
|
|
268
|
+
const classNode = classBody?.parent;
|
|
269
|
+
if (classNode?.type === utils_1.AST_NODE_TYPES.ClassDeclaration ||
|
|
270
|
+
classNode?.type === utils_1.AST_NODE_TYPES.ClassExpression) {
|
|
271
|
+
if (classNode.id) {
|
|
272
|
+
owners.add(classNode.id.name);
|
|
273
|
+
}
|
|
274
|
+
// A class expression assigned to a binding is also reachable by that name.
|
|
275
|
+
if (classNode.parent?.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
276
|
+
classNode.parent.id.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
277
|
+
owners.add(classNode.parent.id.name);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return owners;
|
|
281
|
+
}
|
|
282
|
+
function keyName(node) {
|
|
283
|
+
if (node.computed)
|
|
284
|
+
return undefined;
|
|
285
|
+
if (node.key.type === utils_1.AST_NODE_TYPES.Identifier ||
|
|
286
|
+
(node.key.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
287
|
+
typeof node.key.value === 'string')) {
|
|
288
|
+
return getNameFromIdentifierOrLiteral(node.key);
|
|
289
|
+
}
|
|
290
|
+
return undefined;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Every name by which the function can reach itself. A function with no
|
|
294
|
+
* resolvable name cannot be self-referential by name, so it yields none.
|
|
295
|
+
*/
|
|
296
|
+
function resolveSelfReferences(node) {
|
|
297
|
+
const selfReferences = [];
|
|
298
|
+
if (node.type === utils_1.AST_NODE_TYPES.MethodDefinition) {
|
|
299
|
+
const name = keyName(node);
|
|
300
|
+
if (name) {
|
|
301
|
+
selfReferences.push({
|
|
302
|
+
kind: 'member',
|
|
303
|
+
name,
|
|
304
|
+
owners: ownerNamesOfClassMember(node),
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
return selfReferences;
|
|
308
|
+
}
|
|
309
|
+
if ((node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
|
|
310
|
+
node.type === utils_1.AST_NODE_TYPES.FunctionExpression) &&
|
|
311
|
+
node.id) {
|
|
312
|
+
selfReferences.push({ kind: 'identifier', name: node.id.name });
|
|
313
|
+
}
|
|
314
|
+
const parent = node.parent;
|
|
315
|
+
if (parent?.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
316
|
+
parent.id.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
317
|
+
selfReferences.push({ kind: 'identifier', name: parent.id.name });
|
|
318
|
+
}
|
|
319
|
+
if (parent?.type === utils_1.AST_NODE_TYPES.Property) {
|
|
320
|
+
const name = keyName(parent);
|
|
321
|
+
if (name) {
|
|
322
|
+
selfReferences.push({
|
|
323
|
+
kind: 'member',
|
|
324
|
+
name,
|
|
325
|
+
owners: ownerNamesOfObjectExpression(parent.parent),
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
if (parent?.type === utils_1.AST_NODE_TYPES.PropertyDefinition) {
|
|
330
|
+
const name = keyName(parent);
|
|
331
|
+
if (name) {
|
|
332
|
+
selfReferences.push({
|
|
333
|
+
kind: 'member',
|
|
334
|
+
name,
|
|
335
|
+
owners: ownerNamesOfClassMember(parent),
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
if (parent?.type === utils_1.AST_NODE_TYPES.AssignmentExpression) {
|
|
340
|
+
const target = parent.left;
|
|
341
|
+
if (target.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
342
|
+
selfReferences.push({ kind: 'identifier', name: target.name });
|
|
343
|
+
}
|
|
344
|
+
else if (target.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
345
|
+
!target.computed &&
|
|
346
|
+
target.property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
347
|
+
const owners = new Set();
|
|
348
|
+
if (target.object.type === utils_1.AST_NODE_TYPES.ThisExpression) {
|
|
349
|
+
owners.add(THIS_OWNER);
|
|
350
|
+
}
|
|
351
|
+
else if (target.object.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
352
|
+
owners.add(target.object.name);
|
|
353
|
+
}
|
|
354
|
+
if (owners.size > 0) {
|
|
355
|
+
selfReferences.push({
|
|
356
|
+
kind: 'member',
|
|
357
|
+
name: target.property.name,
|
|
358
|
+
owners,
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return selfReferences;
|
|
364
|
+
}
|
|
365
|
+
function bodyOf(node) {
|
|
366
|
+
if (node.type === utils_1.AST_NODE_TYPES.MethodDefinition) {
|
|
367
|
+
return node.value.body ? node.value : undefined;
|
|
368
|
+
}
|
|
369
|
+
if ((node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
|
|
370
|
+
node.type === utils_1.AST_NODE_TYPES.FunctionExpression) &&
|
|
371
|
+
node.body) {
|
|
372
|
+
return node;
|
|
373
|
+
}
|
|
374
|
+
if (node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
375
|
+
return node;
|
|
376
|
+
}
|
|
377
|
+
return undefined;
|
|
378
|
+
}
|
|
379
|
+
/** Bare identifiers referenced from a function's own return expressions. */
|
|
380
|
+
function collectReturnIdentifierNames(fn, visitorKeys) {
|
|
381
|
+
const names = new Set();
|
|
382
|
+
for (const returnExpression of collectOwnReturnExpressions(fn, visitorKeys)) {
|
|
383
|
+
const stack = [returnExpression];
|
|
384
|
+
while (stack.length > 0) {
|
|
385
|
+
const current = stack.pop();
|
|
386
|
+
if (current.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
387
|
+
isReferencePosition(current)) {
|
|
388
|
+
names.add(current.name);
|
|
389
|
+
}
|
|
390
|
+
pushChildren(current, visitorKeys, stack);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return names;
|
|
394
|
+
}
|
|
395
|
+
function moduleScopeFunctions(program) {
|
|
396
|
+
const functions = new Map();
|
|
397
|
+
const statements = program.body.map((statement) => {
|
|
398
|
+
if (statement.type === utils_1.AST_NODE_TYPES.ExportNamedDeclaration ||
|
|
399
|
+
statement.type === utils_1.AST_NODE_TYPES.ExportDefaultDeclaration) {
|
|
400
|
+
return statement.declaration ?? statement;
|
|
401
|
+
}
|
|
402
|
+
return statement;
|
|
403
|
+
});
|
|
404
|
+
for (const statement of statements) {
|
|
405
|
+
if (statement.type === utils_1.AST_NODE_TYPES.FunctionDeclaration &&
|
|
406
|
+
statement.id &&
|
|
407
|
+
statement.body) {
|
|
408
|
+
functions.set(statement.id.name, statement);
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
if (statement.type === utils_1.AST_NODE_TYPES.VariableDeclaration) {
|
|
412
|
+
for (const declarator of statement.declarations) {
|
|
413
|
+
const init = declarator.init;
|
|
414
|
+
if (declarator.id.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
415
|
+
init &&
|
|
416
|
+
(init.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
417
|
+
(init.type === utils_1.AST_NODE_TYPES.FunctionExpression && init.body))) {
|
|
418
|
+
functions.set(declarator.id.name, init);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return functions;
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Maps each module-scope function name to the names it references from its own
|
|
427
|
+
* return expressions. A cycle in this graph is mutual recursion, which triggers
|
|
428
|
+
* the same TS7023 as direct self-reference.
|
|
429
|
+
*/
|
|
430
|
+
function buildReturnReferenceGraph(program, visitorKeys) {
|
|
431
|
+
const graph = new Map();
|
|
432
|
+
for (const [name, fn] of moduleScopeFunctions(program)) {
|
|
433
|
+
graph.set(name, collectReturnIdentifierNames(fn, visitorKeys));
|
|
434
|
+
}
|
|
435
|
+
return graph;
|
|
436
|
+
}
|
|
437
|
+
function participatesInReturnCycle(name, graph) {
|
|
438
|
+
const seen = new Set();
|
|
439
|
+
const stack = [...(graph.get(name) ?? [])];
|
|
440
|
+
while (stack.length > 0) {
|
|
441
|
+
const current = stack.pop();
|
|
442
|
+
if (current === name)
|
|
443
|
+
return true;
|
|
444
|
+
if (seen.has(current))
|
|
445
|
+
continue;
|
|
446
|
+
seen.add(current);
|
|
447
|
+
stack.push(...(graph.get(current) ?? []));
|
|
448
|
+
}
|
|
449
|
+
return false;
|
|
450
|
+
}
|
|
151
451
|
function isOverloadedFunction(node) {
|
|
152
452
|
if (!node.parent)
|
|
153
453
|
return false;
|
|
@@ -310,6 +610,40 @@ exports.noExplicitReturnType = (0, createRule_1.createRule)({
|
|
|
310
610
|
create(context, [options]) {
|
|
311
611
|
const mergedOptions = { ...defaultOptions, ...options };
|
|
312
612
|
const filename = context.getFilename();
|
|
613
|
+
const sourceCode = context.getSourceCode();
|
|
614
|
+
const visitorKeys = sourceCode.visitorKeys;
|
|
615
|
+
// Built at most once per file, and only when a direct self-reference has
|
|
616
|
+
// already been ruled out.
|
|
617
|
+
let returnReferenceGraph;
|
|
618
|
+
/**
|
|
619
|
+
* True when TypeScript cannot infer the return type because the function
|
|
620
|
+
* is referenced from within its own return expression (TS7023). Removing
|
|
621
|
+
* the annotation in that case does not compile, so the rule stays silent.
|
|
622
|
+
*/
|
|
623
|
+
function isReturnTypeRequiredByRecursion(node) {
|
|
624
|
+
if (!mergedOptions.allowRecursiveFunctions)
|
|
625
|
+
return false;
|
|
626
|
+
const fn = bodyOf(node);
|
|
627
|
+
if (!fn)
|
|
628
|
+
return false;
|
|
629
|
+
const selfReferences = resolveSelfReferences(node);
|
|
630
|
+
if (selfReferences.length === 0)
|
|
631
|
+
return false;
|
|
632
|
+
const returnExpressions = collectOwnReturnExpressions(fn, visitorKeys);
|
|
633
|
+
const referencesItself = returnExpressions.some((expression) => subtreeReferences(expression, selfReferences, visitorKeys));
|
|
634
|
+
if (referencesItself)
|
|
635
|
+
return true;
|
|
636
|
+
const identifierNames = selfReferences
|
|
637
|
+
.filter((reference) => reference.kind === 'identifier')
|
|
638
|
+
.map((reference) => reference.name);
|
|
639
|
+
if (identifierNames.length === 0)
|
|
640
|
+
return false;
|
|
641
|
+
if (!returnReferenceGraph) {
|
|
642
|
+
returnReferenceGraph = buildReturnReferenceGraph(sourceCode.ast, visitorKeys);
|
|
643
|
+
}
|
|
644
|
+
const graph = returnReferenceGraph;
|
|
645
|
+
return identifierNames.some((name) => participatesInReturnCycle(name, graph));
|
|
646
|
+
}
|
|
313
647
|
if ((mergedOptions.allowDtsFiles && filename.endsWith('.d.ts')) ||
|
|
314
648
|
(mergedOptions.allowFirestoreFunctionFiles &&
|
|
315
649
|
filename.endsWith('.f.ts'))) {
|
|
@@ -333,7 +667,9 @@ exports.noExplicitReturnType = (0, createRule_1.createRule)({
|
|
|
333
667
|
return;
|
|
334
668
|
if (isTypeGuardFunction(node) ||
|
|
335
669
|
isReadonlyWideningReturnType(returnType) ||
|
|
336
|
-
(mergedOptions.allowRecursiveFunctions &&
|
|
670
|
+
(mergedOptions.allowRecursiveFunctions &&
|
|
671
|
+
isRecursiveFunction(node)) ||
|
|
672
|
+
isReturnTypeRequiredByRecursion(node)) {
|
|
337
673
|
return;
|
|
338
674
|
}
|
|
339
675
|
const isInferable = Boolean(node.body);
|
|
@@ -357,7 +693,9 @@ exports.noExplicitReturnType = (0, createRule_1.createRule)({
|
|
|
357
693
|
}
|
|
358
694
|
if (isTypeGuardFunction(node) ||
|
|
359
695
|
isReadonlyWideningReturnType(returnType) ||
|
|
360
|
-
(mergedOptions.allowRecursiveFunctions &&
|
|
696
|
+
(mergedOptions.allowRecursiveFunctions &&
|
|
697
|
+
isRecursiveFunction(node)) ||
|
|
698
|
+
isReturnTypeRequiredByRecursion(node)) {
|
|
361
699
|
return;
|
|
362
700
|
}
|
|
363
701
|
context.report({
|
|
@@ -372,7 +710,8 @@ exports.noExplicitReturnType = (0, createRule_1.createRule)({
|
|
|
372
710
|
if (!returnType)
|
|
373
711
|
return;
|
|
374
712
|
if (isTypeGuardFunction(node) ||
|
|
375
|
-
isReadonlyWideningReturnType(returnType)
|
|
713
|
+
isReadonlyWideningReturnType(returnType) ||
|
|
714
|
+
isReturnTypeRequiredByRecursion(node)) {
|
|
376
715
|
return;
|
|
377
716
|
}
|
|
378
717
|
context.report({
|
|
@@ -406,7 +745,8 @@ exports.noExplicitReturnType = (0, createRule_1.createRule)({
|
|
|
406
745
|
if (isTypeGuardFunction(node.value) ||
|
|
407
746
|
isReadonlyWideningReturnType(returnType) ||
|
|
408
747
|
(mergedOptions.allowAbstractMethodSignatures &&
|
|
409
|
-
isInterfaceOrAbstractMethodSignature(node))
|
|
748
|
+
isInterfaceOrAbstractMethodSignature(node)) ||
|
|
749
|
+
isReturnTypeRequiredByRecursion(node)) {
|
|
410
750
|
return;
|
|
411
751
|
}
|
|
412
752
|
const isInferable = Boolean(node.value.body);
|
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.noMockFirebaseAdmin = void 0;
|
|
7
7
|
const path_1 = __importDefault(require("path"));
|
|
8
8
|
const utils_1 = require("@typescript-eslint/utils");
|
|
9
|
+
const ASTHelpers_1 = require("../utils/ASTHelpers");
|
|
9
10
|
const createRule_1 = require("../utils/createRule");
|
|
10
11
|
const FIREBASE_ADMIN_MODULE = 'firebaseAdmin';
|
|
11
12
|
const MODULE_EXTENSION = /\.(?:tsx?|jsx?|mjs|cjs)$/;
|
|
@@ -49,6 +50,72 @@ const bypassesSharedMock = (comparisonPath) => {
|
|
|
49
50
|
// shared mock.
|
|
50
51
|
return true;
|
|
51
52
|
};
|
|
53
|
+
/**
|
|
54
|
+
* The one Firestore surface the shared `mockFirestore` fake cannot express: it
|
|
55
|
+
* seeds collections by path and exposes no `collectionGroup` whatsoever. A suite
|
|
56
|
+
* that must drive a collection-group query — e.g. the `__name__`-ordered,
|
|
57
|
+
* index-free `orderBy`/`limit`/`startAfter` pagination loop the migration
|
|
58
|
+
* scripts run — therefore has no way to obey the message's remedy, and
|
|
59
|
+
* reporting it demands deleting the assertions the suite exists to make.
|
|
60
|
+
*
|
|
61
|
+
* Cursor pagination alone is NOT part of this exemption: `orderBy`, `limit` and
|
|
62
|
+
* `startAfter` over an ordinary collection are expressible through the shared
|
|
63
|
+
* fake, so exempting on those would excuse nearly every hand-rolled factory.
|
|
64
|
+
*/
|
|
65
|
+
const COLLECTION_GROUP = 'collectionGroup';
|
|
66
|
+
/**
|
|
67
|
+
* A string literal only counts where it names a member — an object key or a
|
|
68
|
+
* computed access — so prose that merely mentions the method (an error message,
|
|
69
|
+
* a comment-like string) cannot buy an exemption.
|
|
70
|
+
*/
|
|
71
|
+
const namesCollectionGroupLiteral = (node) => {
|
|
72
|
+
if (node.type === utils_1.AST_NODE_TYPES.Property) {
|
|
73
|
+
return (node.key.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
74
|
+
node.key.value === COLLECTION_GROUP);
|
|
75
|
+
}
|
|
76
|
+
if (node.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
77
|
+
return (node.computed &&
|
|
78
|
+
node.property.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
79
|
+
node.property.value === COLLECTION_GROUP);
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* Walk the factory for any reference to `collectionGroup`, at any depth: agora's
|
|
85
|
+
* fake defines it on a returned `db` object, but an equivalent fake may call it
|
|
86
|
+
* from inside a nested helper or method body.
|
|
87
|
+
*/
|
|
88
|
+
const exercisesCollectionGroup = (factory) => {
|
|
89
|
+
const stack = [factory];
|
|
90
|
+
while (stack.length > 0) {
|
|
91
|
+
const node = stack.pop();
|
|
92
|
+
if (node.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
93
|
+
node.name === COLLECTION_GROUP) {
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
if (namesCollectionGroupLiteral(node)) {
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
for (const [key, value] of Object.entries(node)) {
|
|
100
|
+
// `parent` points back up the tree; following it would walk the whole
|
|
101
|
+
// program and exempt any file that mentions collectionGroup anywhere.
|
|
102
|
+
if (key === 'parent') {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (Array.isArray(value)) {
|
|
106
|
+
for (const item of value) {
|
|
107
|
+
if (ASTHelpers_1.ASTHelpers.isNode(item)) {
|
|
108
|
+
stack.push(item);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
else if (ASTHelpers_1.ASTHelpers.isNode(value)) {
|
|
113
|
+
stack.push(value);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return false;
|
|
118
|
+
};
|
|
52
119
|
exports.noMockFirebaseAdmin = (0, createRule_1.createRule)({
|
|
53
120
|
name: 'no-mock-firebase-admin',
|
|
54
121
|
meta: {
|
|
@@ -101,6 +168,10 @@ exports.noMockFirebaseAdmin = (0, createRule_1.createRule)({
|
|
|
101
168
|
if (!bypassesSharedMock(comparisonPathOf(mockPath, filename))) {
|
|
102
169
|
return;
|
|
103
170
|
}
|
|
171
|
+
const factory = node.arguments[1];
|
|
172
|
+
if (factory && exercisesCollectionGroup(factory)) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
104
175
|
context.report({
|
|
105
176
|
node,
|
|
106
177
|
messageId: 'noMockFirebaseAdmin',
|
|
@@ -248,14 +248,50 @@ function declaredTypeNode(node) {
|
|
|
248
248
|
function checksExcessProperties(typeNode) {
|
|
249
249
|
return typeNode !== null && !UNCHECKED_ANNOTATION_TYPES.has(typeNode.type);
|
|
250
250
|
}
|
|
251
|
+
function isFunctionNode(node) {
|
|
252
|
+
return (node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
|
|
253
|
+
node.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
254
|
+
node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression);
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* The function a `return` statement belongs to — the nearest function ancestor,
|
|
258
|
+
* which is what the language binds the return to. Scoping to the NEAREST one
|
|
259
|
+
* keeps a nested callback's literal from inheriting an outer function's
|
|
260
|
+
* annotation.
|
|
261
|
+
*/
|
|
262
|
+
function enclosingFunction(node) {
|
|
263
|
+
let current = node.parent;
|
|
264
|
+
while (current) {
|
|
265
|
+
if (isFunctionNode(current)) {
|
|
266
|
+
return current;
|
|
267
|
+
}
|
|
268
|
+
current = current.parent;
|
|
269
|
+
}
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Reports whether `fn` declares a return type that checks the shape of what it
|
|
274
|
+
* returns. Which type it names is irrelevant — the signal is that the author
|
|
275
|
+
* declared a contract at all, matching how an annotated variable is treated.
|
|
276
|
+
*/
|
|
277
|
+
function declaresCheckedReturnType(fn) {
|
|
278
|
+
return (fn !== null && checksExcessProperties(fn.returnType?.typeAnnotation ?? null));
|
|
279
|
+
}
|
|
251
280
|
/**
|
|
252
281
|
* Reports whether `node` sits inside a value whose shape TypeScript checks
|
|
253
|
-
* against a declared type — a type-annotated variable or class field,
|
|
254
|
-
* `satisfies` clause
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
282
|
+
* against a declared type — a type-annotated variable or class field, a
|
|
283
|
+
* `satisfies` clause, or the return-type annotation of the function that
|
|
284
|
+
* returns it. Excess-property checking makes such a literal unable to carry a
|
|
285
|
+
* member the target type does not declare, so a member name there is dictated
|
|
286
|
+
* by that type rather than chosen by the author, and renaming it would break
|
|
287
|
+
* conformance (#1350). No member resolution is needed: the signal alone is
|
|
288
|
+
* proof, because code carrying an undeclared member does not compile.
|
|
289
|
+
*
|
|
290
|
+
* The return-type form is the only one a RECURSIVE factory can reach (#1511):
|
|
291
|
+
* `return {...} satisfies Q` inside a self-referencing factory does not compile
|
|
292
|
+
* at all (TS7023 — the return type becomes implicitly `any` because the
|
|
293
|
+
* function is referenced in its own return expression), so the annotation is
|
|
294
|
+
* that shape's sole way to declare the contract it imitates.
|
|
259
295
|
*
|
|
260
296
|
* The walk climbs object/array containers so an outer signal covers nested
|
|
261
297
|
* members, and stops at anything else — notably `as` assertions, which do not
|
|
@@ -278,6 +314,14 @@ function hasConformanceSignal(node) {
|
|
|
278
314
|
case utils_1.AST_NODE_TYPES.PropertyDefinition:
|
|
279
315
|
return (parent.value === current &&
|
|
280
316
|
checksExcessProperties(declaredTypeNode(parent)));
|
|
317
|
+
case utils_1.AST_NODE_TYPES.ReturnStatement:
|
|
318
|
+
// Only the returned value is covered; a literal elsewhere in the body
|
|
319
|
+
// of an annotated function is unrelated to its declared return type.
|
|
320
|
+
return (parent.argument === current &&
|
|
321
|
+
declaresCheckedReturnType(enclosingFunction(parent)));
|
|
322
|
+
case utils_1.AST_NODE_TYPES.ArrowFunctionExpression:
|
|
323
|
+
// A concise arrow body is the returned value.
|
|
324
|
+
return parent.body === current && declaresCheckedReturnType(parent);
|
|
281
325
|
case utils_1.AST_NODE_TYPES.Property:
|
|
282
326
|
case utils_1.AST_NODE_TYPES.ObjectExpression:
|
|
283
327
|
case utils_1.AST_NODE_TYPES.ArrayExpression:
|
|
@@ -505,7 +505,19 @@ exports.preferNullishCoalescingBooleanProps = (0, createRule_1.createRule)({
|
|
|
505
505
|
},
|
|
506
506
|
defaultOptions: [],
|
|
507
507
|
create(context) {
|
|
508
|
-
|
|
508
|
+
// getParserServices throws whenever the parser is not @typescript-eslint/parser
|
|
509
|
+
// (espree for .js, jsonc-eslint-parser for package.json). A throw here fails the
|
|
510
|
+
// entire lint run for that file, not just this rule, so mirror the same
|
|
511
|
+
// precondition it checks and degrade to the syntactic analysis instead. The
|
|
512
|
+
// `allowWithoutFullTypeInformation` flag only covers a TS parser lacking
|
|
513
|
+
// `parserOptions.project`; it does not cover a non-TS parser.
|
|
514
|
+
const services = context.parserServices;
|
|
515
|
+
const hasTypeServices = !!services?.program &&
|
|
516
|
+
!!services.esTreeNodeToTSNodeMap &&
|
|
517
|
+
!!services.tsNodeToESTreeNodeMap;
|
|
518
|
+
const parserServices = hasTypeServices
|
|
519
|
+
? utils_1.ESLintUtils.getParserServices(context, true)
|
|
520
|
+
: undefined;
|
|
509
521
|
const checker = parserServices?.program?.getTypeChecker();
|
|
510
522
|
return {
|
|
511
523
|
LogicalExpression(node) {
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,56 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.20.48",
|
|
4
|
+
"date": "2026-07-31T09:14:53.303Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "enforce-props-naming-consistency",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
1514
|
|
11
|
+
],
|
|
12
|
+
"summary": "stop reporting constructor parameters twice (closes #1514)"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "prefer-nullish-coalescing-boolean-props",
|
|
16
|
+
"changeType": "fix",
|
|
17
|
+
"issues": [
|
|
18
|
+
1513
|
|
19
|
+
],
|
|
20
|
+
"summary": "degrade instead of throwing under a non-TypeScript parser (closes #1513)"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"version": "1.20.47",
|
|
26
|
+
"date": "2026-07-31T08:38:59.580Z",
|
|
27
|
+
"rules": [
|
|
28
|
+
{
|
|
29
|
+
"name": "no-explicit-return-type",
|
|
30
|
+
"changeType": "fix",
|
|
31
|
+
"issues": [
|
|
32
|
+
1512
|
|
33
|
+
],
|
|
34
|
+
"summary": "stop reporting an annotation TypeScript requires (closes #1512)"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"name": "no-mock-firebase-admin",
|
|
38
|
+
"changeType": "fix",
|
|
39
|
+
"issues": [
|
|
40
|
+
1510
|
|
41
|
+
],
|
|
42
|
+
"summary": "stop reporting factories the shared fake cannot replace (closes #1510)"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"name": "no-unnecessary-verb-suffix",
|
|
46
|
+
"changeType": "fix",
|
|
47
|
+
"issues": [
|
|
48
|
+
1511
|
|
49
|
+
],
|
|
50
|
+
"summary": "accept a function's own return type as a conformance signal (closes #1511)"
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
},
|
|
2
54
|
{
|
|
3
55
|
"version": "1.20.46",
|
|
4
56
|
"date": "2026-07-31T07:08:55.415Z",
|