@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 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.116',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -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
- ((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')) &&
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
- 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;
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
- 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') {
@@ -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
- if (current.type === utils_1.AST_NODE_TYPES.Program) {
268
- for (const statement of current.body) {
269
- if (statement.type === utils_1.AST_NODE_TYPES.VariableDeclaration) {
270
- for (const decl of statement.declarations) {
271
- if (decl.id.type === utils_1.AST_NODE_TYPES.Identifier &&
272
- decl.id.name === varName) {
273
- return decl;
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
- // For array methods, report on the first occurrence
407
- else if (loopInfo.isArrayMethod) {
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 scope = context.getScope();
42
- const variable = scope.variables.find((v) => v.name === typeName.name);
43
- if (!variable || !variable.defs[0]?.node) {
44
- // If we can't find the type definition, assume it's a type with required properties
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.type === utils_1.AST_NODE_TYPES.TSTypeReference) {
117
- const typeName = typeAnnotation.typeName;
118
- if (typeName.type === utils_1.AST_NODE_TYPES.Identifier) {
119
- const scope = context.getScope();
120
- const variable = scope.variables.find((v) => v.name === typeName.name);
121
- if (variable && variable.defs[0]?.node) {
122
- const def = variable.defs[0].node;
123
- if (def.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration) {
124
- if (hasAllOptionalProperties(def.typeAnnotation)) {
125
- context.report({
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.type === utils_1.AST_NODE_TYPES.TSAsExpression ||
179
- value.type ===
180
- utils_1.AST_NODE_TYPES
181
- .TSSatisfiesExpression) {
182
- // The cast may wrap a new Date() OR an object literal we need to recurse into
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 (isNewDate(inner)) {
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
  }
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.20.115",
3
+ "version": "1.20.116",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -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",