@blumintinc/eslint-plugin-blumint 1.12.6 → 1.13.0
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/README.md +140 -99
- package/lib/index.js +111 -17
- package/lib/rules/array-methods-this-context.js +20 -2
- package/lib/rules/avoid-utils-directory.js +11 -2
- package/lib/rules/dynamic-https-errors.d.ts +3 -1
- package/lib/rules/dynamic-https-errors.js +48 -10
- package/lib/rules/enforce-assert-safe-object-key.d.ts +8 -0
- package/lib/rules/{enforce-assertSafe-object-key.js → enforce-assert-safe-object-key.js} +51 -95
- package/lib/rules/enforce-assert-throws.d.ts +3 -1
- package/lib/rules/enforce-assert-throws.js +493 -51
- package/lib/rules/enforce-boolean-naming-prefixes.d.ts +3 -1
- package/lib/rules/enforce-boolean-naming-prefixes.js +832 -39
- package/lib/rules/enforce-callable-types.js +4 -4
- package/lib/rules/enforce-callback-memo.js +143 -18
- package/lib/rules/enforce-centralized-mock-firestore.js +4 -1
- package/lib/rules/enforce-console-error.d.ts +3 -0
- package/lib/rules/enforce-console-error.js +250 -0
- package/lib/rules/enforce-css-media-queries.js +24 -46
- package/lib/rules/enforce-dynamic-firebase-imports.js +100 -19
- package/lib/rules/enforce-dynamic-imports.js +1 -1
- package/lib/rules/enforce-exported-function-types.js +3 -3
- package/lib/rules/enforce-fieldpath-syntax-in-docsetter.d.ts +2 -0
- package/lib/rules/enforce-fieldpath-syntax-in-docsetter.js +212 -0
- package/lib/rules/enforce-firestore-doc-ref-generic.js +335 -0
- package/lib/rules/enforce-firestore-facade.js +347 -157
- package/lib/rules/enforce-firestore-rules-get-access.d.ts +3 -0
- package/lib/rules/enforce-firestore-rules-get-access.js +109 -0
- package/lib/rules/enforce-global-constants.d.ts +4 -1
- package/lib/rules/enforce-global-constants.js +240 -12
- package/lib/rules/enforce-memoize-async.d.ts +2 -1
- package/lib/rules/enforce-memoize-async.js +96 -27
- package/lib/rules/enforce-memoize-getters.d.ts +3 -0
- package/lib/rules/enforce-memoize-getters.js +123 -0
- package/lib/rules/enforce-microdiff.js +2 -1
- package/lib/rules/enforce-positive-naming.js +5 -7
- package/lib/rules/enforce-props-argument-name.d.ts +2 -7
- package/lib/rules/enforce-props-argument-name.js +165 -151
- package/lib/rules/enforce-props-naming-consistency.d.ts +2 -0
- package/lib/rules/enforce-props-naming-consistency.js +145 -0
- package/lib/rules/enforce-querykey-ts.d.ts +9 -0
- package/lib/rules/enforce-querykey-ts.js +274 -0
- package/lib/rules/enforce-singular-type-names.js +9 -5
- package/lib/rules/enforce-typescript-markdown-code-blocks.d.ts +1 -0
- package/lib/rules/enforce-typescript-markdown-code-blocks.js +96 -0
- package/lib/rules/enforce-verb-noun-naming.js +85 -10
- package/lib/rules/extract-global-constants.js +57 -45
- package/lib/rules/fast-deep-equal-over-microdiff.d.ts +2 -1
- package/lib/rules/fast-deep-equal-over-microdiff.js +356 -78
- package/lib/rules/firestore-transaction-reads-before-writes.d.ts +1 -0
- package/lib/rules/firestore-transaction-reads-before-writes.js +231 -0
- package/lib/rules/generic-starts-with-t.js +8 -2
- package/lib/rules/memoize-root-level-hocs.d.ts +7 -0
- package/lib/rules/memoize-root-level-hocs.js +242 -0
- package/lib/rules/no-array-length-in-deps.d.ts +12 -0
- package/lib/rules/no-array-length-in-deps.js +308 -0
- package/lib/rules/no-async-array-filter.js +5 -2
- package/lib/rules/no-complex-cloud-params.js +15 -2
- package/lib/rules/no-compositing-layer-props.js +4 -2
- package/lib/rules/no-entire-object-hook-deps.d.ts +3 -1
- package/lib/rules/no-entire-object-hook-deps.js +317 -63
- package/lib/rules/no-excessive-parent-chain.d.ts +5 -0
- package/lib/rules/no-excessive-parent-chain.js +273 -0
- package/lib/rules/no-firestore-object-arrays.js +345 -30
- package/lib/rules/no-hungarian.js +13 -13
- package/lib/rules/no-margin-properties.d.ts +7 -1
- package/lib/rules/no-margin-properties.js +16 -3
- package/lib/rules/no-misleading-boolean-prefixes.d.ts +7 -0
- package/lib/rules/no-misleading-boolean-prefixes.js +327 -0
- package/lib/rules/no-overridable-method-calls-in-constructor.d.ts +1 -0
- package/lib/rules/no-overridable-method-calls-in-constructor.js +260 -0
- package/lib/rules/no-passthrough-getters.d.ts +4 -0
- package/lib/rules/no-passthrough-getters.js +170 -0
- package/lib/rules/no-redundant-usecallback-wrapper.d.ts +8 -0
- package/lib/rules/no-redundant-usecallback-wrapper.js +315 -0
- package/lib/rules/no-restricted-properties-fix.d.ts +12 -0
- package/lib/rules/no-restricted-properties-fix.js +172 -0
- package/lib/rules/no-separate-loading-state.d.ts +8 -0
- package/lib/rules/no-separate-loading-state.js +142 -0
- package/lib/rules/no-stale-state-across-await.d.ts +1 -0
- package/lib/rules/no-stale-state-across-await.js +185 -0
- package/lib/rules/no-static-constants-in-dynamic-files.d.ts +3 -0
- package/lib/rules/no-static-constants-in-dynamic-files.js +149 -0
- package/lib/rules/no-try-catch-already-exists-in-transaction.d.ts +1 -0
- package/lib/rules/no-try-catch-already-exists-in-transaction.js +342 -0
- package/lib/rules/no-type-assertion-returns.js +6 -0
- package/lib/rules/no-undefined-null-passthrough.js +166 -19
- package/lib/rules/no-unnecessary-verb-suffix.js +0 -2
- package/lib/rules/no-unsafe-firestore-spread.js +8 -5
- package/lib/rules/no-unused-props.d.ts +3 -0
- package/lib/rules/no-unused-props.js +213 -78
- package/lib/rules/optimize-object-boolean-conditions.d.ts +1 -0
- package/lib/rules/optimize-object-boolean-conditions.js +234 -0
- package/lib/rules/parallelize-async-operations.d.ts +8 -0
- package/lib/rules/parallelize-async-operations.js +375 -0
- package/lib/rules/prefer-batch-operations.js +199 -33
- package/lib/rules/prefer-block-comments-for-declarations.js +13 -3
- package/lib/rules/prefer-document-flattening.d.ts +4 -0
- package/lib/rules/prefer-document-flattening.js +239 -0
- package/lib/rules/prefer-field-paths-in-transforms.d.ts +7 -0
- package/lib/rules/prefer-field-paths-in-transforms.js +250 -0
- package/lib/rules/prefer-fragment-component.js +3 -3
- package/lib/rules/prefer-global-router-state-key.d.ts +7 -3
- package/lib/rules/prefer-global-router-state-key.js +230 -31
- package/lib/rules/prefer-next-dynamic.d.ts +9 -0
- package/lib/rules/prefer-next-dynamic.js +366 -0
- package/lib/rules/prefer-nullish-coalescing-boolean-props.d.ts +1 -0
- package/lib/rules/prefer-nullish-coalescing-boolean-props.js +430 -0
- package/lib/rules/prefer-nullish-coalescing-override.d.ts +6 -0
- package/lib/rules/prefer-nullish-coalescing-override.js +182 -0
- package/lib/rules/prefer-params-over-parent-id.d.ts +5 -0
- package/lib/rules/prefer-params-over-parent-id.js +228 -0
- package/lib/rules/prefer-settings-object.js +23 -15
- package/lib/rules/prefer-type-alias-over-typeof-constant.d.ts +3 -0
- package/lib/rules/prefer-type-alias-over-typeof-constant.js +259 -0
- package/lib/rules/prefer-url-tostring-over-tojson.d.ts +2 -0
- package/lib/rules/prefer-url-tostring-over-tojson.js +166 -0
- package/lib/rules/prefer-use-deep-compare-memo.d.ts +4 -0
- package/lib/rules/prefer-use-deep-compare-memo.js +428 -0
- package/lib/rules/prefer-usecallback-over-usememo-for-functions.js +24 -1
- package/lib/rules/prefer-usememo-over-useeffect-usestate.js +24 -5
- package/lib/rules/react-usememo-should-be-component.d.ts +2 -1
- package/lib/rules/react-usememo-should-be-component.js +144 -78
- package/lib/rules/require-dynamic-firebase-imports.js +2 -1
- package/lib/rules/require-image-optimized.js +1 -1
- package/lib/rules/require-memo.js +11 -1
- package/lib/rules/require-usememo-object-literals.js +7 -1
- package/lib/rules/semantic-function-prefixes.js +38 -18
- package/lib/rules/sync-onwrite-name-func.js +5 -1
- package/lib/rules/test-file-location-enforcement.d.ts +1 -0
- package/lib/rules/test-file-location-enforcement.js +57 -0
- package/lib/rules/use-custom-link.js +6 -1
- package/lib/rules/use-custom-router.js +6 -1
- package/lib/rules/use-latest-callback.d.ts +3 -0
- package/lib/rules/use-latest-callback.js +270 -0
- package/lib/utils/ASTHelpers.js +10 -0
- package/lib/utils/createRule.js +1 -1
- package/lib/utils/graph/ClassGraphSorterReadability.js +1 -1
- package/lib/utils/ruleTester.d.ts +1 -0
- package/lib/utils/ruleTester.js +4 -1
- package/package.json +21 -14
- package/lib/rules/enforce-assertSafe-object-key.d.ts +0 -2
|
@@ -4,152 +4,68 @@ exports.enforceFirestoreFacade = void 0;
|
|
|
4
4
|
const utils_1 = require("@typescript-eslint/utils");
|
|
5
5
|
const createRule_1 = require("../utils/createRule");
|
|
6
6
|
const FIRESTORE_METHODS = new Set(['get', 'set', 'update', 'delete']);
|
|
7
|
-
const
|
|
8
|
-
|
|
7
|
+
const COLLECTION_CONSTRUCTORS = new Set(['Set', 'Map', 'WeakSet', 'WeakMap']);
|
|
8
|
+
const KNOWN_FIRESTORE_ROOTS = new Set(['db', 'firestore']);
|
|
9
|
+
const isMemberExpression = (node) => node.type === utils_1.AST_NODE_TYPES.MemberExpression;
|
|
10
|
+
const isCallExpression = (node) => node.type === utils_1.AST_NODE_TYPES.CallExpression;
|
|
11
|
+
const isIdentifier = (node) => node.type === utils_1.AST_NODE_TYPES.Identifier;
|
|
12
|
+
const unwrapTSAsExpression = (node) => {
|
|
13
|
+
if (node.type === utils_1.AST_NODE_TYPES.TSAsExpression) {
|
|
14
|
+
return unwrapTSAsExpression(node.expression);
|
|
15
|
+
}
|
|
16
|
+
return node;
|
|
9
17
|
};
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
if (!init)
|
|
18
|
-
return false;
|
|
19
|
-
// Check for direct realtimeDb.ref() assignments
|
|
20
|
-
// e.g., const ref = realtimeDb.ref(path);
|
|
21
|
-
if (init.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
22
|
-
isMemberExpression(init.callee) &&
|
|
23
|
-
isIdentifier(init.callee.property) &&
|
|
24
|
-
init.callee.property.name === 'ref' &&
|
|
25
|
-
isIdentifier(init.callee.object) &&
|
|
26
|
-
(init.callee.object.name === 'realtimeDb' || init.callee.object.name.includes('realtimeDb'))) {
|
|
27
|
-
if (isIdentifier(node.id)) {
|
|
28
|
-
realtimeDbRefVariables.add(node.id.name);
|
|
29
|
-
return true;
|
|
18
|
+
const getLeftmostIdentifier = (node) => {
|
|
19
|
+
let current = node
|
|
20
|
+
? unwrapTSAsExpression(node)
|
|
21
|
+
: null;
|
|
22
|
+
while (current) {
|
|
23
|
+
if (isIdentifier(current)) {
|
|
24
|
+
return current;
|
|
30
25
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
if (init.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
35
|
-
isMemberExpression(init.callee) &&
|
|
36
|
-
isIdentifier(init.callee.property) &&
|
|
37
|
-
init.callee.property.name === 'child' &&
|
|
38
|
-
isIdentifier(init.callee.object) &&
|
|
39
|
-
realtimeDbRefVariables.has(init.callee.object.name)) {
|
|
40
|
-
if (isIdentifier(node.id)) {
|
|
41
|
-
realtimeDbChildVariables.add(node.id.name);
|
|
42
|
-
return true;
|
|
26
|
+
if (isMemberExpression(current)) {
|
|
27
|
+
current = current.object;
|
|
28
|
+
continue;
|
|
43
29
|
}
|
|
30
|
+
if (isCallExpression(current)) {
|
|
31
|
+
current = current.callee;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
44
35
|
}
|
|
45
|
-
return
|
|
46
|
-
};
|
|
47
|
-
const isRealtimeDbReference = (node) => {
|
|
48
|
-
// Check if it's a direct realtimeDb.ref() call
|
|
49
|
-
if (node.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
50
|
-
isMemberExpression(node.callee) &&
|
|
51
|
-
isIdentifier(node.callee.property) &&
|
|
52
|
-
node.callee.property.name === 'ref' &&
|
|
53
|
-
isIdentifier(node.callee.object) &&
|
|
54
|
-
(node.callee.object.name === 'realtimeDb' || node.callee.object.name.includes('realtimeDb'))) {
|
|
55
|
-
return true;
|
|
56
|
-
}
|
|
57
|
-
// Check if it's a variable that holds a realtimeDb reference
|
|
58
|
-
if (isIdentifier(node)) {
|
|
59
|
-
return realtimeDbRefVariables.has(node.name) || realtimeDbChildVariables.has(node.name);
|
|
60
|
-
}
|
|
61
|
-
// Check if it's a child() call on a realtimeDb reference
|
|
62
|
-
if (node.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
63
|
-
isMemberExpression(node.callee) &&
|
|
64
|
-
isIdentifier(node.callee.property) &&
|
|
65
|
-
node.callee.property.name === 'child' &&
|
|
66
|
-
isIdentifier(node.callee.object) &&
|
|
67
|
-
(realtimeDbRefVariables.has(node.callee.object.name) || realtimeDbChildVariables.has(node.callee.object.name))) {
|
|
68
|
-
return true;
|
|
69
|
-
}
|
|
70
|
-
return false;
|
|
36
|
+
return null;
|
|
71
37
|
};
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
return false;
|
|
78
|
-
}
|
|
79
|
-
// Check if the method is called on a facade instance
|
|
80
|
-
const object = node.callee.object;
|
|
81
|
-
if (isIdentifier(object)) {
|
|
82
|
-
const name = object.name;
|
|
83
|
-
// Skip if it's a facade instance
|
|
84
|
-
if (name.includes('Fetcher') ||
|
|
85
|
-
name.includes('Setter') ||
|
|
86
|
-
name.includes('Tx')) {
|
|
87
|
-
return false;
|
|
88
|
-
}
|
|
89
|
-
// Skip if it's a realtimeDb reference variable
|
|
90
|
-
if (realtimeDbRefVariables.has(name) || realtimeDbChildVariables.has(name)) {
|
|
91
|
-
return false;
|
|
38
|
+
const isFirestoreRoot = (node, firestoreCollectionVariables, firestoreDocRefVariables) => {
|
|
39
|
+
const baseIdentifier = getLeftmostIdentifier(node);
|
|
40
|
+
if (baseIdentifier) {
|
|
41
|
+
if (KNOWN_FIRESTORE_ROOTS.has(baseIdentifier.name)) {
|
|
42
|
+
return true;
|
|
92
43
|
}
|
|
93
|
-
|
|
94
|
-
|
|
44
|
+
if (firestoreCollectionVariables.has(baseIdentifier.name) ||
|
|
45
|
+
firestoreDocRefVariables.has(baseIdentifier.name)) {
|
|
95
46
|
return true;
|
|
96
47
|
}
|
|
97
48
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
// Check if it's a Firestore reference
|
|
107
|
-
let current = object;
|
|
108
|
-
let foundDocOrCollection = false;
|
|
109
|
-
while (current) {
|
|
110
|
-
if (isCallExpression(current)) {
|
|
111
|
-
const callee = current.callee;
|
|
112
|
-
if (isMemberExpression(callee)) {
|
|
113
|
-
const property = callee.property;
|
|
114
|
-
if (isIdentifier(property)) {
|
|
115
|
-
// Check for Firestore methods
|
|
116
|
-
if (property.name === 'doc' || property.name === 'collection') {
|
|
117
|
-
foundDocOrCollection = true;
|
|
118
|
-
break;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
if (isMemberExpression(current)) {
|
|
124
|
-
current = current.object;
|
|
125
|
-
}
|
|
126
|
-
else if (current.type === utils_1.AST_NODE_TYPES.TSAsExpression) {
|
|
127
|
-
// Handle nested type assertions
|
|
128
|
-
current = current.expression;
|
|
129
|
-
}
|
|
130
|
-
else {
|
|
131
|
-
break;
|
|
49
|
+
if (isCallExpression(node) &&
|
|
50
|
+
isMemberExpression(node.callee) &&
|
|
51
|
+
isIdentifier(node.callee.property) &&
|
|
52
|
+
node.callee.property.name === 'firestore') {
|
|
53
|
+
const innerBase = getLeftmostIdentifier(node.callee.object);
|
|
54
|
+
if (innerBase &&
|
|
55
|
+
(innerBase.name === 'app' || KNOWN_FIRESTORE_ROOTS.has(innerBase.name))) {
|
|
56
|
+
return true;
|
|
132
57
|
}
|
|
133
58
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
!name.includes('realtimeDb') &&
|
|
141
|
-
!realtimeDbRefVariables.has(name) &&
|
|
142
|
-
!realtimeDbChildVariables.has(name)) {
|
|
59
|
+
if (isMemberExpression(node) &&
|
|
60
|
+
isIdentifier(node.property) &&
|
|
61
|
+
node.property.name === 'firestore') {
|
|
62
|
+
const innerBase = getLeftmostIdentifier(node.object);
|
|
63
|
+
if (innerBase &&
|
|
64
|
+
(innerBase.name === 'app' || KNOWN_FIRESTORE_ROOTS.has(innerBase.name))) {
|
|
143
65
|
return true;
|
|
144
66
|
}
|
|
145
67
|
}
|
|
146
|
-
return
|
|
147
|
-
};
|
|
148
|
-
const isCallExpression = (node) => {
|
|
149
|
-
return node.type === utils_1.AST_NODE_TYPES.CallExpression;
|
|
150
|
-
};
|
|
151
|
-
const isIdentifier = (node) => {
|
|
152
|
-
return node.type === utils_1.AST_NODE_TYPES.Identifier;
|
|
68
|
+
return false;
|
|
153
69
|
};
|
|
154
70
|
exports.enforceFirestoreFacade = (0, createRule_1.createRule)({
|
|
155
71
|
name: 'enforce-firestore-facade',
|
|
@@ -161,21 +77,308 @@ exports.enforceFirestoreFacade = (0, createRule_1.createRule)({
|
|
|
161
77
|
},
|
|
162
78
|
schema: [],
|
|
163
79
|
messages: {
|
|
164
|
-
noDirectGet: '
|
|
165
|
-
noDirectSet: '
|
|
166
|
-
noDirectUpdate: '
|
|
167
|
-
noDirectDelete: '
|
|
80
|
+
noDirectGet: 'Direct Firestore "{{method}}" on {{target}} skips the Firestore fetcher facades that enforce typed deserialization, shared caching, and consistent error handling. Route reads through FirestoreFetcher or FirestoreDocFetcher so Firestore access stays observable and applies the shared safeguards.',
|
|
81
|
+
noDirectSet: 'Direct Firestore "{{method}}" on {{target}} bypasses DocSetter and DocSetterTransaction, which apply validation, merge semantics, and centralized retry/metrics. Send writes through DocSetter or DocSetterTransaction to keep Firestore writes consistent, auditable, and safer under load.',
|
|
82
|
+
noDirectUpdate: 'Direct Firestore "{{method}}" on {{target}} bypasses DocSetter and DocSetterTransaction, which guard partial updates with validation and shared retry/metrics. Use the setter facades for updates so field-level changes stay consistent with our Firestore write contract.',
|
|
83
|
+
noDirectDelete: 'Direct Firestore "{{method}}" on {{target}} bypasses DocSetter and DocSetterTransaction, which coordinate deletes with validation, retries, and any soft-delete policies. Perform deletes through the setter facades to avoid silent data loss and keep write telemetry intact.',
|
|
168
84
|
},
|
|
169
85
|
},
|
|
170
86
|
defaultOptions: [],
|
|
171
87
|
create(context) {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
88
|
+
const realtimeDbRefVariables = new Set();
|
|
89
|
+
const realtimeDbChildVariables = new Set();
|
|
90
|
+
const collectionObjectVariables = new Set();
|
|
91
|
+
const firestoreCollectionVariables = new Set();
|
|
92
|
+
const firestoreDocRefVariables = new Set();
|
|
93
|
+
const firestoreBatchVariables = new Set();
|
|
94
|
+
const firestoreTransactionVariables = new Set();
|
|
95
|
+
const docSetterVariables = new Set();
|
|
96
|
+
const batchManagerVariables = new Set();
|
|
97
|
+
const sourceCode = context.getSourceCode();
|
|
98
|
+
const clearFirestoreTrackingFor = (name) => {
|
|
99
|
+
firestoreDocRefVariables.delete(name);
|
|
100
|
+
firestoreCollectionVariables.delete(name);
|
|
101
|
+
firestoreBatchVariables.delete(name);
|
|
102
|
+
firestoreTransactionVariables.delete(name);
|
|
103
|
+
docSetterVariables.delete(name);
|
|
104
|
+
batchManagerVariables.delete(name);
|
|
105
|
+
};
|
|
106
|
+
const recordFirestoreVariable = (varName, expression) => {
|
|
107
|
+
const target = unwrapTSAsExpression(expression);
|
|
108
|
+
if (target.type === utils_1.AST_NODE_TYPES.ConditionalExpression) {
|
|
109
|
+
const matchedConsequent = recordFirestoreVariable(varName, target.consequent);
|
|
110
|
+
const matchedAlternate = recordFirestoreVariable(varName, target.alternate);
|
|
111
|
+
return matchedConsequent || matchedAlternate;
|
|
112
|
+
}
|
|
113
|
+
if (target.type === utils_1.AST_NODE_TYPES.LogicalExpression) {
|
|
114
|
+
return (recordFirestoreVariable(varName, target.left) ||
|
|
115
|
+
recordFirestoreVariable(varName, target.right));
|
|
116
|
+
}
|
|
117
|
+
if (target.type === utils_1.AST_NODE_TYPES.SequenceExpression) {
|
|
118
|
+
const last = target.expressions[target.expressions.length - 1];
|
|
119
|
+
return last ? recordFirestoreVariable(varName, last) : false;
|
|
120
|
+
}
|
|
121
|
+
if (target.type === utils_1.AST_NODE_TYPES.NewExpression &&
|
|
122
|
+
isIdentifier(target.callee) &&
|
|
123
|
+
(target.callee.name === 'DocSetter' ||
|
|
124
|
+
target.callee.name === 'DocSetterTransaction')) {
|
|
125
|
+
docSetterVariables.add(varName);
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
if (target.type === utils_1.AST_NODE_TYPES.NewExpression &&
|
|
129
|
+
isIdentifier(target.callee) &&
|
|
130
|
+
target.callee.name === 'BatchManager') {
|
|
131
|
+
batchManagerVariables.add(varName);
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
if (isFirestoreDocumentReference(target)) {
|
|
135
|
+
firestoreDocRefVariables.add(varName);
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
if (isFirestoreCollectionCall(target)) {
|
|
139
|
+
firestoreCollectionVariables.add(varName);
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
if (target.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
143
|
+
isMemberExpression(target.callee) &&
|
|
144
|
+
isIdentifier(target.callee.property) &&
|
|
145
|
+
target.callee.property.name === 'batch' &&
|
|
146
|
+
isIdentifier(target.callee.object) &&
|
|
147
|
+
target.callee.object.name === 'db') {
|
|
148
|
+
firestoreBatchVariables.add(varName);
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
if (target.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
152
|
+
(target.name === 'transaction' || target.name === 'tx' || target.name === 't')) {
|
|
153
|
+
firestoreTransactionVariables.add(varName);
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
return false;
|
|
157
|
+
};
|
|
158
|
+
const isCollectionObjectAssignment = (node) => {
|
|
159
|
+
if (node.type !== utils_1.AST_NODE_TYPES.VariableDeclarator)
|
|
160
|
+
return false;
|
|
161
|
+
const init = node.init;
|
|
162
|
+
if (!init)
|
|
163
|
+
return false;
|
|
164
|
+
if (init.type === utils_1.AST_NODE_TYPES.NewExpression &&
|
|
165
|
+
isIdentifier(init.callee) &&
|
|
166
|
+
COLLECTION_CONSTRUCTORS.has(init.callee.name) &&
|
|
167
|
+
isIdentifier(node.id)) {
|
|
168
|
+
collectionObjectVariables.add(node.id.name);
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
return false;
|
|
172
|
+
};
|
|
173
|
+
const isFirestoreCollectionCall = (node) => {
|
|
174
|
+
const candidate = unwrapTSAsExpression(node);
|
|
175
|
+
if (!isCallExpression(candidate))
|
|
176
|
+
return false;
|
|
177
|
+
if (!isMemberExpression(candidate.callee))
|
|
178
|
+
return false;
|
|
179
|
+
const property = candidate.callee.property;
|
|
180
|
+
if (!isIdentifier(property) || property.name !== 'collection')
|
|
181
|
+
return false;
|
|
182
|
+
return isFirestoreRoot(candidate.callee.object, firestoreCollectionVariables, firestoreDocRefVariables);
|
|
183
|
+
};
|
|
184
|
+
const isFirestoreDocumentReference = (node) => {
|
|
185
|
+
const candidate = unwrapTSAsExpression(node);
|
|
186
|
+
if (!isCallExpression(candidate))
|
|
187
|
+
return false;
|
|
188
|
+
if (!isMemberExpression(candidate.callee))
|
|
189
|
+
return false;
|
|
190
|
+
const docProperty = candidate.callee.property;
|
|
191
|
+
if (!isIdentifier(docProperty) || docProperty.name !== 'doc')
|
|
192
|
+
return false;
|
|
193
|
+
const collectionCall = candidate.callee.object;
|
|
194
|
+
if (!isFirestoreCollectionCall(collectionCall)) {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
return true;
|
|
198
|
+
};
|
|
199
|
+
const isFirestoreAssignment = (node) => {
|
|
200
|
+
if (node.type !== utils_1.AST_NODE_TYPES.VariableDeclarator)
|
|
201
|
+
return false;
|
|
202
|
+
const init = node.init;
|
|
203
|
+
if (!init || !isIdentifier(node.id))
|
|
204
|
+
return false;
|
|
205
|
+
return recordFirestoreVariable(node.id.name, init);
|
|
206
|
+
};
|
|
207
|
+
const handleAssignmentExpression = (node) => {
|
|
208
|
+
if (!isIdentifier(node.left))
|
|
209
|
+
return;
|
|
210
|
+
const varName = node.left.name;
|
|
211
|
+
const right = node.right;
|
|
212
|
+
clearFirestoreTrackingFor(varName);
|
|
213
|
+
recordFirestoreVariable(varName, right);
|
|
214
|
+
};
|
|
215
|
+
const isRealtimeDbRefAssignment = (node) => {
|
|
216
|
+
if (node.type !== utils_1.AST_NODE_TYPES.VariableDeclarator)
|
|
217
|
+
return false;
|
|
218
|
+
const init = node.init;
|
|
219
|
+
if (!init)
|
|
220
|
+
return false;
|
|
221
|
+
if (init.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
222
|
+
isMemberExpression(init.callee) &&
|
|
223
|
+
isIdentifier(init.callee.property) &&
|
|
224
|
+
init.callee.property.name === 'ref' &&
|
|
225
|
+
isIdentifier(init.callee.object) &&
|
|
226
|
+
(init.callee.object.name === 'realtimeDb' ||
|
|
227
|
+
init.callee.object.name.includes('realtimeDb')) &&
|
|
228
|
+
isIdentifier(node.id)) {
|
|
229
|
+
realtimeDbRefVariables.add(node.id.name);
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
if (init.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
233
|
+
isMemberExpression(init.callee) &&
|
|
234
|
+
isIdentifier(init.callee.property) &&
|
|
235
|
+
init.callee.property.name === 'child' &&
|
|
236
|
+
isIdentifier(init.callee.object) &&
|
|
237
|
+
realtimeDbRefVariables.has(init.callee.object.name) &&
|
|
238
|
+
isIdentifier(node.id)) {
|
|
239
|
+
realtimeDbChildVariables.add(node.id.name);
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
return false;
|
|
243
|
+
};
|
|
244
|
+
const isRealtimeDbReference = (node) => {
|
|
245
|
+
if (node.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
246
|
+
isMemberExpression(node.callee) &&
|
|
247
|
+
isIdentifier(node.callee.property) &&
|
|
248
|
+
node.callee.property.name === 'ref' &&
|
|
249
|
+
isIdentifier(node.callee.object) &&
|
|
250
|
+
(node.callee.object.name === 'realtimeDb' ||
|
|
251
|
+
node.callee.object.name.includes('realtimeDb'))) {
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
if (isIdentifier(node)) {
|
|
255
|
+
return (realtimeDbRefVariables.has(node.name) ||
|
|
256
|
+
realtimeDbChildVariables.has(node.name));
|
|
257
|
+
}
|
|
258
|
+
if (node.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
259
|
+
isMemberExpression(node.callee) &&
|
|
260
|
+
isIdentifier(node.callee.property) &&
|
|
261
|
+
node.callee.property.name === 'child' &&
|
|
262
|
+
isIdentifier(node.callee.object) &&
|
|
263
|
+
(realtimeDbRefVariables.has(node.callee.object.name) ||
|
|
264
|
+
realtimeDbChildVariables.has(node.callee.object.name))) {
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
return false;
|
|
268
|
+
};
|
|
269
|
+
const isTrackedFirestoreName = (name) => firestoreDocRefVariables.has(name) ||
|
|
270
|
+
firestoreBatchVariables.has(name) ||
|
|
271
|
+
firestoreTransactionVariables.has(name) ||
|
|
272
|
+
firestoreCollectionVariables.has(name);
|
|
273
|
+
const isFirestoreMethodCall = (node) => {
|
|
274
|
+
if (!isMemberExpression(node.callee))
|
|
275
|
+
return false;
|
|
276
|
+
const property = node.callee.property;
|
|
277
|
+
if (!isIdentifier(property) || !FIRESTORE_METHODS.has(property.name)) {
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
const object = node.callee.object;
|
|
281
|
+
if (isIdentifier(object)) {
|
|
282
|
+
const name = object.name;
|
|
283
|
+
if (docSetterVariables.has(name) ||
|
|
284
|
+
batchManagerVariables.has(name) ||
|
|
285
|
+
realtimeDbRefVariables.has(name) ||
|
|
286
|
+
realtimeDbChildVariables.has(name) ||
|
|
287
|
+
collectionObjectVariables.has(name)) {
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
if (isTrackedFirestoreName(name)) {
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
if (/^(batch|transaction)$/i.test(name)) {
|
|
294
|
+
return true;
|
|
295
|
+
}
|
|
296
|
+
if ((name.toLowerCase().includes('doc') ||
|
|
297
|
+
name.toLowerCase().includes('ref')) &&
|
|
298
|
+
!name.includes('realtimeDb') &&
|
|
299
|
+
!realtimeDbRefVariables.has(name) &&
|
|
300
|
+
!realtimeDbChildVariables.has(name)) {
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
if (isRealtimeDbReference(object)) {
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
if (isFirestoreDocumentReference(object)) {
|
|
309
|
+
return true;
|
|
310
|
+
}
|
|
311
|
+
if (object.type === utils_1.AST_NODE_TYPES.TSAsExpression) {
|
|
312
|
+
return isFirestoreMethodCall({
|
|
313
|
+
...node,
|
|
314
|
+
callee: {
|
|
315
|
+
...node.callee,
|
|
316
|
+
object: object.expression,
|
|
317
|
+
},
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
if (object.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
321
|
+
object.computed &&
|
|
322
|
+
object.object.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
323
|
+
const arrayName = object.object.name;
|
|
324
|
+
if (isTrackedFirestoreName(arrayName) ||
|
|
325
|
+
arrayName.toLowerCase().endsWith('refs')) {
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
if (isMemberExpression(object)) {
|
|
331
|
+
let current = object;
|
|
332
|
+
while (isMemberExpression(current)) {
|
|
333
|
+
if (isFirestoreDocumentReference(current) ||
|
|
334
|
+
isFirestoreCollectionCall(current)) {
|
|
335
|
+
return true;
|
|
336
|
+
}
|
|
337
|
+
current = current.object;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
let current = object;
|
|
341
|
+
while (current) {
|
|
342
|
+
const unwrapped = unwrapTSAsExpression(current);
|
|
343
|
+
if (isCallExpression(unwrapped) &&
|
|
344
|
+
isMemberExpression(unwrapped.callee) &&
|
|
345
|
+
isIdentifier(unwrapped.callee.property)) {
|
|
346
|
+
const propName = unwrapped.callee.property.name;
|
|
347
|
+
if ((propName === 'doc' || propName === 'collection') &&
|
|
348
|
+
(isFirestoreCollectionCall(unwrapped) ||
|
|
349
|
+
isFirestoreRoot(unwrapped.callee.object, firestoreCollectionVariables, firestoreDocRefVariables))) {
|
|
350
|
+
return true;
|
|
351
|
+
}
|
|
352
|
+
current = unwrapped.callee.object;
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
if (isMemberExpression(unwrapped)) {
|
|
356
|
+
current = unwrapped.object;
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
break;
|
|
360
|
+
}
|
|
361
|
+
return false;
|
|
362
|
+
};
|
|
363
|
+
const reportDirectFirestoreCall = (messageId, node, method, callee) => {
|
|
364
|
+
const target = sourceCode.getText(callee.object);
|
|
365
|
+
context.report({
|
|
366
|
+
node,
|
|
367
|
+
messageId,
|
|
368
|
+
data: {
|
|
369
|
+
method,
|
|
370
|
+
target,
|
|
371
|
+
},
|
|
372
|
+
});
|
|
373
|
+
};
|
|
175
374
|
return {
|
|
176
|
-
// Track variable declarations that are assigned realtimeDb references
|
|
177
375
|
VariableDeclarator(node) {
|
|
178
376
|
isRealtimeDbRefAssignment(node);
|
|
377
|
+
isCollectionObjectAssignment(node);
|
|
378
|
+
isFirestoreAssignment(node);
|
|
379
|
+
},
|
|
380
|
+
AssignmentExpression(node) {
|
|
381
|
+
handleAssignmentExpression(node);
|
|
179
382
|
},
|
|
180
383
|
CallExpression(node) {
|
|
181
384
|
if (!isFirestoreMethodCall(node))
|
|
@@ -186,31 +389,18 @@ exports.enforceFirestoreFacade = (0, createRule_1.createRule)({
|
|
|
186
389
|
const property = callee.property;
|
|
187
390
|
if (!isIdentifier(property))
|
|
188
391
|
return;
|
|
189
|
-
// Report appropriate error based on method
|
|
190
392
|
switch (property.name) {
|
|
191
393
|
case 'get':
|
|
192
|
-
|
|
193
|
-
node,
|
|
194
|
-
messageId: 'noDirectGet',
|
|
195
|
-
});
|
|
394
|
+
reportDirectFirestoreCall('noDirectGet', node, property.name, callee);
|
|
196
395
|
break;
|
|
197
396
|
case 'set':
|
|
198
|
-
|
|
199
|
-
node,
|
|
200
|
-
messageId: 'noDirectSet',
|
|
201
|
-
});
|
|
397
|
+
reportDirectFirestoreCall('noDirectSet', node, property.name, callee);
|
|
202
398
|
break;
|
|
203
399
|
case 'update':
|
|
204
|
-
|
|
205
|
-
node,
|
|
206
|
-
messageId: 'noDirectUpdate',
|
|
207
|
-
});
|
|
400
|
+
reportDirectFirestoreCall('noDirectUpdate', node, property.name, callee);
|
|
208
401
|
break;
|
|
209
402
|
case 'delete':
|
|
210
|
-
|
|
211
|
-
node,
|
|
212
|
-
messageId: 'noDirectDelete',
|
|
213
|
-
});
|
|
403
|
+
reportDirectFirestoreCall('noDirectDelete', node, property.name, callee);
|
|
214
404
|
break;
|
|
215
405
|
}
|
|
216
406
|
},
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.enforceFirestoreRulesGetAccess = void 0;
|
|
4
|
+
const createRule_1 = require("../utils/createRule");
|
|
5
|
+
const DIRECT_ACCESS_REGEX = /\b(?:request\.resource|resource)\.data(?!\.get\()(?:\.(?!get\()[A-Za-z_]\w*|\[['"][^'"]+['"]\])+(?:\s*)(?:!=|==|!==|===)\s*(?:null|undefined)\b/;
|
|
6
|
+
function hasDirectFieldAccessComparison(text) {
|
|
7
|
+
// Match resource.data.<a>.<b>... or bracketed string segments compared to null/undefined.
|
|
8
|
+
return DIRECT_ACCESS_REGEX.test(text);
|
|
9
|
+
}
|
|
10
|
+
function hasGetWithoutDefault(text) {
|
|
11
|
+
// Look for `.get('field')` with a single argument. We only check in contexts that
|
|
12
|
+
// also mention resource.data/request.resource.data to reduce false positives.
|
|
13
|
+
const mentionsRulesContext = /\b(?:request\.resource|resource)\.data\b/.test(text);
|
|
14
|
+
if (!mentionsRulesContext)
|
|
15
|
+
return false;
|
|
16
|
+
const singleArgGetRegex = /\.get\(\s*(['"][^'"]+['"])\s*\)/;
|
|
17
|
+
return singleArgGetRegex.test(text);
|
|
18
|
+
}
|
|
19
|
+
function applyDirectAccessFixes(text) {
|
|
20
|
+
// Replace each direct access chain with equivalent `.get('seg', null)` chain
|
|
21
|
+
const pattern = /\b((?:request\.resource|resource)\.data)(?!\.get\()((?:\.(?!get\()[A-Za-z_]\w*|\[['"][^'"]+['"]\])+)(\s*)((?:!=|==|!==|===)\s*(?:null|undefined)\b)/g;
|
|
22
|
+
const segmentRegex = /(?:\.(?!get\()[A-Za-z_]\w*|\[['"][^'"]+['"]\])/g;
|
|
23
|
+
const unescapeLiteral = (raw) => raw.replace(/\\(['"\\])/g, '$1');
|
|
24
|
+
return text.replace(pattern, (_m, prefix, path, preOpWhitespace, opAndRest) => {
|
|
25
|
+
const segments = [];
|
|
26
|
+
path.replace(segmentRegex, (seg) => {
|
|
27
|
+
if (seg.startsWith('.')) {
|
|
28
|
+
segments.push(seg.slice(1));
|
|
29
|
+
return '';
|
|
30
|
+
}
|
|
31
|
+
const match = /"\s*([^"]+)"|'\s*([^']+)'/.exec(seg);
|
|
32
|
+
if (match) {
|
|
33
|
+
const raw = match[1] ?? match[2] ?? '';
|
|
34
|
+
segments.push(unescapeLiteral(raw));
|
|
35
|
+
}
|
|
36
|
+
return '';
|
|
37
|
+
});
|
|
38
|
+
const replaced = segments
|
|
39
|
+
.map((seg) => `.get('${seg.replace(/'/g, "\\'")}', null)`)
|
|
40
|
+
.join('');
|
|
41
|
+
return `${prefix}${replaced}${preOpWhitespace}${opAndRest}`;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function applyGetDefaultFixes(text) {
|
|
45
|
+
// Add ", null" as the second argument when `.get('field')` is used
|
|
46
|
+
const singleArgGetRegexGlobal = /\.get\(\s*(['"][^'"]+['"])\s*\)/g;
|
|
47
|
+
return text.replace(singleArgGetRegexGlobal, (_m, keyLiteral) => `.get(${keyLiteral}, null)`);
|
|
48
|
+
}
|
|
49
|
+
exports.enforceFirestoreRulesGetAccess = (0, createRule_1.createRule)({
|
|
50
|
+
name: 'enforce-firestore-rules-get-access',
|
|
51
|
+
meta: {
|
|
52
|
+
type: 'suggestion',
|
|
53
|
+
docs: {
|
|
54
|
+
description: 'Ensure Firestore security rules use .get() with a default value instead of direct field access comparisons (e.g., resource.data.fieldX.fieldY != null).',
|
|
55
|
+
recommended: 'error',
|
|
56
|
+
},
|
|
57
|
+
fixable: 'code',
|
|
58
|
+
schema: [],
|
|
59
|
+
messages: {
|
|
60
|
+
useGetAccess: "Use .get('<field>', null) instead of direct field access in Firestore rules, e.g., resource.data.get('fieldX', null).",
|
|
61
|
+
requireGetDefault: "Provide a default value to .get() in Firestore rules, e.g., .get('fieldX', null).",
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
defaultOptions: [],
|
|
65
|
+
create(context) {
|
|
66
|
+
return {
|
|
67
|
+
Literal(node) {
|
|
68
|
+
if (typeof node.value !== 'string')
|
|
69
|
+
return;
|
|
70
|
+
const value = node.value;
|
|
71
|
+
if (!/(?:request\.resource|resource)\.data/.test(value))
|
|
72
|
+
return;
|
|
73
|
+
const needsDirectFix = hasDirectFieldAccessComparison(value);
|
|
74
|
+
const needsGetDefaultFix = hasGetWithoutDefault(value);
|
|
75
|
+
if (!needsDirectFix && !needsGetDefaultFix)
|
|
76
|
+
return;
|
|
77
|
+
context.report({
|
|
78
|
+
node,
|
|
79
|
+
messageId: needsDirectFix ? 'useGetAccess' : 'requireGetDefault',
|
|
80
|
+
fix: (fixer) => {
|
|
81
|
+
let newText = value;
|
|
82
|
+
if (needsDirectFix)
|
|
83
|
+
newText = applyDirectAccessFixes(newText);
|
|
84
|
+
if (needsGetDefaultFix)
|
|
85
|
+
newText = applyGetDefaultFixes(newText);
|
|
86
|
+
return fixer.replaceText(node, `"${newText.replace(/"/g, '\\"')}"`);
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
},
|
|
90
|
+
TemplateLiteral(node) {
|
|
91
|
+
// Best-effort: we only join static quasis and ignore embedded expressions,
|
|
92
|
+
// so template expressions spanning placeholders may be missed.
|
|
93
|
+
const staticText = node.quasis.map((q) => q.value.raw).join('');
|
|
94
|
+
if (!/(?:request\.resource|resource)\.data/.test(staticText))
|
|
95
|
+
return;
|
|
96
|
+
const needsDirectFix = hasDirectFieldAccessComparison(staticText);
|
|
97
|
+
const needsGetDefaultFix = hasGetWithoutDefault(staticText);
|
|
98
|
+
if (!needsDirectFix && !needsGetDefaultFix)
|
|
99
|
+
return;
|
|
100
|
+
context.report({
|
|
101
|
+
node,
|
|
102
|
+
messageId: needsDirectFix ? 'useGetAccess' : 'requireGetDefault',
|
|
103
|
+
// No auto-fix for template literals due to dynamic expressions
|
|
104
|
+
});
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
//# sourceMappingURL=enforce-firestore-rules-get-access.js.map
|