@blumintinc/eslint-plugin-blumint 1.12.5 → 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.
Files changed (142) hide show
  1. package/README.md +140 -99
  2. package/lib/index.js +111 -20
  3. package/lib/rules/array-methods-this-context.js +20 -2
  4. package/lib/rules/avoid-utils-directory.js +11 -2
  5. package/lib/rules/dynamic-https-errors.d.ts +3 -1
  6. package/lib/rules/dynamic-https-errors.js +48 -10
  7. package/lib/rules/enforce-assert-safe-object-key.d.ts +8 -0
  8. package/lib/rules/{enforce-assertSafe-object-key.js → enforce-assert-safe-object-key.js} +51 -95
  9. package/lib/rules/enforce-assert-throws.d.ts +3 -1
  10. package/lib/rules/enforce-assert-throws.js +493 -51
  11. package/lib/rules/enforce-boolean-naming-prefixes.d.ts +3 -1
  12. package/lib/rules/enforce-boolean-naming-prefixes.js +832 -39
  13. package/lib/rules/enforce-callable-types.js +4 -4
  14. package/lib/rules/enforce-callback-memo.js +143 -18
  15. package/lib/rules/enforce-centralized-mock-firestore.js +4 -1
  16. package/lib/rules/enforce-console-error.d.ts +3 -0
  17. package/lib/rules/enforce-console-error.js +250 -0
  18. package/lib/rules/enforce-css-media-queries.js +24 -46
  19. package/lib/rules/enforce-dynamic-firebase-imports.js +100 -19
  20. package/lib/rules/enforce-dynamic-imports.js +1 -1
  21. package/lib/rules/enforce-exported-function-types.js +3 -3
  22. package/lib/rules/enforce-fieldpath-syntax-in-docsetter.d.ts +2 -0
  23. package/lib/rules/enforce-fieldpath-syntax-in-docsetter.js +212 -0
  24. package/lib/rules/enforce-firestore-doc-ref-generic.js +335 -0
  25. package/lib/rules/enforce-firestore-facade.js +347 -157
  26. package/lib/rules/enforce-firestore-rules-get-access.d.ts +3 -0
  27. package/lib/rules/enforce-firestore-rules-get-access.js +109 -0
  28. package/lib/rules/enforce-global-constants.d.ts +4 -1
  29. package/lib/rules/enforce-global-constants.js +240 -12
  30. package/lib/rules/enforce-memoize-async.d.ts +2 -1
  31. package/lib/rules/enforce-memoize-async.js +96 -27
  32. package/lib/rules/enforce-memoize-getters.d.ts +3 -0
  33. package/lib/rules/enforce-memoize-getters.js +123 -0
  34. package/lib/rules/enforce-microdiff.js +2 -1
  35. package/lib/rules/enforce-positive-naming.js +5 -7
  36. package/lib/rules/enforce-props-argument-name.d.ts +2 -7
  37. package/lib/rules/enforce-props-argument-name.js +165 -151
  38. package/lib/rules/enforce-props-naming-consistency.d.ts +2 -0
  39. package/lib/rules/enforce-props-naming-consistency.js +145 -0
  40. package/lib/rules/enforce-querykey-ts.d.ts +9 -0
  41. package/lib/rules/enforce-querykey-ts.js +274 -0
  42. package/lib/rules/enforce-singular-type-names.js +9 -5
  43. package/lib/rules/enforce-typescript-markdown-code-blocks.d.ts +1 -0
  44. package/lib/rules/enforce-typescript-markdown-code-blocks.js +96 -0
  45. package/lib/rules/enforce-verb-noun-naming.js +85 -10
  46. package/lib/rules/extract-global-constants.js +57 -45
  47. package/lib/rules/fast-deep-equal-over-microdiff.d.ts +2 -1
  48. package/lib/rules/fast-deep-equal-over-microdiff.js +356 -78
  49. package/lib/rules/firestore-transaction-reads-before-writes.d.ts +1 -0
  50. package/lib/rules/firestore-transaction-reads-before-writes.js +231 -0
  51. package/lib/rules/generic-starts-with-t.js +8 -2
  52. package/lib/rules/memoize-root-level-hocs.d.ts +7 -0
  53. package/lib/rules/memoize-root-level-hocs.js +242 -0
  54. package/lib/rules/no-array-length-in-deps.d.ts +12 -0
  55. package/lib/rules/no-array-length-in-deps.js +308 -0
  56. package/lib/rules/no-async-array-filter.js +5 -2
  57. package/lib/rules/no-complex-cloud-params.js +15 -2
  58. package/lib/rules/no-compositing-layer-props.js +4 -2
  59. package/lib/rules/no-entire-object-hook-deps.d.ts +3 -1
  60. package/lib/rules/no-entire-object-hook-deps.js +317 -63
  61. package/lib/rules/no-excessive-parent-chain.d.ts +5 -0
  62. package/lib/rules/no-excessive-parent-chain.js +273 -0
  63. package/lib/rules/no-firestore-object-arrays.js +345 -30
  64. package/lib/rules/no-hungarian.js +13 -13
  65. package/lib/rules/no-margin-properties.d.ts +7 -1
  66. package/lib/rules/no-margin-properties.js +16 -3
  67. package/lib/rules/no-misleading-boolean-prefixes.d.ts +7 -0
  68. package/lib/rules/no-misleading-boolean-prefixes.js +327 -0
  69. package/lib/rules/no-overridable-method-calls-in-constructor.d.ts +1 -0
  70. package/lib/rules/no-overridable-method-calls-in-constructor.js +260 -0
  71. package/lib/rules/no-passthrough-getters.d.ts +4 -0
  72. package/lib/rules/no-passthrough-getters.js +170 -0
  73. package/lib/rules/no-redundant-usecallback-wrapper.d.ts +8 -0
  74. package/lib/rules/no-redundant-usecallback-wrapper.js +315 -0
  75. package/lib/rules/no-restricted-properties-fix.d.ts +12 -0
  76. package/lib/rules/no-restricted-properties-fix.js +172 -0
  77. package/lib/rules/no-separate-loading-state.d.ts +8 -0
  78. package/lib/rules/no-separate-loading-state.js +142 -0
  79. package/lib/rules/no-stale-state-across-await.d.ts +1 -0
  80. package/lib/rules/no-stale-state-across-await.js +185 -0
  81. package/lib/rules/no-static-constants-in-dynamic-files.d.ts +3 -0
  82. package/lib/rules/no-static-constants-in-dynamic-files.js +149 -0
  83. package/lib/rules/no-try-catch-already-exists-in-transaction.d.ts +1 -0
  84. package/lib/rules/no-try-catch-already-exists-in-transaction.js +342 -0
  85. package/lib/rules/no-undefined-null-passthrough.js +166 -19
  86. package/lib/rules/no-unnecessary-verb-suffix.js +0 -2
  87. package/lib/rules/no-unsafe-firestore-spread.js +8 -5
  88. package/lib/rules/no-unused-props.d.ts +3 -0
  89. package/lib/rules/no-unused-props.js +213 -78
  90. package/lib/rules/optimize-object-boolean-conditions.d.ts +1 -0
  91. package/lib/rules/optimize-object-boolean-conditions.js +234 -0
  92. package/lib/rules/parallelize-async-operations.d.ts +8 -0
  93. package/lib/rules/parallelize-async-operations.js +375 -0
  94. package/lib/rules/prefer-batch-operations.js +199 -33
  95. package/lib/rules/prefer-block-comments-for-declarations.js +13 -3
  96. package/lib/rules/prefer-document-flattening.d.ts +4 -0
  97. package/lib/rules/prefer-document-flattening.js +239 -0
  98. package/lib/rules/prefer-field-paths-in-transforms.d.ts +7 -0
  99. package/lib/rules/prefer-field-paths-in-transforms.js +250 -0
  100. package/lib/rules/prefer-fragment-component.js +3 -3
  101. package/lib/rules/prefer-global-router-state-key.d.ts +7 -3
  102. package/lib/rules/prefer-global-router-state-key.js +230 -31
  103. package/lib/rules/prefer-next-dynamic.d.ts +9 -0
  104. package/lib/rules/prefer-next-dynamic.js +366 -0
  105. package/lib/rules/prefer-nullish-coalescing-boolean-props.d.ts +1 -0
  106. package/lib/rules/prefer-nullish-coalescing-boolean-props.js +430 -0
  107. package/lib/rules/prefer-nullish-coalescing-override.d.ts +6 -0
  108. package/lib/rules/prefer-nullish-coalescing-override.js +182 -0
  109. package/lib/rules/prefer-params-over-parent-id.d.ts +5 -0
  110. package/lib/rules/prefer-params-over-parent-id.js +228 -0
  111. package/lib/rules/prefer-settings-object.js +23 -15
  112. package/lib/rules/prefer-type-alias-over-typeof-constant.d.ts +3 -0
  113. package/lib/rules/prefer-type-alias-over-typeof-constant.js +259 -0
  114. package/lib/rules/prefer-url-tostring-over-tojson.d.ts +2 -0
  115. package/lib/rules/prefer-url-tostring-over-tojson.js +166 -0
  116. package/lib/rules/prefer-use-deep-compare-memo.d.ts +4 -0
  117. package/lib/rules/prefer-use-deep-compare-memo.js +428 -0
  118. package/lib/rules/prefer-usecallback-over-usememo-for-functions.js +24 -1
  119. package/lib/rules/prefer-usememo-over-useeffect-usestate.js +24 -5
  120. package/lib/rules/react-usememo-should-be-component.d.ts +2 -1
  121. package/lib/rules/react-usememo-should-be-component.js +144 -78
  122. package/lib/rules/require-dynamic-firebase-imports.js +2 -1
  123. package/lib/rules/require-image-optimized.js +1 -1
  124. package/lib/rules/require-memo.js +11 -1
  125. package/lib/rules/require-usememo-object-literals.js +7 -1
  126. package/lib/rules/semantic-function-prefixes.js +38 -18
  127. package/lib/rules/sync-onwrite-name-func.js +5 -1
  128. package/lib/rules/test-file-location-enforcement.d.ts +1 -0
  129. package/lib/rules/test-file-location-enforcement.js +57 -0
  130. package/lib/rules/use-custom-link.js +6 -1
  131. package/lib/rules/use-custom-router.js +6 -1
  132. package/lib/rules/use-latest-callback.d.ts +3 -0
  133. package/lib/rules/use-latest-callback.js +270 -0
  134. package/lib/utils/ASTHelpers.js +10 -0
  135. package/lib/utils/createRule.js +1 -1
  136. package/lib/utils/graph/ClassGraphSorterReadability.js +1 -1
  137. package/lib/utils/ruleTester.d.ts +1 -0
  138. package/lib/utils/ruleTester.js +4 -1
  139. package/package.json +21 -14
  140. package/lib/rules/enforce-assertSafe-object-key.d.ts +0 -2
  141. package/lib/rules/no-restricted-imports-dynamic.d.ts +0 -25
  142. package/lib/rules/no-restricted-imports-dynamic.js +0 -213
@@ -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 isMemberExpression = (node) => {
8
- return node.type === utils_1.AST_NODE_TYPES.MemberExpression;
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
- // Track variables that are assigned realtimeDb references
11
- const realtimeDbRefVariables = new Set();
12
- const realtimeDbChildVariables = new Set();
13
- const isRealtimeDbRefAssignment = (node) => {
14
- if (node.type !== utils_1.AST_NODE_TYPES.VariableDeclarator)
15
- return false;
16
- const init = node.init;
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
- // Check for child() method calls on realtimeDb refs
33
- // e.g., const childRef = parentRef.child('path');
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 false;
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 isFirestoreMethodCall = (node) => {
73
- if (!isMemberExpression(node.callee))
74
- return false;
75
- const property = node.callee.property;
76
- if (!isIdentifier(property) || !FIRESTORE_METHODS.has(property.name)) {
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
- // Check for batch or transaction
94
- if (/batch|transaction/i.test(name)) {
44
+ if (firestoreCollectionVariables.has(baseIdentifier.name) ||
45
+ firestoreDocRefVariables.has(baseIdentifier.name)) {
95
46
  return true;
96
47
  }
97
48
  }
98
- // Check if the method is called on a realtimeDb reference
99
- if (isRealtimeDbReference(object)) {
100
- return false;
101
- }
102
- // Handle type assertions (as in the bug report)
103
- if (object.type === utils_1.AST_NODE_TYPES.TSAsExpression) {
104
- return true;
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
- // If we haven't found a doc/collection call yet, check if the object is a variable
135
- if (!foundDocOrCollection && isIdentifier(object)) {
136
- const name = object.name;
137
- // If the variable name contains 'doc' or 'ref', it's likely a Firestore reference
138
- // But exclude realtimeDb references
139
- if ((name.toLowerCase().includes('doc') || name.toLowerCase().includes('ref')) &&
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 foundDocOrCollection;
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: 'Use FirestoreFetcher or FirestoreDocFetcher instead of direct .get() calls',
165
- noDirectSet: 'Use DocSetter or DocSetterTransaction instead of direct .set() calls',
166
- noDirectUpdate: 'Use DocSetter or DocSetterTransaction instead of direct .update() calls',
167
- noDirectDelete: 'Use DocSetter or DocSetterTransaction instead of direct .delete() calls',
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
- // Clear the sets at the beginning of each file analysis
173
- realtimeDbRefVariables.clear();
174
- realtimeDbChildVariables.clear();
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
- context.report({
193
- node,
194
- messageId: 'noDirectGet',
195
- });
394
+ reportDirectFirestoreCall('noDirectGet', node, property.name, callee);
196
395
  break;
197
396
  case 'set':
198
- context.report({
199
- node,
200
- messageId: 'noDirectSet',
201
- });
397
+ reportDirectFirestoreCall('noDirectSet', node, property.name, callee);
202
398
  break;
203
399
  case 'update':
204
- context.report({
205
- node,
206
- messageId: 'noDirectUpdate',
207
- });
400
+ reportDirectFirestoreCall('noDirectUpdate', node, property.name, callee);
208
401
  break;
209
402
  case 'delete':
210
- context.report({
211
- node,
212
- messageId: 'noDirectDelete',
213
- });
403
+ reportDirectFirestoreCall('noDirectDelete', node, property.name, callee);
214
404
  break;
215
405
  }
216
406
  },
@@ -0,0 +1,3 @@
1
+ type MessageIds = 'useGetAccess' | 'requireGetDefault';
2
+ export declare const enforceFirestoreRulesGetAccess: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<MessageIds, [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
3
+ export {};
@@ -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