@blumintinc/eslint-plugin-blumint 1.20.64 → 1.20.65

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.64',
226
+ version: '1.20.65',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -565,6 +565,131 @@ exports.enforceFirestoreDocRefGeneric = (0, createRule_1.createRule)({
565
565
  }
566
566
  return false;
567
567
  }
568
+ /**
569
+ * `@firebase/rules-unit-testing` hands back the compat (v8) Firestore, whose
570
+ * `.doc()`, `.collection()` and `.collectionGroup()` declare zero type
571
+ * parameters. Asking for a schema generic there produces
572
+ * `TS2558: Expected 0 type arguments, but got 1`, so every remediation this
573
+ * rule suggests is uncompilable on that surface and the receiver must be
574
+ * exempt.
575
+ */
576
+ const RULES_UNIT_TESTING_MODULE = '@firebase/rules-unit-testing';
577
+ let rulesUnitTestingLocals;
578
+ /**
579
+ * Scanned lazily rather than from an `ImportDeclaration` visitor so that
580
+ * traversal order can never decide whether the exemption applies. Type-only
581
+ * specifiers count because a callback parameter annotated `RulesTestContext`
582
+ * is the other way a compat handle reaches a `.doc()` receiver.
583
+ */
584
+ function getRulesUnitTestingLocals() {
585
+ if (rulesUnitTestingLocals) {
586
+ return rulesUnitTestingLocals;
587
+ }
588
+ const locals = new Set();
589
+ for (const statement of context.sourceCode.ast.body) {
590
+ if (statement.type === utils_1.AST_NODE_TYPES.ImportDeclaration &&
591
+ statement.source.value === RULES_UNIT_TESTING_MODULE) {
592
+ for (const specifier of statement.specifiers) {
593
+ locals.add(specifier.local.name);
594
+ }
595
+ }
596
+ }
597
+ rulesUnitTestingLocals = locals;
598
+ return locals;
599
+ }
600
+ /**
601
+ * A qualified annotation such as `rut.RulesTestContext` is rooted at the
602
+ * namespace binding, which is the name the import declaration provides.
603
+ */
604
+ function rootTypeNameOf(typeNode) {
605
+ if (typeNode.type !== utils_1.AST_NODE_TYPES.TSTypeReference) {
606
+ return undefined;
607
+ }
608
+ let entity = typeNode.typeName;
609
+ while (entity.type === utils_1.AST_NODE_TYPES.TSQualifiedName) {
610
+ entity = entity.left;
611
+ }
612
+ return entity.type === utils_1.AST_NODE_TYPES.Identifier
613
+ ? entity.name
614
+ : undefined;
615
+ }
616
+ function isRulesUnitTestingType(typeNode) {
617
+ if (typeNode.type === utils_1.AST_NODE_TYPES.TSUnionType ||
618
+ typeNode.type === utils_1.AST_NODE_TYPES.TSIntersectionType) {
619
+ return typeNode.types.some(isRulesUnitTestingType);
620
+ }
621
+ const rootName = rootTypeNameOf(typeNode);
622
+ return !!rootName && getRulesUnitTestingLocals().has(rootName);
623
+ }
624
+ /**
625
+ * Walks an expression toward its syntactic root and reports whether that
626
+ * root is a value supplied by `@firebase/rules-unit-testing`.
627
+ *
628
+ * Only `const` bindings are followed, mirroring `isTypedCollectionBinding`:
629
+ * a `let`/`var` receiver can be reassigned to an Admin SDK handle, where the
630
+ * generic is both supportable and valuable, so exempting it would silently
631
+ * drop enforcement.
632
+ */
633
+ function tracesToRulesUnitTesting(node, visited = new Set()) {
634
+ if (!node || getRulesUnitTestingLocals().size === 0) {
635
+ return false;
636
+ }
637
+ // Guards against a self-referential declaration such as `const a = a.b;`.
638
+ if (visited.has(node)) {
639
+ return false;
640
+ }
641
+ visited.add(node);
642
+ switch (node.type) {
643
+ case utils_1.AST_NODE_TYPES.AwaitExpression:
644
+ return tracesToRulesUnitTesting(node.argument, visited);
645
+ case utils_1.AST_NODE_TYPES.CallExpression:
646
+ return tracesToRulesUnitTesting(node.callee, visited);
647
+ case utils_1.AST_NODE_TYPES.MemberExpression:
648
+ return tracesToRulesUnitTesting(node.object, visited);
649
+ case utils_1.AST_NODE_TYPES.ChainExpression:
650
+ return tracesToRulesUnitTesting(node.expression, visited);
651
+ case utils_1.AST_NODE_TYPES.TSNonNullExpression:
652
+ case utils_1.AST_NODE_TYPES.TSAsExpression:
653
+ case utils_1.AST_NODE_TYPES.TSSatisfiesExpression:
654
+ case utils_1.AST_NODE_TYPES.TSTypeAssertion:
655
+ return tracesToRulesUnitTesting(node.expression, visited);
656
+ case utils_1.AST_NODE_TYPES.Identifier:
657
+ return identifierTracesToRulesUnitTesting(node, visited);
658
+ default:
659
+ return false;
660
+ }
661
+ }
662
+ function identifierTracesToRulesUnitTesting(node, visited) {
663
+ const scope = ASTHelpers_1.ASTHelpers.getScope(context, node);
664
+ const variable = ASTHelpers_1.ASTHelpers.findVariableInScope(scope, node.name);
665
+ if (!variable || variable.defs.length !== 1) {
666
+ return false;
667
+ }
668
+ const def = variable.defs[0];
669
+ if (def.type === 'ImportBinding') {
670
+ return (def.parent?.type === utils_1.AST_NODE_TYPES.ImportDeclaration &&
671
+ def.parent.source.value === RULES_UNIT_TESTING_MODULE);
672
+ }
673
+ // Covers the `withSecurityRulesDisabled(async (ctx: RulesTestContext) =>
674
+ // ...)` callback, where the compat handle never appears as an initializer.
675
+ if (def.type === 'Parameter') {
676
+ const annotation = def.name.typeAnnotation;
677
+ return (!!annotation && isRulesUnitTestingType(annotation.typeAnnotation));
678
+ }
679
+ if (def.type !== 'Variable' ||
680
+ def.node.type !== utils_1.AST_NODE_TYPES.VariableDeclarator ||
681
+ def.parent?.type !== utils_1.AST_NODE_TYPES.VariableDeclaration ||
682
+ def.parent.kind !== 'const') {
683
+ return false;
684
+ }
685
+ const declarator = def.node;
686
+ if (declarator.id.type === utils_1.AST_NODE_TYPES.Identifier &&
687
+ declarator.id.typeAnnotation &&
688
+ isRulesUnitTestingType(declarator.id.typeAnnotation.typeAnnotation)) {
689
+ return true;
690
+ }
691
+ return tracesToRulesUnitTesting(declarator.init, visited);
692
+ }
568
693
  return {
569
694
  TSTypeReference(node) {
570
695
  if (node.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
@@ -601,6 +726,9 @@ exports.enforceFirestoreDocRefGeneric = (0, createRule_1.createRule)({
601
726
  if (node.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
602
727
  node.callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
603
728
  node.callee.property.name === 'doc') {
729
+ if (tracesToRulesUnitTesting(node.callee.object)) {
730
+ return;
731
+ }
604
732
  const typeAnnotation = node.typeParameters;
605
733
  const isOnTypedCollection = isTypedCollectionReference(node.callee.object);
606
734
  // If this is a .doc() call on a typed CollectionReference,
@@ -655,6 +783,9 @@ exports.enforceFirestoreDocRefGeneric = (0, createRule_1.createRule)({
655
783
  node.callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
656
784
  node.callee.property.name === 'collection' &&
657
785
  !isPartOfMethodChain(node)) {
786
+ if (tracesToRulesUnitTesting(node.callee.object)) {
787
+ return;
788
+ }
658
789
  const typeAnnotation = node.typeParameters;
659
790
  if (!typeAnnotation) {
660
791
  context.report({
@@ -675,6 +806,9 @@ exports.enforceFirestoreDocRefGeneric = (0, createRule_1.createRule)({
675
806
  else if (node.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
676
807
  node.callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
677
808
  node.callee.property.name === 'collectionGroup') {
809
+ if (tracesToRulesUnitTesting(node.callee.object)) {
810
+ return;
811
+ }
678
812
  const typeAnnotation = node.typeParameters;
679
813
  if (!typeAnnotation) {
680
814
  context.report({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.20.64",
3
+ "version": "1.20.65",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -1,4 +1,18 @@
1
1
  [
2
+ {
3
+ "version": "1.20.65",
4
+ "date": "2026-08-01T13:35:40.762Z",
5
+ "rules": [
6
+ {
7
+ "name": "enforce-firestore-doc-ref-generic",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 1564
11
+ ],
12
+ "summary": "exempt compat Firestore receivers from @firebase/rules-unit-testing (closes #1564)"
13
+ }
14
+ ]
15
+ },
2
16
  {
3
17
  "version": "1.20.64",
4
18
  "date": "2026-08-01T12:02:23.073Z",