@blumintinc/eslint-plugin-blumint 1.20.0 → 1.20.2

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.0',
226
+ version: '1.20.2',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -296,7 +296,7 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
296
296
  if (lowerCallee.startsWith('assert')) {
297
297
  return identifierReturnsBoolean(calleeName);
298
298
  }
299
- return isPrefixedByBooleanKeyword(calleeName, approvedPrefixesWithoutAsserts);
299
+ return calleeNameImpliesBoolean(calleeName, approvedPrefixesWithoutAsserts);
300
300
  }
301
301
  // Default to false for other cases with || to avoid false positives
302
302
  return false;
@@ -316,7 +316,7 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
316
316
  return identifierReturnsBoolean(calleeName);
317
317
  }
318
318
  // Check if the function name suggests it returns a boolean
319
- return isPrefixedByBooleanKeyword(calleeName, approvedPrefixes.filter((p) => p !== 'asserts'));
319
+ return calleeNameImpliesBoolean(calleeName, approvedPrefixesWithoutAsserts);
320
320
  }
321
321
  }
322
322
  return false;
@@ -418,7 +418,12 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
418
418
  ];
419
419
  const matchesPrefix = isPrefixedByBooleanKeyword(calleeName, calleeBooleanPrefixes);
420
420
  if (matchesPrefix) {
421
- return 'boolean';
421
+ // The callee's declaration outranks its name: a resolvable predicate
422
+ // that demonstrably returns a verdict object, a string or a Promise
423
+ // yields a non-boolean value no matter how it is named.
424
+ return calleeReturnEvaluation(calleeName) === 'nonBoolean'
425
+ ? 'nonBoolean'
426
+ : 'boolean';
422
427
  }
423
428
  if (lowerCallee.startsWith('get') ||
424
429
  lowerCallee.startsWith('fetch') ||
@@ -565,6 +570,238 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
565
570
  }
566
571
  return false;
567
572
  }
573
+ // Predicates that call themselves (or each other) would otherwise recurse
574
+ // forever while their returns are classified.
575
+ const calleesUnderEvaluation = new Set();
576
+ const NON_BOOLEAN_TYPE_KEYWORDS = new Set([
577
+ utils_1.AST_NODE_TYPES.TSStringKeyword,
578
+ utils_1.AST_NODE_TYPES.TSNumberKeyword,
579
+ utils_1.AST_NODE_TYPES.TSBigIntKeyword,
580
+ utils_1.AST_NODE_TYPES.TSSymbolKeyword,
581
+ utils_1.AST_NODE_TYPES.TSObjectKeyword,
582
+ utils_1.AST_NODE_TYPES.TSVoidKeyword,
583
+ utils_1.AST_NODE_TYPES.TSNullKeyword,
584
+ utils_1.AST_NODE_TYPES.TSUndefinedKeyword,
585
+ utils_1.AST_NODE_TYPES.TSTypeLiteral,
586
+ utils_1.AST_NODE_TYPES.TSArrayType,
587
+ utils_1.AST_NODE_TYPES.TSTupleType,
588
+ utils_1.AST_NODE_TYPES.TSFunctionType,
589
+ utils_1.AST_NODE_TYPES.TSConstructorType,
590
+ utils_1.AST_NODE_TYPES.TSIntersectionType,
591
+ utils_1.AST_NODE_TYPES.TSTemplateLiteralType,
592
+ utils_1.AST_NODE_TYPES.TSMappedType,
593
+ ]);
594
+ const NON_BOOLEAN_EXPRESSION_TYPES = new Set([
595
+ utils_1.AST_NODE_TYPES.ObjectExpression,
596
+ utils_1.AST_NODE_TYPES.ArrayExpression,
597
+ utils_1.AST_NODE_TYPES.TemplateLiteral,
598
+ utils_1.AST_NODE_TYPES.TaggedTemplateExpression,
599
+ utils_1.AST_NODE_TYPES.ArrowFunctionExpression,
600
+ utils_1.AST_NODE_TYPES.FunctionExpression,
601
+ utils_1.AST_NODE_TYPES.ClassExpression,
602
+ utils_1.AST_NODE_TYPES.NewExpression,
603
+ utils_1.AST_NODE_TYPES.UpdateExpression,
604
+ utils_1.AST_NODE_TYPES.JSXElement,
605
+ utils_1.AST_NODE_TYPES.JSXFragment,
606
+ ]);
607
+ // `as const` preserves the operand's shape, so the assertion says nothing
608
+ // about booleanness and the operand must be classified instead.
609
+ function isConstAssertion(typeNode) {
610
+ return (typeNode.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
611
+ typeNode.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
612
+ typeNode.typeName.name === 'const');
613
+ }
614
+ function classifyTypeAnnotation(typeNode) {
615
+ if (typeNode.type === utils_1.AST_NODE_TYPES.TSBooleanKeyword ||
616
+ typeNode.type === utils_1.AST_NODE_TYPES.TSTypePredicate) {
617
+ return 'boolean';
618
+ }
619
+ if (typeNode.type === utils_1.AST_NODE_TYPES.TSLiteralType) {
620
+ const literal = typeNode.literal;
621
+ return literal.type === utils_1.AST_NODE_TYPES.Literal &&
622
+ typeof literal.value === 'boolean'
623
+ ? 'boolean'
624
+ : 'nonBoolean';
625
+ }
626
+ if (typeNode.type === utils_1.AST_NODE_TYPES.TSUnionType) {
627
+ const members = typeNode.types.map(classifyTypeAnnotation);
628
+ if (members.every((member) => member === 'boolean')) {
629
+ return 'boolean';
630
+ }
631
+ // A union that mixes boolean with anything else cannot be trusted to
632
+ // hold a boolean; the repository prefers false negatives here.
633
+ return members.some((member) => member === 'nonBoolean')
634
+ ? 'nonBoolean'
635
+ : 'indeterminate';
636
+ }
637
+ if (typeNode.type === utils_1.AST_NODE_TYPES.TSTypeReference) {
638
+ if (typeNode.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
639
+ typeNode.typeName.name === 'Boolean') {
640
+ return 'boolean';
641
+ }
642
+ // Named types (including `Promise<boolean>`, whose call site yields a
643
+ // promise rather than a boolean) are treated as non-boolean values.
644
+ return 'nonBoolean';
645
+ }
646
+ if (NON_BOOLEAN_TYPE_KEYWORDS.has(typeNode.type)) {
647
+ return 'nonBoolean';
648
+ }
649
+ // `any`, `unknown`, generics, conditional and indexed-access types carry
650
+ // no reliable syntactic verdict.
651
+ return 'indeterminate';
652
+ }
653
+ function classifyReturnExpression(expression) {
654
+ // A bare `return;` (or a fall-through) produces undefined.
655
+ if (!expression)
656
+ return 'nonBoolean';
657
+ if (expression.type === utils_1.AST_NODE_TYPES.TSAsExpression ||
658
+ expression.type === utils_1.AST_NODE_TYPES.TSSatisfiesExpression ||
659
+ expression.type === utils_1.AST_NODE_TYPES.TSTypeAssertion) {
660
+ const annotation = expression.typeAnnotation;
661
+ if (annotation && !isConstAssertion(annotation)) {
662
+ const classified = classifyTypeAnnotation(annotation);
663
+ if (classified !== 'indeterminate') {
664
+ return classified;
665
+ }
666
+ }
667
+ return classifyReturnExpression(expression.expression);
668
+ }
669
+ if (expression.type === utils_1.AST_NODE_TYPES.ChainExpression ||
670
+ expression.type === utils_1.AST_NODE_TYPES.TSNonNullExpression) {
671
+ return classifyReturnExpression(expression.expression);
672
+ }
673
+ if (expression.type === utils_1.AST_NODE_TYPES.SequenceExpression) {
674
+ return classifyReturnExpression(expression.expressions[expression.expressions.length - 1]);
675
+ }
676
+ if (NON_BOOLEAN_EXPRESSION_TYPES.has(expression.type)) {
677
+ return 'nonBoolean';
678
+ }
679
+ if (expression.type === utils_1.AST_NODE_TYPES.Literal) {
680
+ return typeof expression.value === 'boolean' ? 'boolean' : 'nonBoolean';
681
+ }
682
+ if (expression.type === utils_1.AST_NODE_TYPES.Identifier &&
683
+ expression.name === 'undefined') {
684
+ return 'nonBoolean';
685
+ }
686
+ if (expression.type === utils_1.AST_NODE_TYPES.UnaryExpression) {
687
+ if (expression.operator === '!' || expression.operator === 'delete') {
688
+ return 'boolean';
689
+ }
690
+ return 'nonBoolean';
691
+ }
692
+ if (expression.type === utils_1.AST_NODE_TYPES.BinaryExpression &&
693
+ !BOOLEANISH_BINARY_OPERATORS.has(expression.operator)) {
694
+ return 'nonBoolean';
695
+ }
696
+ // Branching expressions are classified from their branches so a mix of a
697
+ // boolean and an object (or two objects) is recognized as non-boolean.
698
+ if (expression.type === utils_1.AST_NODE_TYPES.ConditionalExpression) {
699
+ return combineClassifications([
700
+ classifyReturnExpression(expression.consequent),
701
+ classifyReturnExpression(expression.alternate),
702
+ ]);
703
+ }
704
+ if (expression.type === utils_1.AST_NODE_TYPES.LogicalExpression) {
705
+ return combineClassifications([
706
+ classifyReturnExpression(expression.left),
707
+ classifyReturnExpression(expression.right),
708
+ ]);
709
+ }
710
+ const evaluation = evaluateBooleanishExpression(expression);
711
+ if (evaluation === 'boolean')
712
+ return 'boolean';
713
+ if (evaluation === 'nonBoolean')
714
+ return 'nonBoolean';
715
+ return 'indeterminate';
716
+ }
717
+ function combineClassifications(classifications) {
718
+ if (classifications.length === 0)
719
+ return 'indeterminate';
720
+ if (classifications.every((entry) => entry === 'boolean')) {
721
+ return 'boolean';
722
+ }
723
+ return classifications.some((entry) => entry === 'nonBoolean')
724
+ ? 'nonBoolean'
725
+ : 'indeterminate';
726
+ }
727
+ function classifyFunctionReturn(functionLike) {
728
+ const annotation = functionLike.returnType?.typeAnnotation;
729
+ if (annotation) {
730
+ const classified = classifyTypeAnnotation(annotation);
731
+ if (classified !== 'indeterminate') {
732
+ return classified;
733
+ }
734
+ }
735
+ // Calling an async or generator function hands back a promise or an
736
+ // iterator, never the boolean produced inside the body.
737
+ if (functionLike.async || functionLike.generator) {
738
+ return 'nonBoolean';
739
+ }
740
+ if (functionLike.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression &&
741
+ functionLike.expression) {
742
+ return classifyReturnExpression(functionLike.body);
743
+ }
744
+ // Overload signatures and ambient declarations expose no body to inspect.
745
+ if (functionLike.body?.type !== utils_1.AST_NODE_TYPES.BlockStatement) {
746
+ return 'indeterminate';
747
+ }
748
+ const returnArguments = collectReturnArguments(functionLike.body);
749
+ if (returnArguments.length === 0) {
750
+ return 'nonBoolean';
751
+ }
752
+ return combineClassifications(returnArguments.map(classifyReturnExpression));
753
+ }
754
+ function functionOfDefinition(definition) {
755
+ if (definition.type === 'FunctionName') {
756
+ return definition.node;
757
+ }
758
+ if (definition.type === 'Variable') {
759
+ const init = definition.node
760
+ .init;
761
+ if (init?.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
762
+ init?.type === utils_1.AST_NODE_TYPES.FunctionExpression) {
763
+ return init;
764
+ }
765
+ }
766
+ return undefined;
767
+ }
768
+ /**
769
+ * Resolve a callee identifier through the scope chain and classify the value
770
+ * its calls produce, without a type checker.
771
+ */
772
+ function calleeReturnEvaluation(name) {
773
+ if (calleesUnderEvaluation.has(name))
774
+ return 'indeterminate';
775
+ const variable = findVariableInScopes(name);
776
+ if (!variable || variable.defs.length === 0)
777
+ return 'indeterminate';
778
+ calleesUnderEvaluation.add(name);
779
+ try {
780
+ const classifications = [];
781
+ for (const definition of variable.defs) {
782
+ const functionLike = functionOfDefinition(definition);
783
+ // Imports, parameters and aliases hide the implementation, so the
784
+ // name heuristic must keep its reach across module boundaries.
785
+ if (!functionLike)
786
+ return 'indeterminate';
787
+ classifications.push(classifyFunctionReturn(functionLike));
788
+ }
789
+ return combineClassifications(classifications);
790
+ }
791
+ finally {
792
+ calleesUnderEvaluation.delete(name);
793
+ }
794
+ }
795
+ /**
796
+ * Decide whether a call to `calleeName` yields a boolean, giving the callee's
797
+ * resolvable declaration the final say over its name.
798
+ */
799
+ function calleeNameImpliesBoolean(calleeName, prefixes) {
800
+ if (!isPrefixedByBooleanKeyword(calleeName, prefixes)) {
801
+ return false;
802
+ }
803
+ return calleeReturnEvaluation(calleeName) !== 'nonBoolean';
804
+ }
568
805
  /**
569
806
  * Check if a variable is used in a while loop condition and is likely a DOM element or tree node
570
807
  * This helps identify variables like 'parent', 'element', 'node', etc. that are used
@@ -6,6 +6,23 @@ const createRule_1 = require("../utils/createRule");
6
6
  const FIRESTORE_METHODS = new Set(['get', 'set', 'update', 'delete']);
7
7
  const COLLECTION_CONSTRUCTORS = new Set(['Set', 'Map', 'WeakSet', 'WeakMap']);
8
8
  const KNOWN_FIRESTORE_ROOTS = new Set(['db', 'firestore']);
9
+ // The modular client SDK. DocSetter/DocSetterTransaction wrap
10
+ // firebase-admin/firestore and cannot be imported from frontend code, so a
11
+ // client-SDK batch or transaction has no facade to route through and must not
12
+ // be reported.
13
+ const CLIENT_SDK_MODULES = new Set([
14
+ 'firebase/firestore',
15
+ '@firebase/firestore',
16
+ 'firebase/firestore/lite',
17
+ ]);
18
+ // firebase-admin/firestore exposes no `writeBatch` export at all (the admin
19
+ // API is `db.batch()`), so this spelling identifies the client SDK with
20
+ // certainty even when the binding's origin cannot be traced.
21
+ const CLIENT_BATCH_FACTORY = 'writeBatch';
22
+ // Shared spelling: the client SDK exports a free `runTransaction(db, cb)`
23
+ // while the admin SDK only exposes the `db.runTransaction(cb)` method, so the
24
+ // callee shape, not the name, decides which SDK a transaction belongs to.
25
+ const TRANSACTION_RUNNER = 'runTransaction';
9
26
  const isMemberExpression = (node) => node.type === utils_1.AST_NODE_TYPES.MemberExpression;
10
27
  const isCallExpression = (node) => node.type === utils_1.AST_NODE_TYPES.CallExpression;
11
28
  const isIdentifier = (node) => node.type === utils_1.AST_NODE_TYPES.Identifier;
@@ -94,6 +111,16 @@ exports.enforceFirestoreFacade = (0, createRule_1.createRule)({
94
111
  const firestoreTransactionVariables = new Set();
95
112
  const docSetterVariables = new Set();
96
113
  const batchManagerVariables = new Set();
114
+ // local name -> imported name, for bindings that come from the modular
115
+ // client SDK (covers `import { writeBatch as wb }` aliases).
116
+ const clientSdkImportedLocals = new Map();
117
+ // Namespace bindings for the client SDK (`import * as fs`).
118
+ const clientSdkNamespaceLocals = new Set();
119
+ // Bindings that demonstrably come from some other module, so a local
120
+ // helper named `writeBatch` is not mistaken for the SDK export.
121
+ const nonClientImportedLocals = new Set();
122
+ // Batches/transactions proven to originate from the client SDK.
123
+ const clientFirestoreVariables = new Set();
97
124
  const sourceCode = context.sourceCode;
98
125
  const clearFirestoreTrackingFor = (name) => {
99
126
  firestoreDocRefVariables.delete(name);
@@ -102,7 +129,36 @@ exports.enforceFirestoreFacade = (0, createRule_1.createRule)({
102
129
  firestoreTransactionVariables.delete(name);
103
130
  docSetterVariables.delete(name);
104
131
  batchManagerVariables.delete(name);
132
+ clientFirestoreVariables.delete(name);
105
133
  };
134
+ const isClientSdkExportReference = (node, exportName, allowUntracedSpelling) => {
135
+ const callee = unwrapTSAsExpression(node);
136
+ if (isIdentifier(callee)) {
137
+ if (clientSdkImportedLocals.get(callee.name) === exportName) {
138
+ return true;
139
+ }
140
+ return (allowUntracedSpelling &&
141
+ callee.name === exportName &&
142
+ !nonClientImportedLocals.has(callee.name));
143
+ }
144
+ if (isMemberExpression(callee) &&
145
+ isIdentifier(callee.property) &&
146
+ callee.property.name === exportName) {
147
+ const base = getLeftmostIdentifier(callee.object);
148
+ return !!base && clientSdkNamespaceLocals.has(base.name);
149
+ }
150
+ return false;
151
+ };
152
+ // `writeBatch(firestore)` / `fs.writeBatch(firestore)` — client SDK only.
153
+ const isClientBatchFactoryCall = (node) => {
154
+ const candidate = unwrapTSAsExpression(node);
155
+ return (isCallExpression(candidate) &&
156
+ isClientSdkExportReference(candidate.callee, CLIENT_BATCH_FACTORY, true));
157
+ };
158
+ // `runTransaction(firestore, cb)` — the free-function form only exists in
159
+ // the client SDK. The name alone is not enough, because a project helper
160
+ // could share it, so the binding must be traced back to the SDK.
161
+ const isClientTransactionRunnerCall = (node) => isClientSdkExportReference(node.callee, TRANSACTION_RUNNER, false);
106
162
  const recordFirestoreVariable = (varName, expression) => {
107
163
  const target = unwrapTSAsExpression(expression);
108
164
  if (target.type === utils_1.AST_NODE_TYPES.ConditionalExpression) {
@@ -131,6 +187,10 @@ exports.enforceFirestoreFacade = (0, createRule_1.createRule)({
131
187
  batchManagerVariables.add(varName);
132
188
  return true;
133
189
  }
190
+ if (isClientBatchFactoryCall(target)) {
191
+ clientFirestoreVariables.add(varName);
192
+ return true;
193
+ }
134
194
  if (isFirestoreDocumentReference(target)) {
135
195
  firestoreDocRefVariables.add(varName);
136
196
  return true;
@@ -143,8 +203,7 @@ exports.enforceFirestoreFacade = (0, createRule_1.createRule)({
143
203
  isMemberExpression(target.callee) &&
144
204
  isIdentifier(target.callee.property) &&
145
205
  target.callee.property.name === 'batch' &&
146
- isIdentifier(target.callee.object) &&
147
- target.callee.object.name === 'db') {
206
+ isFirestoreRoot(target.callee.object, firestoreCollectionVariables, firestoreDocRefVariables)) {
148
207
  firestoreBatchVariables.add(varName);
149
208
  return true;
150
209
  }
@@ -214,6 +273,110 @@ exports.enforceFirestoreFacade = (0, createRule_1.createRule)({
214
273
  clearFirestoreTrackingFor(varName);
215
274
  recordFirestoreVariable(varName, right);
216
275
  };
276
+ const recordImportBinding = (localName, importedName, isClientSource) => {
277
+ if (!isClientSource) {
278
+ nonClientImportedLocals.add(localName);
279
+ return;
280
+ }
281
+ if (importedName === null) {
282
+ clientSdkNamespaceLocals.add(localName);
283
+ return;
284
+ }
285
+ clientSdkImportedLocals.set(localName, importedName);
286
+ };
287
+ const recordImportPattern = (pattern, source) => {
288
+ const isClientSource = CLIENT_SDK_MODULES.has(source);
289
+ if (pattern.type === utils_1.AST_NODE_TYPES.ObjectPattern) {
290
+ for (const property of pattern.properties) {
291
+ if (property.type !== utils_1.AST_NODE_TYPES.Property)
292
+ continue;
293
+ if (!isIdentifier(property.key) || !isIdentifier(property.value)) {
294
+ continue;
295
+ }
296
+ recordImportBinding(property.value.name, property.key.name, isClientSource);
297
+ }
298
+ return;
299
+ }
300
+ if (isIdentifier(pattern)) {
301
+ recordImportBinding(pattern.name, null, isClientSource);
302
+ }
303
+ };
304
+ const getImportExpressionSource = (node) => {
305
+ const expression = unwrapTSAsExpression(node);
306
+ if (expression.type !== utils_1.AST_NODE_TYPES.ImportExpression)
307
+ return null;
308
+ const source = expression.source;
309
+ return source.type === utils_1.AST_NODE_TYPES.Literal &&
310
+ typeof source.value === 'string'
311
+ ? source.value
312
+ : null;
313
+ };
314
+ const isPromiseAllCall = (node) => isCallExpression(node) &&
315
+ isMemberExpression(node.callee) &&
316
+ isIdentifier(node.callee.object) &&
317
+ node.callee.object.name === 'Promise' &&
318
+ isIdentifier(node.callee.property) &&
319
+ node.callee.property.name === 'all';
320
+ // Frontend Firebase access is required to be dynamically imported, so
321
+ // `await import(...)` — including the correlated `Promise.all` array form —
322
+ // is the ordinary client-SDK call site rather than an edge case.
323
+ const recordDynamicImportBindings = (node) => {
324
+ const init = node.init;
325
+ if (!init || init.type !== utils_1.AST_NODE_TYPES.AwaitExpression)
326
+ return;
327
+ const awaited = unwrapTSAsExpression(init.argument);
328
+ const directSource = getImportExpressionSource(awaited);
329
+ if (directSource !== null) {
330
+ recordImportPattern(node.id, directSource);
331
+ return;
332
+ }
333
+ if (!isPromiseAllCall(awaited) ||
334
+ node.id.type !== utils_1.AST_NODE_TYPES.ArrayPattern) {
335
+ return;
336
+ }
337
+ const promises = awaited.arguments[0];
338
+ if (!promises || promises.type !== utils_1.AST_NODE_TYPES.ArrayExpression)
339
+ return;
340
+ node.id.elements.forEach((element, index) => {
341
+ const promise = promises.elements[index];
342
+ if (!element || !promise)
343
+ return;
344
+ const source = getImportExpressionSource(promise);
345
+ if (source === null)
346
+ return;
347
+ recordImportPattern(element, source);
348
+ });
349
+ };
350
+ const getCallbackFirstParam = (node) => {
351
+ const callback = node.arguments.find((argument) => argument.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
352
+ argument.type === utils_1.AST_NODE_TYPES.FunctionExpression);
353
+ const param = callback?.params[0];
354
+ return param && isIdentifier(param) ? param : null;
355
+ };
356
+ // Binds the transaction callback parameter to the SDK that produced it, so
357
+ // classification rests on origin rather than on the parameter being
358
+ // spelled `transaction`.
359
+ const recordTransactionCallback = (node) => {
360
+ if (isClientTransactionRunnerCall(node)) {
361
+ const param = getCallbackFirstParam(node);
362
+ if (param) {
363
+ clearFirestoreTrackingFor(param.name);
364
+ clientFirestoreVariables.add(param.name);
365
+ }
366
+ return;
367
+ }
368
+ const callee = node.callee;
369
+ if (!isMemberExpression(callee) ||
370
+ !isIdentifier(callee.property) ||
371
+ callee.property.name !== TRANSACTION_RUNNER ||
372
+ !isFirestoreRoot(callee.object, firestoreCollectionVariables, firestoreDocRefVariables)) {
373
+ return;
374
+ }
375
+ const param = getCallbackFirstParam(node);
376
+ if (param && !clientFirestoreVariables.has(param.name)) {
377
+ firestoreTransactionVariables.add(param.name);
378
+ }
379
+ };
217
380
  const isRealtimeDbRefAssignment = (node) => {
218
381
  if (node.type !== utils_1.AST_NODE_TYPES.VariableDeclarator)
219
382
  return false;
@@ -282,7 +445,8 @@ exports.enforceFirestoreFacade = (0, createRule_1.createRule)({
282
445
  const object = node.callee.object;
283
446
  if (isIdentifier(object)) {
284
447
  const name = object.name;
285
- if (docSetterVariables.has(name) ||
448
+ if (clientFirestoreVariables.has(name) ||
449
+ docSetterVariables.has(name) ||
286
450
  batchManagerVariables.has(name) ||
287
451
  realtimeDbRefVariables.has(name) ||
288
452
  realtimeDbChildVariables.has(name) ||
@@ -304,7 +468,7 @@ exports.enforceFirestoreFacade = (0, createRule_1.createRule)({
304
468
  }
305
469
  return false;
306
470
  }
307
- if (isRealtimeDbReference(object)) {
471
+ if (isRealtimeDbReference(object) || isClientBatchFactoryCall(object)) {
308
472
  return false;
309
473
  }
310
474
  if (isFirestoreDocumentReference(object)) {
@@ -374,7 +538,19 @@ exports.enforceFirestoreFacade = (0, createRule_1.createRule)({
374
538
  });
375
539
  };
376
540
  return {
541
+ ImportDeclaration(node) {
542
+ const source = node.source.value;
543
+ const isClientSource = typeof source === 'string' && CLIENT_SDK_MODULES.has(source);
544
+ for (const specifier of node.specifiers) {
545
+ if (specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier) {
546
+ recordImportBinding(specifier.local.name, specifier.imported.name, isClientSource);
547
+ continue;
548
+ }
549
+ recordImportBinding(specifier.local.name, null, isClientSource);
550
+ }
551
+ },
377
552
  VariableDeclarator(node) {
553
+ recordDynamicImportBindings(node);
378
554
  isRealtimeDbRefAssignment(node);
379
555
  isCollectionObjectAssignment(node);
380
556
  isFirestoreAssignment(node);
@@ -383,6 +559,7 @@ exports.enforceFirestoreFacade = (0, createRule_1.createRule)({
383
559
  handleAssignmentExpression(node);
384
560
  },
385
561
  CallExpression(node) {
562
+ recordTransactionCallback(node);
386
563
  if (!isFirestoreMethodCall(node))
387
564
  return;
388
565
  const callee = node.callee;