@blumintinc/eslint-plugin-blumint 1.12.6 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +140 -99
- package/lib/index.js +111 -17
- package/lib/rules/array-methods-this-context.js +20 -2
- package/lib/rules/avoid-utils-directory.js +11 -2
- package/lib/rules/dynamic-https-errors.d.ts +3 -1
- package/lib/rules/dynamic-https-errors.js +48 -10
- package/lib/rules/enforce-assert-safe-object-key.d.ts +8 -0
- package/lib/rules/{enforce-assertSafe-object-key.js → enforce-assert-safe-object-key.js} +51 -95
- package/lib/rules/enforce-assert-throws.d.ts +3 -1
- package/lib/rules/enforce-assert-throws.js +493 -51
- package/lib/rules/enforce-boolean-naming-prefixes.d.ts +3 -1
- package/lib/rules/enforce-boolean-naming-prefixes.js +832 -39
- package/lib/rules/enforce-callable-types.js +4 -4
- package/lib/rules/enforce-callback-memo.js +143 -18
- package/lib/rules/enforce-centralized-mock-firestore.js +4 -1
- package/lib/rules/enforce-console-error.d.ts +3 -0
- package/lib/rules/enforce-console-error.js +250 -0
- package/lib/rules/enforce-css-media-queries.js +24 -46
- package/lib/rules/enforce-dynamic-firebase-imports.js +100 -19
- package/lib/rules/enforce-dynamic-imports.js +1 -1
- package/lib/rules/enforce-exported-function-types.js +3 -3
- package/lib/rules/enforce-fieldpath-syntax-in-docsetter.d.ts +2 -0
- package/lib/rules/enforce-fieldpath-syntax-in-docsetter.js +212 -0
- package/lib/rules/enforce-firestore-doc-ref-generic.js +335 -0
- package/lib/rules/enforce-firestore-facade.js +347 -157
- package/lib/rules/enforce-firestore-rules-get-access.d.ts +3 -0
- package/lib/rules/enforce-firestore-rules-get-access.js +109 -0
- package/lib/rules/enforce-global-constants.d.ts +4 -1
- package/lib/rules/enforce-global-constants.js +240 -12
- package/lib/rules/enforce-memoize-async.d.ts +2 -1
- package/lib/rules/enforce-memoize-async.js +96 -27
- package/lib/rules/enforce-memoize-getters.d.ts +3 -0
- package/lib/rules/enforce-memoize-getters.js +123 -0
- package/lib/rules/enforce-microdiff.js +2 -1
- package/lib/rules/enforce-positive-naming.js +5 -7
- package/lib/rules/enforce-props-argument-name.d.ts +2 -7
- package/lib/rules/enforce-props-argument-name.js +165 -151
- package/lib/rules/enforce-props-naming-consistency.d.ts +2 -0
- package/lib/rules/enforce-props-naming-consistency.js +145 -0
- package/lib/rules/enforce-querykey-ts.d.ts +9 -0
- package/lib/rules/enforce-querykey-ts.js +274 -0
- package/lib/rules/enforce-singular-type-names.js +9 -5
- package/lib/rules/enforce-typescript-markdown-code-blocks.d.ts +1 -0
- package/lib/rules/enforce-typescript-markdown-code-blocks.js +96 -0
- package/lib/rules/enforce-verb-noun-naming.js +85 -10
- package/lib/rules/extract-global-constants.js +57 -45
- package/lib/rules/fast-deep-equal-over-microdiff.d.ts +2 -1
- package/lib/rules/fast-deep-equal-over-microdiff.js +356 -78
- package/lib/rules/firestore-transaction-reads-before-writes.d.ts +1 -0
- package/lib/rules/firestore-transaction-reads-before-writes.js +231 -0
- package/lib/rules/generic-starts-with-t.js +8 -2
- package/lib/rules/memoize-root-level-hocs.d.ts +7 -0
- package/lib/rules/memoize-root-level-hocs.js +242 -0
- package/lib/rules/no-array-length-in-deps.d.ts +12 -0
- package/lib/rules/no-array-length-in-deps.js +308 -0
- package/lib/rules/no-async-array-filter.js +5 -2
- package/lib/rules/no-complex-cloud-params.js +15 -2
- package/lib/rules/no-compositing-layer-props.js +4 -2
- package/lib/rules/no-entire-object-hook-deps.d.ts +3 -1
- package/lib/rules/no-entire-object-hook-deps.js +317 -63
- package/lib/rules/no-excessive-parent-chain.d.ts +5 -0
- package/lib/rules/no-excessive-parent-chain.js +273 -0
- package/lib/rules/no-firestore-object-arrays.js +345 -30
- package/lib/rules/no-hungarian.js +13 -13
- package/lib/rules/no-margin-properties.d.ts +7 -1
- package/lib/rules/no-margin-properties.js +16 -3
- package/lib/rules/no-misleading-boolean-prefixes.d.ts +7 -0
- package/lib/rules/no-misleading-boolean-prefixes.js +327 -0
- package/lib/rules/no-overridable-method-calls-in-constructor.d.ts +1 -0
- package/lib/rules/no-overridable-method-calls-in-constructor.js +260 -0
- package/lib/rules/no-passthrough-getters.d.ts +4 -0
- package/lib/rules/no-passthrough-getters.js +170 -0
- package/lib/rules/no-redundant-usecallback-wrapper.d.ts +8 -0
- package/lib/rules/no-redundant-usecallback-wrapper.js +315 -0
- package/lib/rules/no-restricted-properties-fix.d.ts +12 -0
- package/lib/rules/no-restricted-properties-fix.js +172 -0
- package/lib/rules/no-separate-loading-state.d.ts +8 -0
- package/lib/rules/no-separate-loading-state.js +142 -0
- package/lib/rules/no-stale-state-across-await.d.ts +1 -0
- package/lib/rules/no-stale-state-across-await.js +185 -0
- package/lib/rules/no-static-constants-in-dynamic-files.d.ts +3 -0
- package/lib/rules/no-static-constants-in-dynamic-files.js +149 -0
- package/lib/rules/no-try-catch-already-exists-in-transaction.d.ts +1 -0
- package/lib/rules/no-try-catch-already-exists-in-transaction.js +342 -0
- package/lib/rules/no-type-assertion-returns.js +6 -0
- package/lib/rules/no-undefined-null-passthrough.js +166 -19
- package/lib/rules/no-unnecessary-verb-suffix.js +0 -2
- package/lib/rules/no-unsafe-firestore-spread.js +8 -5
- package/lib/rules/no-unused-props.d.ts +3 -0
- package/lib/rules/no-unused-props.js +213 -78
- package/lib/rules/optimize-object-boolean-conditions.d.ts +1 -0
- package/lib/rules/optimize-object-boolean-conditions.js +234 -0
- package/lib/rules/parallelize-async-operations.d.ts +8 -0
- package/lib/rules/parallelize-async-operations.js +375 -0
- package/lib/rules/prefer-batch-operations.js +199 -33
- package/lib/rules/prefer-block-comments-for-declarations.js +13 -3
- package/lib/rules/prefer-document-flattening.d.ts +4 -0
- package/lib/rules/prefer-document-flattening.js +239 -0
- package/lib/rules/prefer-field-paths-in-transforms.d.ts +7 -0
- package/lib/rules/prefer-field-paths-in-transforms.js +250 -0
- package/lib/rules/prefer-fragment-component.js +3 -3
- package/lib/rules/prefer-global-router-state-key.d.ts +7 -3
- package/lib/rules/prefer-global-router-state-key.js +230 -31
- package/lib/rules/prefer-next-dynamic.d.ts +9 -0
- package/lib/rules/prefer-next-dynamic.js +366 -0
- package/lib/rules/prefer-nullish-coalescing-boolean-props.d.ts +1 -0
- package/lib/rules/prefer-nullish-coalescing-boolean-props.js +430 -0
- package/lib/rules/prefer-nullish-coalescing-override.d.ts +6 -0
- package/lib/rules/prefer-nullish-coalescing-override.js +182 -0
- package/lib/rules/prefer-params-over-parent-id.d.ts +5 -0
- package/lib/rules/prefer-params-over-parent-id.js +228 -0
- package/lib/rules/prefer-settings-object.js +23 -15
- package/lib/rules/prefer-type-alias-over-typeof-constant.d.ts +3 -0
- package/lib/rules/prefer-type-alias-over-typeof-constant.js +259 -0
- package/lib/rules/prefer-url-tostring-over-tojson.d.ts +2 -0
- package/lib/rules/prefer-url-tostring-over-tojson.js +166 -0
- package/lib/rules/prefer-use-deep-compare-memo.d.ts +4 -0
- package/lib/rules/prefer-use-deep-compare-memo.js +428 -0
- package/lib/rules/prefer-usecallback-over-usememo-for-functions.js +24 -1
- package/lib/rules/prefer-usememo-over-useeffect-usestate.js +24 -5
- package/lib/rules/react-usememo-should-be-component.d.ts +2 -1
- package/lib/rules/react-usememo-should-be-component.js +144 -78
- package/lib/rules/require-dynamic-firebase-imports.js +2 -1
- package/lib/rules/require-image-optimized.js +1 -1
- package/lib/rules/require-memo.js +11 -1
- package/lib/rules/require-usememo-object-literals.js +7 -1
- package/lib/rules/semantic-function-prefixes.js +38 -18
- package/lib/rules/sync-onwrite-name-func.js +5 -1
- package/lib/rules/test-file-location-enforcement.d.ts +1 -0
- package/lib/rules/test-file-location-enforcement.js +57 -0
- package/lib/rules/use-custom-link.js +6 -1
- package/lib/rules/use-custom-router.js +6 -1
- package/lib/rules/use-latest-callback.d.ts +3 -0
- package/lib/rules/use-latest-callback.js +270 -0
- package/lib/utils/ASTHelpers.js +10 -0
- package/lib/utils/createRule.js +1 -1
- package/lib/utils/graph/ClassGraphSorterReadability.js +1 -1
- package/lib/utils/ruleTester.d.ts +1 -0
- package/lib/utils/ruleTester.js +4 -1
- package/package.json +21 -14
- package/lib/rules/enforce-assertSafe-object-key.d.ts +0 -2
|
@@ -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: '
|
|
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: '
|
|
18
|
-
addFragmentImport:
|
|
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
|
|
3
|
-
*
|
|
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:
|
|
8
|
+
export declare const preferGlobalRouterStateKey: TSESLint.RuleModule<MessageIds, [], TSESLint.RuleListener>;
|
|
9
|
+
export {};
|