@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
|
@@ -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
package/release-manifest.json
CHANGED
|
@@ -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",
|