@blumintinc/eslint-plugin-blumint 1.20.64 → 1.20.66

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.66',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -565,6 +565,183 @@ 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
+ case utils_1.AST_NODE_TYPES.ArrowFunctionExpression:
659
+ case utils_1.AST_NODE_TYPES.FunctionExpression:
660
+ case utils_1.AST_NODE_TYPES.FunctionDeclaration:
661
+ return functionReturnTracesToRulesUnitTesting(node, visited);
662
+ default:
663
+ return false;
664
+ }
665
+ }
666
+ /**
667
+ * Reached when a receiver is the result of calling a local helper, so what
668
+ * the helper hands back decides the surface. Only top-level returns are
669
+ * read; a return nested inside another function belongs to that one.
670
+ */
671
+ function functionReturnTracesToRulesUnitTesting(node, visited) {
672
+ if (node.body.type !== utils_1.AST_NODE_TYPES.BlockStatement) {
673
+ return tracesToRulesUnitTesting(node.body, visited);
674
+ }
675
+ return node.body.body.some((statement) => statement.type === utils_1.AST_NODE_TYPES.ReturnStatement &&
676
+ tracesToRulesUnitTesting(statement.argument, visited));
677
+ }
678
+ /**
679
+ * `withSecurityRulesDisabled` is the one API here that delivers a context
680
+ * through a callback rather than a return value, and the documented spelling
681
+ * leaves the parameter unannotated, so the call it belongs to is the only
682
+ * evidence of the surface.
683
+ */
684
+ const CONTEXT_CALLBACK_METHOD = 'withSecurityRulesDisabled';
685
+ function parameterTracesToRulesUnitTesting(name, owner, visited) {
686
+ if (name.type === utils_1.AST_NODE_TYPES.Identifier &&
687
+ name.typeAnnotation &&
688
+ isRulesUnitTestingType(name.typeAnnotation.typeAnnotation)) {
689
+ return true;
690
+ }
691
+ const call = owner.parent;
692
+ if (!call ||
693
+ call.type !== utils_1.AST_NODE_TYPES.CallExpression ||
694
+ !call.arguments.includes(owner) ||
695
+ call.callee.type !== utils_1.AST_NODE_TYPES.MemberExpression ||
696
+ call.callee.property.type !== utils_1.AST_NODE_TYPES.Identifier ||
697
+ call.callee.property.name !== CONTEXT_CALLBACK_METHOD) {
698
+ return false;
699
+ }
700
+ return tracesToRulesUnitTesting(call.callee.object, visited);
701
+ }
702
+ function identifierTracesToRulesUnitTesting(node, visited) {
703
+ const scope = ASTHelpers_1.ASTHelpers.getScope(context, node);
704
+ const variable = ASTHelpers_1.ASTHelpers.findVariableInScope(scope, node.name);
705
+ if (!variable || variable.defs.length !== 1) {
706
+ return false;
707
+ }
708
+ const def = variable.defs[0];
709
+ if (def.type === 'ImportBinding') {
710
+ return (def.parent?.type === utils_1.AST_NODE_TYPES.ImportDeclaration &&
711
+ def.parent.source.value === RULES_UNIT_TESTING_MODULE);
712
+ }
713
+ if (def.type === 'Parameter') {
714
+ return parameterTracesToRulesUnitTesting(def.name, def.node, visited);
715
+ }
716
+ // A hoisted declaration binds the helper the same way a `const` arrow
717
+ // does; an ambient `declare function` has no body and falls through.
718
+ if (def.type === 'FunctionName') {
719
+ return tracesToRulesUnitTesting(def.node, visited);
720
+ }
721
+ if (def.type !== 'Variable' ||
722
+ def.node.type !== utils_1.AST_NODE_TYPES.VariableDeclarator) {
723
+ return false;
724
+ }
725
+ const declarator = def.node;
726
+ // An annotation constrains every assignment rather than just the
727
+ // initializer, so unlike an initializer it survives a `let`. The
728
+ // environment handle is created in `beforeAll` and so cannot be `const`;
729
+ // `let testEnv: RulesTestEnvironment` is the documented spelling, and
730
+ // TypeScript refuses to put another surface in it.
731
+ if (declarator.id.type === utils_1.AST_NODE_TYPES.Identifier &&
732
+ declarator.id.typeAnnotation &&
733
+ isRulesUnitTestingType(declarator.id.typeAnnotation.typeAnnotation)) {
734
+ return true;
735
+ }
736
+ // Without that guarantee only a `const` initializer is followed, mirroring
737
+ // `isTypedCollectionBinding`: an unannotated reassignable binding can hold
738
+ // an Admin SDK handle by the time it reaches `.doc()`.
739
+ if (def.parent?.type !== utils_1.AST_NODE_TYPES.VariableDeclaration ||
740
+ def.parent.kind !== 'const') {
741
+ return false;
742
+ }
743
+ return tracesToRulesUnitTesting(declarator.init, visited);
744
+ }
568
745
  return {
569
746
  TSTypeReference(node) {
570
747
  if (node.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
@@ -601,6 +778,9 @@ exports.enforceFirestoreDocRefGeneric = (0, createRule_1.createRule)({
601
778
  if (node.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
602
779
  node.callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
603
780
  node.callee.property.name === 'doc') {
781
+ if (tracesToRulesUnitTesting(node.callee.object)) {
782
+ return;
783
+ }
604
784
  const typeAnnotation = node.typeParameters;
605
785
  const isOnTypedCollection = isTypedCollectionReference(node.callee.object);
606
786
  // If this is a .doc() call on a typed CollectionReference,
@@ -655,6 +835,9 @@ exports.enforceFirestoreDocRefGeneric = (0, createRule_1.createRule)({
655
835
  node.callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
656
836
  node.callee.property.name === 'collection' &&
657
837
  !isPartOfMethodChain(node)) {
838
+ if (tracesToRulesUnitTesting(node.callee.object)) {
839
+ return;
840
+ }
658
841
  const typeAnnotation = node.typeParameters;
659
842
  if (!typeAnnotation) {
660
843
  context.report({
@@ -675,6 +858,9 @@ exports.enforceFirestoreDocRefGeneric = (0, createRule_1.createRule)({
675
858
  else if (node.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
676
859
  node.callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
677
860
  node.callee.property.name === 'collectionGroup') {
861
+ if (tracesToRulesUnitTesting(node.callee.object)) {
862
+ return;
863
+ }
678
864
  const typeAnnotation = node.typeParameters;
679
865
  if (!typeAnnotation) {
680
866
  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.66",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -1,4 +1,32 @@
1
1
  [
2
+ {
3
+ "version": "1.20.66",
4
+ "date": "2026-08-01T13:53:31.680Z",
5
+ "rules": [
6
+ {
7
+ "name": "enforce-firestore-doc-ref-generic",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 1564
11
+ ],
12
+ "summary": "trace compat receivers through annotated lets and seeding callbacks (closes #1564)"
13
+ }
14
+ ]
15
+ },
16
+ {
17
+ "version": "1.20.65",
18
+ "date": "2026-08-01T13:35:40.762Z",
19
+ "rules": [
20
+ {
21
+ "name": "enforce-firestore-doc-ref-generic",
22
+ "changeType": "fix",
23
+ "issues": [
24
+ 1564
25
+ ],
26
+ "summary": "exempt compat Firestore receivers from @firebase/rules-unit-testing (closes #1564)"
27
+ }
28
+ ]
29
+ },
2
30
  {
3
31
  "version": "1.20.64",
4
32
  "date": "2026-08-01T12:02:23.073Z",