@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.
Files changed (141) hide show
  1. package/README.md +140 -99
  2. package/lib/index.js +111 -17
  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-type-assertion-returns.js +6 -0
  86. package/lib/rules/no-undefined-null-passthrough.js +166 -19
  87. package/lib/rules/no-unnecessary-verb-suffix.js +0 -2
  88. package/lib/rules/no-unsafe-firestore-spread.js +8 -5
  89. package/lib/rules/no-unused-props.d.ts +3 -0
  90. package/lib/rules/no-unused-props.js +213 -78
  91. package/lib/rules/optimize-object-boolean-conditions.d.ts +1 -0
  92. package/lib/rules/optimize-object-boolean-conditions.js +234 -0
  93. package/lib/rules/parallelize-async-operations.d.ts +8 -0
  94. package/lib/rules/parallelize-async-operations.js +375 -0
  95. package/lib/rules/prefer-batch-operations.js +199 -33
  96. package/lib/rules/prefer-block-comments-for-declarations.js +13 -3
  97. package/lib/rules/prefer-document-flattening.d.ts +4 -0
  98. package/lib/rules/prefer-document-flattening.js +239 -0
  99. package/lib/rules/prefer-field-paths-in-transforms.d.ts +7 -0
  100. package/lib/rules/prefer-field-paths-in-transforms.js +250 -0
  101. package/lib/rules/prefer-fragment-component.js +3 -3
  102. package/lib/rules/prefer-global-router-state-key.d.ts +7 -3
  103. package/lib/rules/prefer-global-router-state-key.js +230 -31
  104. package/lib/rules/prefer-next-dynamic.d.ts +9 -0
  105. package/lib/rules/prefer-next-dynamic.js +366 -0
  106. package/lib/rules/prefer-nullish-coalescing-boolean-props.d.ts +1 -0
  107. package/lib/rules/prefer-nullish-coalescing-boolean-props.js +430 -0
  108. package/lib/rules/prefer-nullish-coalescing-override.d.ts +6 -0
  109. package/lib/rules/prefer-nullish-coalescing-override.js +182 -0
  110. package/lib/rules/prefer-params-over-parent-id.d.ts +5 -0
  111. package/lib/rules/prefer-params-over-parent-id.js +228 -0
  112. package/lib/rules/prefer-settings-object.js +23 -15
  113. package/lib/rules/prefer-type-alias-over-typeof-constant.d.ts +3 -0
  114. package/lib/rules/prefer-type-alias-over-typeof-constant.js +259 -0
  115. package/lib/rules/prefer-url-tostring-over-tojson.d.ts +2 -0
  116. package/lib/rules/prefer-url-tostring-over-tojson.js +166 -0
  117. package/lib/rules/prefer-use-deep-compare-memo.d.ts +4 -0
  118. package/lib/rules/prefer-use-deep-compare-memo.js +428 -0
  119. package/lib/rules/prefer-usecallback-over-usememo-for-functions.js +24 -1
  120. package/lib/rules/prefer-usememo-over-useeffect-usestate.js +24 -5
  121. package/lib/rules/react-usememo-should-be-component.d.ts +2 -1
  122. package/lib/rules/react-usememo-should-be-component.js +144 -78
  123. package/lib/rules/require-dynamic-firebase-imports.js +2 -1
  124. package/lib/rules/require-image-optimized.js +1 -1
  125. package/lib/rules/require-memo.js +11 -1
  126. package/lib/rules/require-usememo-object-literals.js +7 -1
  127. package/lib/rules/semantic-function-prefixes.js +38 -18
  128. package/lib/rules/sync-onwrite-name-func.js +5 -1
  129. package/lib/rules/test-file-location-enforcement.d.ts +1 -0
  130. package/lib/rules/test-file-location-enforcement.js +57 -0
  131. package/lib/rules/use-custom-link.js +6 -1
  132. package/lib/rules/use-custom-router.js +6 -1
  133. package/lib/rules/use-latest-callback.d.ts +3 -0
  134. package/lib/rules/use-latest-callback.js +270 -0
  135. package/lib/utils/ASTHelpers.js +10 -0
  136. package/lib/utils/createRule.js +1 -1
  137. package/lib/utils/graph/ClassGraphSorterReadability.js +1 -1
  138. package/lib/utils/ruleTester.d.ts +1 -0
  139. package/lib/utils/ruleTester.js +4 -1
  140. package/package.json +21 -14
  141. package/lib/rules/enforce-assertSafe-object-key.d.ts +0 -2
@@ -0,0 +1,4 @@
1
+ import { TSESLint } from '@typescript-eslint/utils';
2
+ type MessageIds = 'preferDocumentFlattening' | 'addShouldFlatten';
3
+ export declare const preferDocumentFlattening: TSESLint.RuleModule<MessageIds, [], TSESLint.RuleListener>;
4
+ export {};
@@ -0,0 +1,239 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.preferDocumentFlattening = void 0;
4
+ const utils_1 = require("@typescript-eslint/utils");
5
+ const createRule_1 = require("../utils/createRule");
6
+ function isIdentifier(node) {
7
+ return node.type === utils_1.AST_NODE_TYPES.Identifier;
8
+ }
9
+ function isMemberExpression(node) {
10
+ return node.type === utils_1.AST_NODE_TYPES.MemberExpression;
11
+ }
12
+ function isObjectExpression(node) {
13
+ return node.type === utils_1.AST_NODE_TYPES.ObjectExpression;
14
+ }
15
+ function isProperty(node) {
16
+ return node.type === utils_1.AST_NODE_TYPES.Property;
17
+ }
18
+ /**
19
+ * Recursively checks if an object has deeply nested objects
20
+ */
21
+ const hasDeepNestedObjects = (node) => {
22
+ if (isObjectExpression(node)) {
23
+ for (const property of node.properties) {
24
+ if (!isProperty(property))
25
+ continue;
26
+ const value = property.value;
27
+ // If the property value is an object, it's a nested object
28
+ if (isObjectExpression(value)) {
29
+ return true;
30
+ }
31
+ // Check arrays for nested objects
32
+ if (value.type === utils_1.AST_NODE_TYPES.ArrayExpression) {
33
+ for (const element of value.elements) {
34
+ if (element && hasDeepNestedObjects(element)) {
35
+ return true;
36
+ }
37
+ }
38
+ }
39
+ }
40
+ }
41
+ // Check arrays for nested objects
42
+ if (node.type === utils_1.AST_NODE_TYPES.ArrayExpression) {
43
+ for (const element of node.elements) {
44
+ if (element && hasDeepNestedObjects(element)) {
45
+ return true;
46
+ }
47
+ }
48
+ }
49
+ return false;
50
+ };
51
+ exports.preferDocumentFlattening = (0, createRule_1.createRule)({
52
+ name: 'prefer-document-flattening',
53
+ meta: {
54
+ type: 'suggestion',
55
+ hasSuggestions: true,
56
+ docs: {
57
+ description: 'Enforce using the shouldFlatten option when setting deeply nested objects in Firestore documents',
58
+ recommended: 'error',
59
+ },
60
+ schema: [],
61
+ messages: {
62
+ preferDocumentFlattening: '{{className}} instance "{{instanceName}}" sets nested Firestore data without enabling shouldFlatten. Nested object writes overwrite sibling fields and require read-modify-write cycles, which increases contention and hides field-level query paths. Add shouldFlatten: true in the {{className}} options or pass flattened field paths (for example, "profile.settings.theme") so nested updates stay atomic and queryable.',
63
+ addShouldFlatten: 'Add shouldFlatten: true to the DocSetter options.',
64
+ },
65
+ },
66
+ defaultOptions: [],
67
+ create(context) {
68
+ // Track DocSetter instances without shouldFlatten option
69
+ const docSetterInstances = [];
70
+ // Track which DocSetter instances are used to set nested objects
71
+ const docSetterWithNestedObjects = new Set();
72
+ const buildSuggestion = (instance) => {
73
+ const newExpr = instance.node;
74
+ const hasOptionsArg = newExpr.arguments.length >= 2;
75
+ const optionsArg = hasOptionsArg ? newExpr.arguments[1] : undefined;
76
+ if (optionsArg && isObjectExpression(optionsArg)) {
77
+ const insertPos = (optionsArg.range?.[1] ?? optionsArg.parent?.range?.[1] ?? 0) - 1;
78
+ const prefix = optionsArg.properties.length ? ', ' : '';
79
+ return [
80
+ {
81
+ messageId: 'addShouldFlatten',
82
+ fix(fixer) {
83
+ return fixer.insertTextBeforeRange([insertPos, insertPos], `${prefix}shouldFlatten: true`);
84
+ },
85
+ },
86
+ ];
87
+ }
88
+ const endPos = (newExpr.range?.[1] ?? newExpr.parent?.range?.[1] ?? 0) - 1;
89
+ return [
90
+ {
91
+ messageId: 'addShouldFlatten',
92
+ fix(fixer) {
93
+ return fixer.insertTextBeforeRange([endPos, endPos], `${hasOptionsArg ? '' : ','} { shouldFlatten: true }`);
94
+ },
95
+ },
96
+ ];
97
+ };
98
+ return {
99
+ // Detect DocSetter and DocSetterTransaction instantiations
100
+ NewExpression(node) {
101
+ if (!isIdentifier(node.callee))
102
+ return;
103
+ const className = node.callee.name;
104
+ // Only check DocSetter and DocSetterTransaction classes
105
+ if (className !== 'DocSetter' && className !== 'DocSetterTransaction')
106
+ return;
107
+ // Check if shouldFlatten option is provided
108
+ let hasShouldFlatten = false;
109
+ // The options object is typically the second argument
110
+ if (node.arguments.length >= 2) {
111
+ const optionsArg = node.arguments[1];
112
+ if (isObjectExpression(optionsArg)) {
113
+ for (const property of optionsArg.properties) {
114
+ if (!isProperty(property))
115
+ continue;
116
+ if (isIdentifier(property.key) &&
117
+ property.key.name === 'shouldFlatten' &&
118
+ property.value.type === utils_1.AST_NODE_TYPES.Literal &&
119
+ property.value.value === true) {
120
+ hasShouldFlatten = true;
121
+ break;
122
+ }
123
+ }
124
+ }
125
+ }
126
+ // Get variable name from parent node if it's a variable declaration
127
+ let instanceName = '';
128
+ if (node.parent &&
129
+ node.parent.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
130
+ isIdentifier(node.parent.id)) {
131
+ instanceName = node.parent.id.name;
132
+ }
133
+ if (instanceName && !hasShouldFlatten) {
134
+ docSetterInstances.push({
135
+ className,
136
+ name: instanceName,
137
+ node,
138
+ hasShouldFlatten,
139
+ });
140
+ }
141
+ },
142
+ // Check for set method calls on DocSetter instances
143
+ CallExpression(node) {
144
+ if (!isMemberExpression(node.callee))
145
+ return;
146
+ const property = node.callee.property;
147
+ if (!isIdentifier(property))
148
+ return;
149
+ // Check if it's a set or setAll method
150
+ if (property.name !== 'set' && property.name !== 'setAll')
151
+ return;
152
+ const object = node.callee.object;
153
+ let instance;
154
+ if (isIdentifier(object)) {
155
+ instance = docSetterInstances.find((i) => i.name === object.name);
156
+ }
157
+ else if (object.type === utils_1.AST_NODE_TYPES.NewExpression &&
158
+ isIdentifier(object.callee)) {
159
+ const className = object.callee.name;
160
+ if (className === 'DocSetter' ||
161
+ className === 'DocSetterTransaction') {
162
+ let hasShouldFlatten = false;
163
+ if (object.arguments.length >= 2) {
164
+ const optionsArg = object.arguments[1];
165
+ if (isObjectExpression(optionsArg)) {
166
+ for (const property of optionsArg.properties) {
167
+ if (!isProperty(property))
168
+ continue;
169
+ if (isIdentifier(property.key) &&
170
+ property.key.name === 'shouldFlatten' &&
171
+ property.value.type === utils_1.AST_NODE_TYPES.Literal &&
172
+ property.value.value === true) {
173
+ hasShouldFlatten = true;
174
+ break;
175
+ }
176
+ }
177
+ }
178
+ }
179
+ if (!hasShouldFlatten) {
180
+ instance = {
181
+ className,
182
+ name: `(inline-${docSetterInstances.length})`,
183
+ node: object,
184
+ hasShouldFlatten,
185
+ };
186
+ docSetterInstances.push(instance);
187
+ }
188
+ }
189
+ }
190
+ if (!instance)
191
+ return;
192
+ // Check if we're setting a nested object
193
+ if (node.arguments.length > 0) {
194
+ const dataArg = node.arguments[0];
195
+ // For set method
196
+ if (isObjectExpression(dataArg) && hasDeepNestedObjects(dataArg)) {
197
+ docSetterWithNestedObjects.add(instance.name);
198
+ }
199
+ // For setAll method with array argument
200
+ if (dataArg.type === utils_1.AST_NODE_TYPES.ArrayExpression &&
201
+ dataArg.elements.some((element) => {
202
+ if (!element || !isObjectExpression(element)) {
203
+ return false;
204
+ }
205
+ for (const prop of element.properties) {
206
+ if (!isProperty(prop) || !isIdentifier(prop.key))
207
+ continue;
208
+ if (prop.key.name !== 'data')
209
+ continue;
210
+ if (!isObjectExpression(prop.value))
211
+ return false;
212
+ return hasDeepNestedObjects(prop.value);
213
+ }
214
+ return false;
215
+ })) {
216
+ docSetterWithNestedObjects.add(instance.name);
217
+ }
218
+ }
219
+ },
220
+ // Report at the end of the program
221
+ 'Program:exit'() {
222
+ for (const instance of docSetterInstances) {
223
+ if (docSetterWithNestedObjects.has(instance.name)) {
224
+ context.report({
225
+ node: instance.node,
226
+ messageId: 'preferDocumentFlattening',
227
+ data: {
228
+ className: instance.className,
229
+ instanceName: instance.name,
230
+ },
231
+ suggest: buildSuggestion(instance),
232
+ });
233
+ }
234
+ }
235
+ },
236
+ };
237
+ },
238
+ });
239
+ //# sourceMappingURL=prefer-document-flattening.js.map
@@ -0,0 +1,7 @@
1
+ import type { TSESLint } from '@typescript-eslint/utils';
2
+ interface RuleOptions {
3
+ containers?: string[];
4
+ allowNestedIn?: string[];
5
+ }
6
+ export declare const preferFieldPathsInTransforms: TSESLint.RuleModule<"preferFieldPathsInTransforms", [(RuleOptions | undefined)?], TSESLint.RuleListener>;
7
+ export {};
@@ -0,0 +1,250 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.preferFieldPathsInTransforms = void 0;
4
+ const utils_1 = require("@typescript-eslint/utils");
5
+ const minimatch_1 = require("minimatch");
6
+ const createRule_1 = require("../utils/createRule");
7
+ // Defaults aim to catch common BluMint aggregation container names
8
+ const DEFAULT_CONTAINERS = ['*Aggregation', 'previews', '*Previews'];
9
+ function describeNestedPath(containerValue) {
10
+ let objectFallback = null;
11
+ let primitiveFallback = null;
12
+ for (const prop of containerValue.properties) {
13
+ if (prop.type === utils_1.AST_NODE_TYPES.SpreadElement)
14
+ continue;
15
+ if (!isProperty(prop))
16
+ continue;
17
+ if (prop.computed)
18
+ continue;
19
+ const firstKey = getPropertyName(prop);
20
+ if (!firstKey)
21
+ continue;
22
+ if (isObjectExpression(prop.value)) {
23
+ for (const child of prop.value.properties) {
24
+ if (child.type === utils_1.AST_NODE_TYPES.SpreadElement)
25
+ continue;
26
+ if (!isProperty(child))
27
+ continue;
28
+ if (child.computed)
29
+ continue;
30
+ const childKey = getPropertyName(child);
31
+ if (childKey)
32
+ return `${firstKey}.${childKey}`;
33
+ }
34
+ // Object container without usable child keys still signals nested intent
35
+ if (!objectFallback)
36
+ objectFallback = firstKey;
37
+ continue;
38
+ }
39
+ if (!primitiveFallback)
40
+ primitiveFallback = firstKey;
41
+ }
42
+ return objectFallback ?? primitiveFallback;
43
+ }
44
+ function isObjectExpression(node) {
45
+ return !!node && node.type === utils_1.AST_NODE_TYPES.ObjectExpression;
46
+ }
47
+ function isProperty(node) {
48
+ return node.type === utils_1.AST_NODE_TYPES.Property;
49
+ }
50
+ function getPropertyName(node) {
51
+ if (node.computed)
52
+ return null;
53
+ if (node.key.type === utils_1.AST_NODE_TYPES.Identifier)
54
+ return node.key.name;
55
+ if (node.key.type === utils_1.AST_NODE_TYPES.Literal &&
56
+ typeof node.key.value === 'string')
57
+ return node.key.value;
58
+ return null;
59
+ }
60
+ function isNamedFunction(fn, name) {
61
+ if (fn.type === utils_1.AST_NODE_TYPES.FunctionDeclaration) {
62
+ return (!!fn.id && fn.id.type === utils_1.AST_NODE_TYPES.Identifier && fn.id.name === name);
63
+ }
64
+ // For function expressions, parent determines name association
65
+ return false;
66
+ }
67
+ function isBoundToName(fn, name) {
68
+ if (fn.type === utils_1.AST_NODE_TYPES.FunctionDeclaration) {
69
+ return isNamedFunction(fn, name);
70
+ }
71
+ const parent = fn.parent;
72
+ if (!parent)
73
+ return false;
74
+ if (parent.type === utils_1.AST_NODE_TYPES.Property ||
75
+ parent.type === utils_1.AST_NODE_TYPES.MethodDefinition ||
76
+ parent.type === utils_1.AST_NODE_TYPES.PropertyDefinition) {
77
+ const key = parent.key;
78
+ if (parent.computed) {
79
+ return key.type === utils_1.AST_NODE_TYPES.Literal && key.value === name;
80
+ }
81
+ if (key.type === utils_1.AST_NODE_TYPES.Identifier)
82
+ return key.name === name;
83
+ if (key.type === utils_1.AST_NODE_TYPES.Literal)
84
+ return key.value === name;
85
+ }
86
+ if (parent.type === utils_1.AST_NODE_TYPES.VariableDeclarator) {
87
+ return parent.id.type === utils_1.AST_NODE_TYPES.Identifier && parent.id.name === name;
88
+ }
89
+ if (parent.type === utils_1.AST_NODE_TYPES.AssignmentExpression) {
90
+ const left = parent.left;
91
+ if (left.type === utils_1.AST_NODE_TYPES.MemberExpression) {
92
+ const prop = left.property;
93
+ if (prop.type === utils_1.AST_NODE_TYPES.Identifier)
94
+ return prop.name === name;
95
+ if (prop.type === utils_1.AST_NODE_TYPES.Literal)
96
+ return prop.value === name;
97
+ }
98
+ else if (left.type === utils_1.AST_NODE_TYPES.Identifier) {
99
+ return left.name === name;
100
+ }
101
+ }
102
+ return false;
103
+ }
104
+ function isTransformEachFunction(fn) {
105
+ return isBoundToName(fn, 'transformEach');
106
+ }
107
+ function isTransformEachVaripotent(fn) {
108
+ return isBoundToName(fn, 'transformEachVaripotent');
109
+ }
110
+ // Determine whether any path two or more levels below a container is created using object literals
111
+ function hasDeeperThanOneLevelUnderContainer(containerObj) {
112
+ for (const prop of containerObj.properties) {
113
+ if (prop.type === utils_1.AST_NODE_TYPES.SpreadElement) {
114
+ if (isObjectExpression(prop.argument)) {
115
+ return true;
116
+ }
117
+ continue;
118
+ }
119
+ if (!isProperty(prop))
120
+ continue;
121
+ const value = prop.value;
122
+ if (isObjectExpression(value)) {
123
+ // Any nested object literal implies depth >= 2 (even if it only spreads)
124
+ return true;
125
+ }
126
+ }
127
+ return false;
128
+ }
129
+ function analyzeReturnedObject(obj, context, containerNameMatches) {
130
+ for (const top of obj.properties) {
131
+ if (top.type === utils_1.AST_NODE_TYPES.SpreadElement)
132
+ continue;
133
+ if (!isProperty(top))
134
+ continue;
135
+ // Skip computed top-level keys and already-flattened keys containing dots
136
+ if (top.computed)
137
+ continue;
138
+ const keyName = getPropertyName(top);
139
+ if (!keyName)
140
+ continue;
141
+ if (keyName.includes('.'))
142
+ continue;
143
+ if (!containerNameMatches(keyName))
144
+ continue;
145
+ const containerValue = top.value;
146
+ if (!isObjectExpression(containerValue))
147
+ continue; // only care if returning an object under the container
148
+ if (hasDeeperThanOneLevelUnderContainer(containerValue)) {
149
+ const nestedPath = describeNestedPath(containerValue) ?? 'nestedField';
150
+ context.report({
151
+ node: top,
152
+ messageId: 'preferFieldPathsInTransforms',
153
+ data: {
154
+ container: keyName,
155
+ nestedPath,
156
+ flattenedPath: `${keyName}.${nestedPath}`,
157
+ },
158
+ fix: () => null,
159
+ });
160
+ }
161
+ }
162
+ }
163
+ exports.preferFieldPathsInTransforms = (0, createRule_1.createRule)({
164
+ name: 'prefer-field-paths-in-transforms',
165
+ meta: {
166
+ type: 'suggestion',
167
+ docs: {
168
+ description: 'Flatten aggregation updates inside transformEach so diff-based deletes remove only the intended fields instead of wiping sibling data.',
169
+ recommended: 'warn',
170
+ },
171
+ fixable: 'code',
172
+ schema: [
173
+ {
174
+ type: 'object',
175
+ properties: {
176
+ containers: {
177
+ type: 'array',
178
+ items: { type: 'string' },
179
+ },
180
+ allowNestedIn: {
181
+ type: 'array',
182
+ items: { type: 'string' },
183
+ },
184
+ },
185
+ additionalProperties: false,
186
+ },
187
+ ],
188
+ messages: {
189
+ preferFieldPathsInTransforms: 'Transform returns nested object under "{{container}}" (e.g., "{{nestedPath}}"). Nested writes in shared aggregation containers cause diff reconciliation to delete the whole subtree, wiping sibling fields. Flatten the update into field-path keys such as "{{flattenedPath}}" so only the intended leaf changes and other aggregation data stays intact.',
190
+ },
191
+ },
192
+ defaultOptions: [
193
+ {
194
+ containers: DEFAULT_CONTAINERS,
195
+ allowNestedIn: [],
196
+ },
197
+ ],
198
+ create(context, [options]) {
199
+ const filename = context.getFilename();
200
+ const resolvedOptions = options ?? {
201
+ containers: DEFAULT_CONTAINERS,
202
+ allowNestedIn: [],
203
+ };
204
+ const containers = resolvedOptions.containers ?? DEFAULT_CONTAINERS;
205
+ const allowNestedIn = resolvedOptions.allowNestedIn ?? [];
206
+ // Skip files explicitly allowed
207
+ if (allowNestedIn.length > 0 &&
208
+ allowNestedIn.some((glob) => (0, minimatch_1.minimatch)(filename, glob))) {
209
+ return {};
210
+ }
211
+ function containerNameMatches(name) {
212
+ return containers.some((pattern) => (0, minimatch_1.minimatch)(name, pattern));
213
+ }
214
+ function isInTargetTransform(returnNode) {
215
+ // Find nearest function ancestor
216
+ let current = returnNode.parent ?? null;
217
+ while (current) {
218
+ if (current.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
219
+ current.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
220
+ current.type === utils_1.AST_NODE_TYPES.FunctionDeclaration) {
221
+ const fn = current;
222
+ if (isTransformEachVaripotent(fn))
223
+ return false;
224
+ if (isTransformEachFunction(fn))
225
+ return true;
226
+ }
227
+ current = current.parent ?? null;
228
+ }
229
+ return false;
230
+ }
231
+ return {
232
+ ReturnStatement(node) {
233
+ if (!node.argument || !isObjectExpression(node.argument))
234
+ return;
235
+ if (!isInTargetTransform(node))
236
+ return;
237
+ analyzeReturnedObject(node.argument, context, containerNameMatches);
238
+ },
239
+ ArrowFunctionExpression(node) {
240
+ // Handle implicit returns: transformEach: doc => ({ ... })
241
+ if (!isTransformEachFunction(node) || isTransformEachVaripotent(node))
242
+ return;
243
+ if (isObjectExpression(node.body)) {
244
+ analyzeReturnedObject(node.body, context, containerNameMatches);
245
+ }
246
+ },
247
+ };
248
+ },
249
+ });
250
+ //# sourceMappingURL=prefer-field-paths-in-transforms.js.map
@@ -8,14 +8,14 @@ exports.preferFragmentComponent = (0, createRule_1.createRule)({
8
8
  meta: {
9
9
  type: 'suggestion',
10
10
  docs: {
11
- description: 'Enforce using Fragment imported from react over shorthand fragments and React.Fragment',
11
+ description: 'Require the Fragment named import instead of shorthand fragments or React.Fragment to keep fragments explicit and prop-friendly',
12
12
  recommended: 'error',
13
13
  },
14
14
  fixable: 'code',
15
15
  schema: [],
16
16
  messages: {
17
- preferFragment: 'Use Fragment imported from react instead of {{type}}',
18
- addFragmentImport: 'Add Fragment import from react',
17
+ preferFragment: 'Prefer Fragment imported from react over {{type}}. Shorthand fragments block props like "key" and mixing fragment styles makes JSX harder to refactor. Import { Fragment } from "react" and wrap the children with <Fragment>...</Fragment> so fragment usage stays explicit.',
18
+ addFragmentImport: "Fragment is used but not imported from react. Without an explicit import the fixer leaves <Fragment> undefined and the React dependency implicit. Add `import { Fragment } from 'react'` alongside your other React imports so the file compiles.",
19
19
  },
20
20
  },
21
21
  defaultOptions: [],
@@ -1,5 +1,9 @@
1
+ import { TSESLint } from '@typescript-eslint/utils';
2
+ type MessageIds = 'preferGlobalRouterStateKey' | 'invalidQueryKeySource';
1
3
  /**
2
- * Rule to enforce best practices for the `key` parameter in `useRouterState` hook calls.
3
- * Encourages type safety and maintainability by using global constants or type-safe functions.
4
+ * Rule to enforce the use of centralized router state key constants imported from
5
+ * `src/util/routing/queryKeys.ts` instead of arbitrary string literals when calling
6
+ * router methods that accept key parameters.
4
7
  */
5
- export declare const preferGlobalRouterStateKey: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"preferGlobalRouterStateKey", [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
8
+ export declare const preferGlobalRouterStateKey: TSESLint.RuleModule<MessageIds, [], TSESLint.RuleListener>;
9
+ export {};