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