@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 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.46',
226
+ version: '1.20.48',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -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 && isRecursiveFunction(node))) {
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 && isRecursiveFunction(node))) {
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, or a
254
- * `satisfies` clause. Excess-property checking makes such a literal unable to
255
- * carry a member the target type does not declare, so a member name there is
256
- * dictated by that type rather than chosen by the author, and renaming it would
257
- * break conformance (#1350). No member resolution is needed: the signal alone
258
- * is proof, because code carrying an undeclared member does not compile.
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
- const parserServices = utils_1.ESLintUtils.getParserServices(context, true);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.20.46",
3
+ "version": "1.20.48",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -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",