@blumintinc/eslint-plugin-blumint 1.13.0 → 1.15.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 (199) hide show
  1. package/README.md +169 -139
  2. package/lib/index.js +200 -9
  3. package/lib/rules/array-methods-this-context.js +1 -1
  4. package/lib/rules/avoid-utils-directory.js +0 -4
  5. package/lib/rules/class-methods-read-top-to-bottom.js +24 -5
  6. package/lib/rules/consistent-callback-naming.js +43 -4
  7. package/lib/rules/dynamic-https-errors.d.ts +1 -1
  8. package/lib/rules/dynamic-https-errors.js +134 -44
  9. package/lib/rules/enforce-assert-safe-object-key.js +11 -11
  10. package/lib/rules/enforce-boolean-naming-prefixes.js +11 -235
  11. package/lib/rules/enforce-callback-memo.js +1 -1
  12. package/lib/rules/enforce-centralized-mock-firestore.js +1 -1
  13. package/lib/rules/enforce-console-error.js +35 -7
  14. package/lib/rules/enforce-date-ttime.d.ts +1 -0
  15. package/lib/rules/enforce-date-ttime.js +156 -0
  16. package/lib/rules/enforce-dynamic-file-naming.d.ts +2 -0
  17. package/lib/rules/enforce-dynamic-file-naming.js +53 -28
  18. package/lib/rules/enforce-dynamic-firebase-imports.d.ts +2 -1
  19. package/lib/rules/enforce-dynamic-firebase-imports.js +3 -2
  20. package/lib/rules/enforce-early-destructuring.d.ts +2 -0
  21. package/lib/rules/enforce-early-destructuring.js +980 -0
  22. package/lib/rules/enforce-empty-object-check.d.ts +11 -0
  23. package/lib/rules/enforce-empty-object-check.js +502 -0
  24. package/lib/rules/enforce-exported-function-types.js +1 -1
  25. package/lib/rules/enforce-f-extension-for-entry-points.d.ts +8 -0
  26. package/lib/rules/enforce-f-extension-for-entry-points.js +283 -0
  27. package/lib/rules/enforce-fieldpath-syntax-in-docsetter.js +100 -50
  28. package/lib/rules/enforce-firestore-doc-ref-generic.js +15 -3
  29. package/lib/rules/enforce-firestore-facade.js +4 -2
  30. package/lib/rules/enforce-firestore-set-merge.js +3 -4
  31. package/lib/rules/enforce-global-constants.js +4 -4
  32. package/lib/rules/enforce-id-capitalization.js +1 -1
  33. package/lib/rules/enforce-identifiable-firestore-type.js +108 -58
  34. package/lib/rules/enforce-memoize-async.js +71 -21
  35. package/lib/rules/enforce-memoize-getters.js +39 -15
  36. package/lib/rules/enforce-microdiff.js +1 -1
  37. package/lib/rules/enforce-object-literal-as-const.js +4 -4
  38. package/lib/rules/enforce-positive-naming.js +4 -0
  39. package/lib/rules/enforce-props-argument-name.js +44 -18
  40. package/lib/rules/enforce-safe-stringify.js +1 -1
  41. package/lib/rules/enforce-serializable-params.js +3 -3
  42. package/lib/rules/enforce-singular-type-names.js +1 -1
  43. package/lib/rules/enforce-stable-hash-spread-props.d.ts +13 -0
  44. package/lib/rules/enforce-stable-hash-spread-props.js +342 -0
  45. package/lib/rules/enforce-storage-context.d.ts +9 -0
  46. package/lib/rules/enforce-storage-context.js +646 -0
  47. package/lib/rules/enforce-timestamp-now.js +23 -7
  48. package/lib/rules/enforce-transform-memoization.d.ts +4 -0
  49. package/lib/rules/enforce-transform-memoization.js +416 -0
  50. package/lib/rules/enforce-typescript-markdown-code-blocks.js +7 -3
  51. package/lib/rules/enforce-unique-cursor-headers.d.ts +16 -0
  52. package/lib/rules/enforce-unique-cursor-headers.js +365 -0
  53. package/lib/rules/enforce-verb-noun-naming.js +3818 -4644
  54. package/lib/rules/ensure-pointer-events-none.js +28 -2
  55. package/lib/rules/export-if-in-doubt.js +27 -4
  56. package/lib/rules/extract-global-constants.js +40 -13
  57. package/lib/rules/fast-deep-equal-over-microdiff.js +29 -39
  58. package/lib/rules/firestore-transaction-reads-before-writes.js +50 -4
  59. package/lib/rules/flatten-push-calls.d.ts +2 -0
  60. package/lib/rules/flatten-push-calls.js +428 -0
  61. package/lib/rules/global-const-style.js +66 -23
  62. package/lib/rules/jsdoc-above-field.d.ts +11 -0
  63. package/lib/rules/jsdoc-above-field.js +208 -0
  64. package/lib/rules/key-only-outermost-element.js +21 -3
  65. package/lib/rules/logical-top-to-bottom-grouping.d.ts +5 -0
  66. package/lib/rules/logical-top-to-bottom-grouping.js +1258 -0
  67. package/lib/rules/memo-compare-deeply-complex-props.d.ts +3 -0
  68. package/lib/rules/memo-compare-deeply-complex-props.js +801 -0
  69. package/lib/rules/memo-nested-react-components.d.ts +8 -0
  70. package/lib/rules/memo-nested-react-components.js +415 -0
  71. package/lib/rules/no-always-true-false-conditions.d.ts +2 -1
  72. package/lib/rules/no-always-true-false-conditions.js +68 -8
  73. package/lib/rules/no-array-length-in-deps.js +9 -3
  74. package/lib/rules/no-async-foreach.js +215 -12
  75. package/lib/rules/no-circular-references.d.ts +2 -1
  76. package/lib/rules/no-circular-references.js +31 -32
  77. package/lib/rules/no-class-instance-destructuring.js +55 -12
  78. package/lib/rules/no-complex-cloud-params.js +7 -3
  79. package/lib/rules/no-compositing-layer-props.js +2 -2
  80. package/lib/rules/no-conditional-literals-in-jsx.js +37 -12
  81. package/lib/rules/no-console-error.d.ts +9 -0
  82. package/lib/rules/no-console-error.js +527 -0
  83. package/lib/rules/no-curly-brackets-around-commented-properties.d.ts +2 -0
  84. package/lib/rules/no-curly-brackets-around-commented-properties.js +258 -0
  85. package/lib/rules/no-empty-dependency-use-callbacks.d.ts +11 -0
  86. package/lib/rules/no-empty-dependency-use-callbacks.js +576 -0
  87. package/lib/rules/no-entire-object-hook-deps.js +52 -7
  88. package/lib/rules/no-excessive-parent-chain.js +4 -1
  89. package/lib/rules/no-explicit-return-type.d.ts +2 -1
  90. package/lib/rules/no-explicit-return-type.js +215 -26
  91. package/lib/rules/no-filter-without-return.js +5 -1
  92. package/lib/rules/no-firestore-jest-mock.d.ts +2 -1
  93. package/lib/rules/no-firestore-jest-mock.js +126 -8
  94. package/lib/rules/no-firestore-object-arrays.js +67 -12
  95. package/lib/rules/no-handler-suffix.d.ts +12 -0
  96. package/lib/rules/no-handler-suffix.js +305 -0
  97. package/lib/rules/no-hungarian.js +1 -1
  98. package/lib/rules/no-inline-component-prop.d.ts +10 -0
  99. package/lib/rules/no-inline-component-prop.js +465 -0
  100. package/lib/rules/no-jsx-in-hooks.js +6 -1
  101. package/lib/rules/no-jsx-whitespace-literal.js +8 -2
  102. package/lib/rules/no-margin-properties.js +6 -6
  103. package/lib/rules/no-memoize-on-static.js +9 -1
  104. package/lib/rules/no-misleading-boolean-prefixes.js +7 -3
  105. package/lib/rules/no-misused-switch-case.js +22 -1
  106. package/lib/rules/no-mixed-firestore-transactions.d.ts +3 -1
  107. package/lib/rules/no-mixed-firestore-transactions.js +296 -34
  108. package/lib/rules/no-mock-firebase-admin.js +5 -2
  109. package/lib/rules/no-object-values-on-strings.js +10 -2
  110. package/lib/rules/no-overridable-method-calls-in-constructor.js +29 -17
  111. package/lib/rules/no-passthrough-getters.d.ts +2 -2
  112. package/lib/rules/no-passthrough-getters.js +120 -2
  113. package/lib/rules/no-redundant-annotation-assertion.d.ts +2 -0
  114. package/lib/rules/no-redundant-annotation-assertion.js +402 -0
  115. package/lib/rules/no-redundant-param-types.js +16 -6
  116. package/lib/rules/no-redundant-this-params.d.ts +3 -0
  117. package/lib/rules/no-redundant-this-params.js +508 -0
  118. package/lib/rules/no-redundant-usecallback-wrapper.js +16 -3
  119. package/lib/rules/no-res-error-status-in-onrequest.d.ts +4 -0
  120. package/lib/rules/no-res-error-status-in-onrequest.js +521 -0
  121. package/lib/rules/no-restricted-properties-fix.js +11 -10
  122. package/lib/rules/no-separate-loading-state.js +7 -16
  123. package/lib/rules/no-stale-state-across-await.js +1 -1
  124. package/lib/rules/no-type-assertion-returns.js +43 -49
  125. package/lib/rules/no-undefined-null-passthrough.js +61 -31
  126. package/lib/rules/no-unmemoized-memo-without-props.d.ts +8 -0
  127. package/lib/rules/no-unmemoized-memo-without-props.js +426 -0
  128. package/lib/rules/no-unnecessary-destructuring-rename.d.ts +2 -0
  129. package/lib/rules/no-unnecessary-destructuring-rename.js +344 -0
  130. package/lib/rules/no-unnecessary-destructuring.js +14 -4
  131. package/lib/rules/no-unnecessary-verb-suffix.js +2 -1
  132. package/lib/rules/no-unpinned-dependencies.js +36 -5
  133. package/lib/rules/no-unused-props.d.ts +2 -2
  134. package/lib/rules/no-unused-props.js +295 -91
  135. package/lib/rules/no-unused-usestate.js +6 -2
  136. package/lib/rules/no-useless-fragment.js +28 -2
  137. package/lib/rules/no-useless-usememo-primitives.d.ts +9 -0
  138. package/lib/rules/no-useless-usememo-primitives.js +393 -0
  139. package/lib/rules/no-usememo-for-pass-by-value.d.ts +13 -0
  140. package/lib/rules/no-usememo-for-pass-by-value.js +757 -0
  141. package/lib/rules/no-uuidv4-base62-as-key.js +18 -13
  142. package/lib/rules/omit-index-html.d.ts +2 -1
  143. package/lib/rules/omit-index-html.js +62 -7
  144. package/lib/rules/optimize-object-boolean-conditions.js +6 -4
  145. package/lib/rules/parallelize-async-operations.js +143 -59
  146. package/lib/rules/prefer-batch-operations.d.ts +1 -3
  147. package/lib/rules/prefer-batch-operations.js +32 -5
  148. package/lib/rules/prefer-block-comments-for-declarations.js +9 -4
  149. package/lib/rules/prefer-clone-deep.js +3 -3
  150. package/lib/rules/prefer-destructuring-no-class.d.ts +2 -1
  151. package/lib/rules/prefer-destructuring-no-class.js +244 -59
  152. package/lib/rules/prefer-docsetter-setall.d.ts +2 -0
  153. package/lib/rules/prefer-docsetter-setall.js +243 -0
  154. package/lib/rules/prefer-field-paths-in-transforms.js +4 -3
  155. package/lib/rules/prefer-fragment-component.js +1 -1
  156. package/lib/rules/prefer-fragment-shorthand.js +2 -1
  157. package/lib/rules/prefer-getter-over-parameterless-method.d.ts +13 -0
  158. package/lib/rules/prefer-getter-over-parameterless-method.js +648 -0
  159. package/lib/rules/prefer-global-router-state-key.js +88 -13
  160. package/lib/rules/prefer-memoized-props.d.ts +3 -0
  161. package/lib/rules/prefer-memoized-props.js +445 -0
  162. package/lib/rules/prefer-next-dynamic.js +60 -69
  163. package/lib/rules/prefer-nullish-coalescing-boolean-props.js +118 -8
  164. package/lib/rules/prefer-nullish-coalescing-override.d.ts +2 -1
  165. package/lib/rules/prefer-nullish-coalescing-override.js +11 -5
  166. package/lib/rules/prefer-params-over-parent-id.js +221 -172
  167. package/lib/rules/prefer-settings-object.js +27 -10
  168. package/lib/rules/prefer-type-alias-over-typeof-constant.js +9 -0
  169. package/lib/rules/prefer-type-over-interface.js +7 -2
  170. package/lib/rules/prefer-url-tostring-over-tojson.js +6 -3
  171. package/lib/rules/prefer-use-deep-compare-memo.js +8 -5
  172. package/lib/rules/prefer-usecallback-over-usememo-for-functions.js +1 -1
  173. package/lib/rules/prefer-usememo-over-useeffect-usestate.js +1 -1
  174. package/lib/rules/prefer-utility-function-over-private-static.js +34 -2
  175. package/lib/rules/prevent-children-clobber.d.ts +2 -0
  176. package/lib/rules/prevent-children-clobber.js +540 -0
  177. package/lib/rules/react-memoize-literals.d.ts +4 -0
  178. package/lib/rules/react-memoize-literals.js +614 -0
  179. package/lib/rules/react-usememo-should-be-component.js +2 -2
  180. package/lib/rules/require-hooks-default-params.js +17 -16
  181. package/lib/rules/require-https-error-cause.d.ts +4 -0
  182. package/lib/rules/require-https-error-cause.js +155 -0
  183. package/lib/rules/require-https-error.js +43 -21
  184. package/lib/rules/require-image-optimized.js +1 -1
  185. package/lib/rules/require-memo.js +18 -10
  186. package/lib/rules/require-memoize-jsx-returners.d.ts +3 -0
  187. package/lib/rules/require-memoize-jsx-returners.js +485 -0
  188. package/lib/rules/require-usememo-object-literals.js +2 -3
  189. package/lib/rules/test-file-location-enforcement.js +1 -1
  190. package/lib/rules/use-latest-callback.js +6 -5
  191. package/lib/rules/vertically-group-related-functions.d.ts +16 -0
  192. package/lib/rules/vertically-group-related-functions.js +480 -0
  193. package/lib/utils/ASTHelpers.d.ts +47 -2
  194. package/lib/utils/ASTHelpers.js +370 -112
  195. package/lib/utils/getMethodName.d.ts +27 -0
  196. package/lib/utils/getMethodName.js +35 -0
  197. package/lib/utils/tsTypeClassifier.d.ts +30 -0
  198. package/lib/utils/tsTypeClassifier.js +149 -0
  199. package/package.json +3 -1
@@ -0,0 +1,283 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.enforceFExtensionForEntryPoints = void 0;
7
+ const utils_1 = require("@typescript-eslint/utils");
8
+ const createRule_1 = require("../utils/createRule");
9
+ const path_1 = __importDefault(require("path"));
10
+ const ASTHelpers_1 = require("../utils/ASTHelpers");
11
+ const DEFAULT_ENTRY_POINTS = [
12
+ 'onCall',
13
+ 'onCallVaripotent',
14
+ 'onRequest',
15
+ 'onQueueTask',
16
+ 'onWebhook',
17
+ 'sequentialDocumentWritten',
18
+ 'onDocumentWritten',
19
+ 'onDocumentCreated',
20
+ 'onDocumentDeleted',
21
+ 'onDocumentUpdated',
22
+ 'onSchedule',
23
+ 'onValueWritten',
24
+ 'onValueCreated',
25
+ 'onValueUpdated',
26
+ 'onValueDeleted',
27
+ 'sequentialValueWritten',
28
+ 'sequentialValueCreated',
29
+ 'sequentialValueUpdated',
30
+ 'sequentialValueDeleted',
31
+ ];
32
+ /**
33
+ * Entry points must be defined at the top level because Firebase deployment
34
+ * discovers and registers them by evaluating the module scope. Nested definitions
35
+ * inside functions or classes won't be discovered during the deployment process.
36
+ */
37
+ function isTopLevel(node) {
38
+ let current = node.parent;
39
+ while (current) {
40
+ if (current.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
41
+ current.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
42
+ current.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
43
+ current.type === utils_1.AST_NODE_TYPES.ClassDeclaration ||
44
+ current.type === utils_1.AST_NODE_TYPES.ClassExpression ||
45
+ current.type === utils_1.AST_NODE_TYPES.MethodDefinition) {
46
+ return false;
47
+ }
48
+ current = current.parent;
49
+ }
50
+ return true;
51
+ }
52
+ /**
53
+ * Checks if a node directly defines an entry point as a function or variable declaration.
54
+ * This exempts wrapper implementation files (e.g., functions/src/v2/) but intentionally
55
+ * does not exempt re-export files (e.g., `const myCall = onCall; export { myCall }`).
56
+ * Re-export files that also invoke entry points must still follow the .f.ts naming convention
57
+ * and will be flagged by the CallExpression handler.
58
+ *
59
+ * This function specifically checks AST node types FunctionDeclaration and
60
+ * VariableDeclaration but does not catch ExportSpecifiers used in re-exports.
61
+ */
62
+ function isNodeDefiningEntryPoint(node, entryPoints) {
63
+ // Handle function declaration: export function onCall() {}
64
+ if (node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration &&
65
+ node.id &&
66
+ entryPoints.has(node.id.name)) {
67
+ return true;
68
+ }
69
+ // Handle variable declaration: export const onCall = ...
70
+ if (node.type === utils_1.AST_NODE_TYPES.VariableDeclaration) {
71
+ return node.declarations.some((decl) => decl.id.type === utils_1.AST_NODE_TYPES.Identifier &&
72
+ entryPoints.has(decl.id.name));
73
+ }
74
+ return false;
75
+ }
76
+ /**
77
+ * Obtains the scope for a node in an ESLint 9+ compatible way.
78
+ *
79
+ * Note: In ESLint 8, falls back to context.getScope() which returns the scope
80
+ * of the currently visited node—only call this from within visitor handlers
81
+ * for the target node.
82
+ */
83
+ function getScopeForNode(context, node) {
84
+ const sourceCode = context.sourceCode ?? context.getSourceCode();
85
+ if (isEslint9OrLater(sourceCode)) {
86
+ return getEslint9Scope(sourceCode, node);
87
+ }
88
+ return context.getScope();
89
+ }
90
+ /**
91
+ * Checks if the current environment is ESLint 9 or later.
92
+ */
93
+ function isEslint9OrLater(sourceCode) {
94
+ return typeof sourceCode.getScope === 'function';
95
+ }
96
+ /**
97
+ * Gets the scope for a node using the ESLint 9+ SourceCode.getScope() method.
98
+ */
99
+ function getEslint9Scope(sourceCode, node) {
100
+ return sourceCode.getScope(node);
101
+ }
102
+ /**
103
+ * Gets the import definition for a given name in the current scope.
104
+ */
105
+ function getImportDef(context, calleeName, node) {
106
+ const scope = getScopeForNode(context, node);
107
+ const variable = ASTHelpers_1.ASTHelpers.findVariableInScope(scope, calleeName);
108
+ if (!variable) {
109
+ return null;
110
+ }
111
+ return variable.defs.find((def) => def.type === 'ImportBinding') ?? null;
112
+ }
113
+ /**
114
+ * Checks if the import comes from an allowed Firebase or internal wrapper source.
115
+ */
116
+ function isFromAllowedSource(importDef) {
117
+ if (!importDef || !importDef.parent) {
118
+ return false;
119
+ }
120
+ const importDeclaration = importDef.parent;
121
+ if (importDeclaration.type !== utils_1.AST_NODE_TYPES.ImportDeclaration) {
122
+ return false;
123
+ }
124
+ const source = importDeclaration.source.value;
125
+ return (source.startsWith('firebase-functions') ||
126
+ /(^|\/)v2(\/|$)/.test(source) ||
127
+ /(^|\/)util\/webhook(\/|$)/.test(source));
128
+ }
129
+ /**
130
+ * Helper function to extract name from import specifier.
131
+ */
132
+ function getNameFromImportSpecifier(node) {
133
+ const { imported } = node;
134
+ if (imported.type === utils_1.AST_NODE_TYPES.Identifier) {
135
+ return imported.name;
136
+ }
137
+ // Handle Literal imports (string-named imports in ESLint 8+)
138
+ const importedNode = imported;
139
+ if (importedNode.type === utils_1.AST_NODE_TYPES.Literal &&
140
+ typeof importedNode.value === 'string') {
141
+ return importedNode.value;
142
+ }
143
+ return undefined;
144
+ }
145
+ /**
146
+ * Helper function to extract name from module path.
147
+ */
148
+ function getNameFromModulePath(importDeclaration, entryPoints) {
149
+ const modulePath = importDeclaration.source.value;
150
+ // Extract the module name from the path.
151
+ // Example: '../../v2/https/onCall' -> 'onCall'
152
+ const match = modulePath.match(/[/\\]([^/\\]+)$/);
153
+ const segment = (match ? match[1] : modulePath).replace(/\.[jt]sx?$/, '');
154
+ return entryPoints.has(segment) ? segment : undefined;
155
+ }
156
+ /**
157
+ * Resolves the original name of the imported function, handling aliases and default imports.
158
+ */
159
+ function getOriginalName(importDef, calleeName, entryPoints) {
160
+ if (!importDef || !importDef.node) {
161
+ return calleeName;
162
+ }
163
+ // Handle named imports: import { onCall as myCall } from ...
164
+ if (importDef.node.type === utils_1.AST_NODE_TYPES.ImportSpecifier) {
165
+ return getNameFromImportSpecifier(importDef.node) ?? calleeName;
166
+ }
167
+ // Handle default imports: import onCall from '../../v2/https/onCall'
168
+ // or namespace imports: import * as onCall from '../../v2/https/onCall'
169
+ if (importDef.node.type === utils_1.AST_NODE_TYPES.ImportDefaultSpecifier ||
170
+ importDef.node.type === utils_1.AST_NODE_TYPES.ImportNamespaceSpecifier) {
171
+ return (getNameFromModulePath(importDef.parent, entryPoints) ?? calleeName);
172
+ }
173
+ return calleeName;
174
+ }
175
+ exports.enforceFExtensionForEntryPoints = (0, createRule_1.createRule)({
176
+ name: 'enforce-f-extension-for-entry-points',
177
+ meta: {
178
+ type: 'problem',
179
+ docs: {
180
+ description: 'Enforce .f.ts extension for entry points',
181
+ recommended: 'error',
182
+ },
183
+ schema: [
184
+ {
185
+ type: 'object',
186
+ properties: {
187
+ entryPoints: {
188
+ type: 'array',
189
+ items: { type: 'string' },
190
+ },
191
+ },
192
+ additionalProperties: false,
193
+ },
194
+ ],
195
+ messages: {
196
+ requireFExtension: 'File "{{fileName}}" contains a Firebase Cloud Function entry point "{{entryPoint}}" but lacks the ".f.ts" extension. ' +
197
+ 'Entry points must use ".f.ts" to clearly mark the public function surface to avoid accidental export/name collisions, ' +
198
+ 'unintended exposure of internal implementation during deployment, confusion during imports/tests, and maintenance bugs. ' +
199
+ 'Rename this file to "{{suggestedName}}" to mark it as a public entry point.',
200
+ },
201
+ },
202
+ defaultOptions: [{ entryPoints: DEFAULT_ENTRY_POINTS }],
203
+ create(context, [options]) {
204
+ const filePath = context.filename ??
205
+ context.getFilename();
206
+ const fileName = path_1.default.basename(filePath);
207
+ const entryPoints = new Set(options.entryPoints?.length ? options.entryPoints : DEFAULT_ENTRY_POINTS);
208
+ // Only apply to files under functions/src/
209
+ const normalizedPath = filePath.replace(/\\/g, '/');
210
+ if (!normalizedPath.includes('/functions/src/') &&
211
+ !normalizedPath.startsWith('functions/src/')) {
212
+ return {};
213
+ }
214
+ // Exclude test files and declaration files
215
+ if (/\.(test|spec)\.tsx?$|\.d\.ts$/.test(fileName)) {
216
+ return {};
217
+ }
218
+ // Only apply to .ts and .tsx files, but not files already using .f.ts or .f.tsx
219
+ const isTsOrTsx = /\.tsx?$/.test(fileName);
220
+ const isFExtension = /\.f\.tsx?$/.test(fileName);
221
+ if (!isTsOrTsx || isFExtension) {
222
+ return {};
223
+ }
224
+ let isDefiningEntryPoint = false;
225
+ let reported = false;
226
+ return {
227
+ ExportNamedDeclaration(node) {
228
+ if (isDefiningEntryPoint || !node.declaration) {
229
+ return;
230
+ }
231
+ if (isNodeDefiningEntryPoint(node.declaration, entryPoints)) {
232
+ isDefiningEntryPoint = true;
233
+ }
234
+ },
235
+ ExportDefaultDeclaration(node) {
236
+ if (isDefiningEntryPoint) {
237
+ return;
238
+ }
239
+ if (isNodeDefiningEntryPoint(node.declaration, entryPoints)) {
240
+ isDefiningEntryPoint = true;
241
+ }
242
+ },
243
+ CallExpression(node) {
244
+ if (reported || isDefiningEntryPoint) {
245
+ return;
246
+ }
247
+ // We only care about simple identifier calls (e.g., onCall())
248
+ if (node.callee.type !== utils_1.AST_NODE_TYPES.Identifier) {
249
+ return;
250
+ }
251
+ const calleeName = node.callee.name;
252
+ // Optimization: Early return if it's not a top-level call
253
+ if (!isTopLevel(node)) {
254
+ return;
255
+ }
256
+ // Variable lookup logic and import source validation
257
+ const importDef = getImportDef(context, calleeName, node);
258
+ if (!importDef || !isFromAllowedSource(importDef)) {
259
+ return;
260
+ }
261
+ // Entry point checking
262
+ const originalName = getOriginalName(importDef, calleeName, entryPoints);
263
+ if (!entryPoints.has(originalName)) {
264
+ return;
265
+ }
266
+ reported = true;
267
+ const suggestedName = fileName.replace(/\.tsx?$/, '.f$&');
268
+ context.report({
269
+ node,
270
+ messageId: 'requireFExtension',
271
+ data: {
272
+ fileName,
273
+ entryPoint: calleeName === originalName
274
+ ? calleeName
275
+ : `${calleeName} (${originalName})`,
276
+ suggestedName,
277
+ },
278
+ });
279
+ },
280
+ };
281
+ },
282
+ });
283
+ //# sourceMappingURL=enforce-f-extension-for-entry-points.js.map
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.enforceFieldPathSyntaxInDocSetter = void 0;
4
4
  const utils_1 = require("@typescript-eslint/utils");
5
5
  const createRule_1 = require("../utils/createRule");
6
+ const getMethodName_1 = require("../utils/getMethodName");
6
7
  exports.enforceFieldPathSyntaxInDocSetter = (0, createRule_1.createRule)({
7
8
  name: 'enforce-fieldpath-syntax-in-docsetter',
8
9
  meta: {
@@ -16,11 +17,12 @@ exports.enforceFieldPathSyntaxInDocSetter = (0, createRule_1.createRule)({
16
17
  fixable: 'code',
17
18
  schema: [],
18
19
  messages: {
19
- enforceFieldPathSyntax: 'Use FieldPath syntax (dot notation) for nested fields in DocSetter. Instead of `{ roles: { contributor: value } }`, use `{ "roles.contributor": value }`.',
20
+ enforceFieldPathSyntax: 'What’s wrong: DocSetter {{methodName}} receives nested object data under "{{topLevelKey}}". → Why it matters: Firestore treats that nested map as a whole sub-document write, so partial updates can overwrite sibling fields you did not include. How to fix: Flatten nested properties into FieldPath (dot) keys before passing documentData (e.g., "{{exampleFieldPath}}") so only the intended leaves are written.',
20
21
  },
21
22
  },
22
23
  defaultOptions: [],
23
24
  create(context) {
25
+ const sourceCode = context.getSourceCode();
24
26
  // Track DocSetter variables
25
27
  const docSetterVariables = new Set();
26
28
  // Helper function to check if a node is a DocSetter method call
@@ -51,45 +53,14 @@ exports.enforceFieldPathSyntaxInDocSetter = (0, createRule_1.createRule)({
51
53
  return (prop.type === utils_1.AST_NODE_TYPES.SpreadElement ||
52
54
  (prop.type === utils_1.AST_NODE_TYPES.Property && prop.computed === true));
53
55
  }
54
- // Helper function to check if an object has nested objects (excluding arrays)
55
- function hasNestedObjects(node, prefix = '') {
56
- for (const property of node.properties) {
57
- // Skip spread elements
58
- if (property.type === utils_1.AST_NODE_TYPES.SpreadElement) {
59
- continue;
60
- }
61
- if (property.type !== utils_1.AST_NODE_TYPES.Property) {
62
- continue;
63
- }
64
- // Skip computed properties (dynamic keys)
65
- if (property.computed) {
66
- continue;
67
- }
68
- const isNumericKey = property.key.type === utils_1.AST_NODE_TYPES.Literal &&
69
- (typeof property.key.value === 'number' ||
70
- (typeof property.key.value === 'string' &&
71
- /^\d+$/.test(property.key.value)));
72
- // Allow top-level numeric keys (treated like array indexes)
73
- if (prefix === '' && isNumericKey) {
74
- continue;
75
- }
76
- const value = property.value;
77
- // Skip if the property key is already using dot notation
78
- if (property.key.type === utils_1.AST_NODE_TYPES.Literal &&
79
- typeof property.key.value === 'string' &&
80
- property.key.value.includes('.')) {
81
- continue;
82
- }
83
- // Check if the value is an object (but not an array)
84
- if (value.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
85
- // Skip nested objects that contain spread elements or computed properties
86
- const hasSpreadOrComputed = value.properties.some((prop) => isSpreadOrComputed(prop));
87
- if (!hasSpreadOrComputed) {
88
- return true;
89
- }
90
- }
91
- }
92
- return false;
56
+ function isNumericKey(property) {
57
+ return (property.key.type === utils_1.AST_NODE_TYPES.Literal &&
58
+ (typeof property.key.value === 'number' ||
59
+ (typeof property.key.value === 'string' &&
60
+ /^\d+$/.test(property.key.value))));
61
+ }
62
+ function hasRootNumericKey(node) {
63
+ return node.properties.some((property) => property.type === utils_1.AST_NODE_TYPES.Property && isNumericKey(property));
93
64
  }
94
65
  // Helper function to flatten nested objects into FieldPath syntax
95
66
  function flattenObject(obj, sourceCode, prefix = '') {
@@ -127,11 +98,8 @@ exports.enforceFieldPathSyntaxInDocSetter = (0, createRule_1.createRule)({
127
98
  // Skip other key types
128
99
  continue;
129
100
  }
130
- const isNumericKey = property.key.type === utils_1.AST_NODE_TYPES.Literal &&
131
- (typeof property.key.value === 'number' ||
132
- (typeof property.key.value === 'string' &&
133
- /^\d+$/.test(property.key.value)));
134
- if (prefix === '' && isNumericKey) {
101
+ const numericKey = isNumericKey(property);
102
+ if (prefix === '' && numericKey) {
135
103
  continue;
136
104
  }
137
105
  const fullKey = prefix ? `${prefix}.${key}` : key;
@@ -152,11 +120,13 @@ exports.enforceFieldPathSyntaxInDocSetter = (0, createRule_1.createRule)({
152
120
  return key.includes('.') || !/^(?:[$_A-Za-z][$\w]*)$/u.test(key);
153
121
  }
154
122
  // Helper function to convert an object to FieldPath syntax
155
- function convertToFieldPathSyntax(node, sourceCode) {
123
+ function convertToFieldPathSyntax(node, sourceCode, preflattened) {
156
124
  const idProperty = node.properties.find((prop) => prop.type === utils_1.AST_NODE_TYPES.Property &&
157
125
  prop.key.type === utils_1.AST_NODE_TYPES.Identifier &&
158
126
  prop.key.name === 'id');
159
- const flattenedProperties = flattenObject(node, sourceCode);
127
+ const flattenedProperties = {
128
+ ...(preflattened ?? flattenObject(node, sourceCode)),
129
+ };
160
130
  if (idProperty && idProperty.type === utils_1.AST_NODE_TYPES.Property) {
161
131
  delete flattenedProperties['id'];
162
132
  }
@@ -175,6 +145,72 @@ exports.enforceFieldPathSyntaxInDocSetter = (0, createRule_1.createRule)({
175
145
  result += '}';
176
146
  return result;
177
147
  }
148
+ function getPropertyKeyText(property) {
149
+ if (property.key.type === utils_1.AST_NODE_TYPES.Identifier) {
150
+ return property.key.name;
151
+ }
152
+ if (property.key.type === utils_1.AST_NODE_TYPES.Literal &&
153
+ (typeof property.key.value === 'string' ||
154
+ typeof property.key.value === 'number')) {
155
+ return String(property.key.value);
156
+ }
157
+ return undefined;
158
+ }
159
+ function getFirstNestedObjectProperty(node, keyPredicate) {
160
+ for (const property of node.properties) {
161
+ if (isSpreadOrComputed(property)) {
162
+ continue;
163
+ }
164
+ if (property.type !== utils_1.AST_NODE_TYPES.Property) {
165
+ continue;
166
+ }
167
+ const propertyKeyText = getPropertyKeyText(property);
168
+ if (!propertyKeyText) {
169
+ continue;
170
+ }
171
+ if (keyPredicate && !keyPredicate(propertyKeyText)) {
172
+ continue;
173
+ }
174
+ // Root-level numeric keys typically model array-style buckets rather than
175
+ // Firestore document fields, so ignore them when identifying nested objects
176
+ if (node.parent?.type !== utils_1.AST_NODE_TYPES.Property &&
177
+ isNumericKey(property)) {
178
+ continue;
179
+ }
180
+ if (property.value.type !== utils_1.AST_NODE_TYPES.ObjectExpression) {
181
+ continue;
182
+ }
183
+ const hasSpreadOrComputed = property.value.properties.some((prop) => isSpreadOrComputed(prop));
184
+ if (hasSpreadOrComputed) {
185
+ continue;
186
+ }
187
+ return property;
188
+ }
189
+ return undefined;
190
+ }
191
+ function extractViolationDetails(firstArg, sourceCode) {
192
+ const firstNestedPropertyWithoutDots = getFirstNestedObjectProperty(firstArg, (key) => !key.includes('.'));
193
+ const firstNestedProperty = firstNestedPropertyWithoutDots ||
194
+ getFirstNestedObjectProperty(firstArg);
195
+ if (!firstNestedProperty) {
196
+ return null;
197
+ }
198
+ const propertyKeyText = getPropertyKeyText(firstNestedProperty);
199
+ const exampleFieldPathFromProperty = propertyKeyText &&
200
+ flattenObject(firstNestedProperty.value, sourceCode, propertyKeyText);
201
+ const flattenedProperties = flattenObject(firstArg, sourceCode);
202
+ const exampleFieldPath = (exampleFieldPathFromProperty &&
203
+ Object.keys(exampleFieldPathFromProperty).find((key) => key.includes('.'))) ??
204
+ (propertyKeyText &&
205
+ Object.keys(flattenedProperties).find((key) => key.startsWith(`${propertyKeyText}.`))) ??
206
+ Object.keys(flattenedProperties).find((key) => key.includes('.')) ??
207
+ 'field.nested';
208
+ return {
209
+ topLevelKey: propertyKeyText ?? 'nested field',
210
+ exampleFieldPath,
211
+ flattenedProperties,
212
+ };
213
+ }
178
214
  return {
179
215
  // Track DocSetter variable declarations
180
216
  VariableDeclarator(node) {
@@ -192,16 +228,30 @@ exports.enforceFieldPathSyntaxInDocSetter = (0, createRule_1.createRule)({
192
228
  }
193
229
  // Check if the first argument is an object literal
194
230
  const firstArg = node.arguments[0];
195
- if (firstArg?.type !== utils_1.AST_NODE_TYPES.ObjectExpression ||
196
- !hasNestedObjects(firstArg)) {
231
+ if (firstArg?.type !== utils_1.AST_NODE_TYPES.ObjectExpression) {
232
+ return;
233
+ }
234
+ if (hasRootNumericKey(firstArg)) {
235
+ return;
236
+ }
237
+ const violationDetails = extractViolationDetails(firstArg, sourceCode);
238
+ if (!violationDetails) {
197
239
  return;
198
240
  }
199
241
  // Report and fix the issue
242
+ const callee = node.callee;
200
243
  context.report({
201
244
  node: firstArg,
202
245
  messageId: 'enforceFieldPathSyntax',
246
+ data: {
247
+ methodName: `${(0, getMethodName_1.getMemberExpressionName)(callee, sourceCode, {
248
+ computedFallbackToText: false,
249
+ }) || 'set'}()`,
250
+ topLevelKey: violationDetails.topLevelKey,
251
+ exampleFieldPath: violationDetails.exampleFieldPath,
252
+ },
203
253
  fix(fixer) {
204
- const newText = convertToFieldPathSyntax(firstArg, context.getSourceCode());
254
+ const newText = convertToFieldPathSyntax(firstArg, sourceCode, violationDetails.flattenedProperties);
205
255
  return fixer.replaceText(firstArg, newText);
206
256
  },
207
257
  });
@@ -24,8 +24,20 @@ exports.enforceFirestoreDocRefGeneric = (0, createRule_1.createRule)({
24
24
  },
25
25
  schema: [],
26
26
  messages: {
27
- missingGeneric: '{{ type }} must specify a generic type argument for type safety. Instead of `const docRef = doc(collection)`, use `const docRef = doc<YourType>(collection)`.',
28
- invalidGeneric: '{{ type }} must not use "any" or "{}" as generic type argument. Define a proper interface/type for your document: `interface UserDoc { name: string; age: number; }` and use it: `const docRef = doc<UserDoc>(collection)`.',
27
+ missingGeneric: [
28
+ "What's wrong: {{ type }} is missing its document schema generic (the document data type).",
29
+ '',
30
+ 'Why it matters: Without the generic, Firestore references fall back to loose DocumentData, so TypeScript cannot catch field typos or missing required properties before they reach Firestore.',
31
+ '',
32
+ 'How to fix: Add the document interface/type as the generic (e.g., const ref: {{ type }}<UserDoc> = ... or doc<UserDoc>(collection)).',
33
+ ].join('\n'),
34
+ invalidGeneric: [
35
+ 'What\'s wrong: {{ type }} uses "any" or an empty object ({}) in its schema generic.',
36
+ '',
37
+ 'Why it matters: This erases the document schema and disables TypeScript checks on Firestore reads and writes, so malformed payloads and missing fields can pass silently.',
38
+ '',
39
+ 'How to fix: Define a concrete interface/type for the document (e.g., interface UserDoc { name: string }) and use it as the generic instead of "any" or {}.',
40
+ ].join('\n'),
29
41
  },
30
42
  },
31
43
  defaultOptions: [],
@@ -61,7 +73,7 @@ exports.enforceFirestoreDocRefGeneric = (0, createRule_1.createRule)({
61
73
  }
62
74
  // Prevent infinite recursion
63
75
  typeCache.set(typeName, false);
64
- const program = context.getSourceCode().ast;
76
+ const program = context.sourceCode.ast;
65
77
  const interfaceDecl = program.body.find((n) => n.type === utils_1.AST_NODE_TYPES.TSInterfaceDeclaration &&
66
78
  n.id.name === typeName);
67
79
  if (interfaceDecl) {
@@ -94,7 +94,7 @@ exports.enforceFirestoreFacade = (0, createRule_1.createRule)({
94
94
  const firestoreTransactionVariables = new Set();
95
95
  const docSetterVariables = new Set();
96
96
  const batchManagerVariables = new Set();
97
- const sourceCode = context.getSourceCode();
97
+ const sourceCode = context.sourceCode;
98
98
  const clearFirestoreTrackingFor = (name) => {
99
99
  firestoreDocRefVariables.delete(name);
100
100
  firestoreCollectionVariables.delete(name);
@@ -149,7 +149,9 @@ exports.enforceFirestoreFacade = (0, createRule_1.createRule)({
149
149
  return true;
150
150
  }
151
151
  if (target.type === utils_1.AST_NODE_TYPES.Identifier &&
152
- (target.name === 'transaction' || target.name === 'tx' || target.name === 't')) {
152
+ (target.name === 'transaction' ||
153
+ target.name === 'tx' ||
154
+ target.name === 't')) {
153
155
  firestoreTransactionVariables.add(varName);
154
156
  return true;
155
157
  }
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.enforceFirestoreSetMerge = void 0;
4
4
  const utils_1 = require("@typescript-eslint/utils");
5
5
  const createRule_1 = require("../utils/createRule");
6
+ const ASTHelpers_1 = require("../utils/ASTHelpers");
6
7
  exports.enforceFirestoreSetMerge = (0, createRule_1.createRule)({
7
8
  name: 'enforce-firestore-set-merge',
8
9
  meta: {
@@ -93,9 +94,7 @@ exports.enforceFirestoreSetMerge = (0, createRule_1.createRule)({
93
94
  return true;
94
95
  }
95
96
  // Check if it's a Firestore document reference by looking at imports
96
- const program = context
97
- .getAncestors()
98
- .find((node) => node.type === utils_1.AST_NODE_TYPES.Program);
97
+ const program = ASTHelpers_1.ASTHelpers.getAncestors(context, node).find((node) => node.type === utils_1.AST_NODE_TYPES.Program);
99
98
  if (program) {
100
99
  for (const node of program.body) {
101
100
  if (node.type === utils_1.AST_NODE_TYPES.VariableDeclaration) {
@@ -212,7 +211,7 @@ exports.enforceFirestoreSetMerge = (0, createRule_1.createRule)({
212
211
  node,
213
212
  messageId: 'preferSetMerge',
214
213
  fix(fixer) {
215
- const newText = convertUpdateToSetMerge(node, context.getSourceCode());
214
+ const newText = convertUpdateToSetMerge(node, context.sourceCode);
216
215
  return fixer.replaceText(node, newText);
217
216
  },
218
217
  });
@@ -9,19 +9,19 @@ exports.enforceGlobalConstants = (0, createRule_1.createRule)({
9
9
  meta: {
10
10
  type: 'suggestion',
11
11
  docs: {
12
- description: 'Enforce using global static constants instead of useMemo with empty dependency arrays for object literals, and extract inline destructuring defaults in React components/hooks to global constants',
12
+ description: 'Enforce global static constants for React components/hooks',
13
13
  recommended: 'error',
14
14
  },
15
15
  fixable: 'code',
16
16
  schema: [],
17
17
  messages: {
18
- useGlobalConstant: 'Use a global static constant instead of useMemo with an empty dependency array for object literals',
19
- extractDefaultToGlobalConstant: 'Extract inline default value to a module-level constant for stable reference',
18
+ useGlobalConstant: 'Object literal returned from useMemo with empty dependencies creates a new reference every render without providing memoization benefits this wastes memory and misleads readers into thinking the value is computed → move the object to a module-level constant (e.g., const OPTIONS = { ... } as const;).',
19
+ extractDefaultToGlobalConstant: 'Inline default value in destructuring creates a new reference on every render → this causes unnecessary re-renders in child components due to unstable identity → extract the default to a module-level constant (e.g., const DEFAULT_OPTIONS = { ... } as const;).',
20
20
  },
21
21
  },
22
22
  defaultOptions: [],
23
23
  create(context) {
24
- const sourceCode = context.getSourceCode();
24
+ const sourceCode = context.sourceCode;
25
25
  function isHookName(name) {
26
26
  return /^use[A-Z]/.test(name);
27
27
  }
@@ -19,7 +19,7 @@ exports.enforceIdCapitalization = (0, createRule_1.createRule)({
19
19
  fixable: 'code',
20
20
  schema: [],
21
21
  messages: {
22
- enforceIdCapitalization: 'Use "ID" instead of "id" in user-facing text for better readability',
22
+ enforceIdCapitalization: 'Found lowercase "id" in user-facing text → "ID" (capitalized) is the standard abbreviation for "identifier" and improves readability in UI labels and messages → replace lowercase "id" with uppercase "ID" in displayed labels and messages.',
23
23
  },
24
24
  },
25
25
  defaultOptions: [],