@blumintinc/eslint-plugin-blumint 1.12.5 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +140 -99
- package/lib/index.js +111 -20
- 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-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
- package/lib/rules/no-restricted-imports-dynamic.d.ts +0 -25
- package/lib/rules/no-restricted-imports-dynamic.js +0 -213
|
@@ -10,48 +10,58 @@ function isHookCall(node) {
|
|
|
10
10
|
return (callee.type === utils_1.AST_NODE_TYPES.Identifier && HOOK_NAMES.has(callee.name));
|
|
11
11
|
}
|
|
12
12
|
function isArrayOrPrimitive(checker, esTreeNode, nodeMap) {
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
try {
|
|
14
|
+
const tsNode = nodeMap.get(esTreeNode);
|
|
15
|
+
if (!tsNode)
|
|
16
|
+
return false;
|
|
17
|
+
const type = checker.getTypeAtLocation(tsNode);
|
|
18
|
+
// Check if it's a primitive type
|
|
19
|
+
if (type.flags &
|
|
20
|
+
(typescript_1.TypeFlags.String |
|
|
21
|
+
typescript_1.TypeFlags.StringLike |
|
|
22
|
+
typescript_1.TypeFlags.StringLiteral |
|
|
23
|
+
typescript_1.TypeFlags.Number |
|
|
24
|
+
typescript_1.TypeFlags.Boolean |
|
|
25
|
+
typescript_1.TypeFlags.Null |
|
|
26
|
+
typescript_1.TypeFlags.Undefined |
|
|
27
|
+
typescript_1.TypeFlags.Void |
|
|
28
|
+
typescript_1.TypeFlags.Never |
|
|
29
|
+
typescript_1.TypeFlags.BigInt |
|
|
30
|
+
typescript_1.TypeFlags.ESSymbol)) {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
// Check if it's an array type
|
|
34
|
+
const typeNode = checker.typeToTypeNode(type, undefined, undefined);
|
|
35
|
+
if (type.symbol?.name === 'Array' ||
|
|
36
|
+
type.symbol?.escapedName === 'Array' ||
|
|
37
|
+
(typeNode && ((0, typescript_1.isArrayTypeNode)(typeNode) || (0, typescript_1.isTupleTypeNode)(typeNode)))) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
// Check if it's a string type with methods (like String object)
|
|
41
|
+
if (type.symbol?.name === 'String' ||
|
|
42
|
+
type.symbol?.escapedName === 'String') {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
// Be more conservative - if we can't determine the type clearly, assume it's an object
|
|
46
|
+
// This prevents false positives where complex objects are incorrectly identified as primitives
|
|
47
|
+
if (type.flags & (typescript_1.TypeFlags.Any | typescript_1.TypeFlags.Unknown)) {
|
|
48
|
+
return false; // Treat Any/Unknown as potential objects
|
|
49
|
+
}
|
|
50
|
+
// If it's not a primitive or array, and has properties, it's an object
|
|
15
51
|
return false;
|
|
16
|
-
const type = checker.getTypeAtLocation(tsNode);
|
|
17
|
-
// Check if it's a primitive type
|
|
18
|
-
if (type.flags &
|
|
19
|
-
(typescript_1.TypeFlags.String |
|
|
20
|
-
typescript_1.TypeFlags.StringLike |
|
|
21
|
-
typescript_1.TypeFlags.StringLiteral |
|
|
22
|
-
typescript_1.TypeFlags.Number |
|
|
23
|
-
typescript_1.TypeFlags.Boolean |
|
|
24
|
-
typescript_1.TypeFlags.Null |
|
|
25
|
-
typescript_1.TypeFlags.Undefined |
|
|
26
|
-
typescript_1.TypeFlags.Void |
|
|
27
|
-
typescript_1.TypeFlags.Never |
|
|
28
|
-
typescript_1.TypeFlags.Any |
|
|
29
|
-
typescript_1.TypeFlags.Unknown |
|
|
30
|
-
typescript_1.TypeFlags.BigInt |
|
|
31
|
-
typescript_1.TypeFlags.ESSymbol)) {
|
|
32
|
-
return true;
|
|
33
|
-
}
|
|
34
|
-
// Check if it's an array type
|
|
35
|
-
const typeNode = checker.typeToTypeNode(type, undefined, undefined);
|
|
36
|
-
if (type.symbol?.name === 'Array' ||
|
|
37
|
-
type.symbol?.escapedName === 'Array' ||
|
|
38
|
-
(typeNode && ((0, typescript_1.isArrayTypeNode)(typeNode) || (0, typescript_1.isTupleTypeNode)(typeNode)))) {
|
|
39
|
-
return true;
|
|
40
52
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
return
|
|
53
|
+
catch (error) {
|
|
54
|
+
// If there's any error in type checking, assume it's an object to be safe
|
|
55
|
+
return false;
|
|
44
56
|
}
|
|
45
|
-
// If it's not a primitive or array, and has properties, it's an object
|
|
46
|
-
return false;
|
|
47
57
|
}
|
|
48
58
|
function getObjectUsagesInHook(hookBody, objectName,
|
|
49
59
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
50
60
|
context) {
|
|
51
|
-
const usages = new
|
|
61
|
+
const usages = new Map(); // Track usage and its position
|
|
52
62
|
const visited = new Set();
|
|
53
63
|
let needsEntireObject = false;
|
|
54
|
-
// Built-in array methods that
|
|
64
|
+
// Built-in array methods that indicate usage of the entire array
|
|
55
65
|
const ARRAY_METHODS = new Set([
|
|
56
66
|
'map',
|
|
57
67
|
'filter',
|
|
@@ -76,6 +86,33 @@ context) {
|
|
|
76
86
|
'flat',
|
|
77
87
|
'flatMap',
|
|
78
88
|
]);
|
|
89
|
+
// Built-in string methods that indicate usage of the entire string
|
|
90
|
+
const STRING_METHODS = new Set([
|
|
91
|
+
'charAt',
|
|
92
|
+
'charCodeAt',
|
|
93
|
+
'concat',
|
|
94
|
+
'indexOf',
|
|
95
|
+
'lastIndexOf',
|
|
96
|
+
'localeCompare',
|
|
97
|
+
'match',
|
|
98
|
+
'replace',
|
|
99
|
+
'search',
|
|
100
|
+
'slice',
|
|
101
|
+
'split',
|
|
102
|
+
'substr',
|
|
103
|
+
'substring',
|
|
104
|
+
'toLowerCase',
|
|
105
|
+
'toUpperCase',
|
|
106
|
+
'trim',
|
|
107
|
+
'trimStart',
|
|
108
|
+
'trimEnd',
|
|
109
|
+
'padStart',
|
|
110
|
+
'padEnd',
|
|
111
|
+
'repeat',
|
|
112
|
+
'startsWith',
|
|
113
|
+
'endsWith',
|
|
114
|
+
'includes',
|
|
115
|
+
]);
|
|
79
116
|
function buildAccessPath(node) {
|
|
80
117
|
const parts = [];
|
|
81
118
|
let current = node;
|
|
@@ -85,6 +122,20 @@ context) {
|
|
|
85
122
|
const memberExpr = current;
|
|
86
123
|
// Handle computed properties (like array indices)
|
|
87
124
|
if (memberExpr.computed) {
|
|
125
|
+
// For computed properties with variables (like user[key]), we need the entire object
|
|
126
|
+
if (memberExpr.property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
127
|
+
// Check if this is accessing our target object
|
|
128
|
+
let current = memberExpr.object;
|
|
129
|
+
while (current.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
130
|
+
current = current.object;
|
|
131
|
+
}
|
|
132
|
+
if (current.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
133
|
+
current.name === objectName) {
|
|
134
|
+
// This is a computed property access on our target object, so we need the entire object
|
|
135
|
+
needsEntireObject = true;
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
88
139
|
// For computed properties with literals
|
|
89
140
|
if (memberExpr.property.type === utils_1.AST_NODE_TYPES.Literal) {
|
|
90
141
|
const literalProp = memberExpr.property;
|
|
@@ -118,9 +169,38 @@ context) {
|
|
|
118
169
|
if (memberExpr.property.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
119
170
|
return null;
|
|
120
171
|
}
|
|
121
|
-
//
|
|
172
|
+
// Check for array/string methods - these indicate usage of the entire array/string
|
|
122
173
|
if (memberExpr.property.name &&
|
|
123
|
-
ARRAY_METHODS.has(memberExpr.property.name)
|
|
174
|
+
(ARRAY_METHODS.has(memberExpr.property.name) ||
|
|
175
|
+
STRING_METHODS.has(memberExpr.property.name))) {
|
|
176
|
+
// Check if this is accessing our target object or a property of it
|
|
177
|
+
let current = memberExpr.object;
|
|
178
|
+
let pathParts = [];
|
|
179
|
+
let hasOptionalChaining = false;
|
|
180
|
+
// Build the path to the array/string being accessed
|
|
181
|
+
while (current.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
182
|
+
const currentMember = current;
|
|
183
|
+
if (currentMember.property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
184
|
+
pathParts.unshift(currentMember.property.name);
|
|
185
|
+
}
|
|
186
|
+
if (currentMember.optional) {
|
|
187
|
+
hasOptionalChaining = true;
|
|
188
|
+
}
|
|
189
|
+
current = currentMember.object;
|
|
190
|
+
}
|
|
191
|
+
if (current.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
192
|
+
current.name === objectName) {
|
|
193
|
+
if (pathParts.length === 0) {
|
|
194
|
+
// Direct method call on the object (e.g., userData.map(...))
|
|
195
|
+
needsEntireObject = true;
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
// Method call on a property (e.g., userData.items.map(...) or userData?.items?.map(...))
|
|
199
|
+
let path = objectName + (hasOptionalChaining ? '?' : '');
|
|
200
|
+
path += '.' + pathParts.join('.');
|
|
201
|
+
usages.set(path, memberExpr.range?.[0] || 0);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
124
204
|
return null;
|
|
125
205
|
}
|
|
126
206
|
parts.unshift(memberExpr.property.name);
|
|
@@ -153,6 +233,11 @@ context) {
|
|
|
153
233
|
return;
|
|
154
234
|
visited.add(node);
|
|
155
235
|
if (node.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
236
|
+
// Check if the object is being called as a function
|
|
237
|
+
if (node.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
238
|
+
node.callee.name === objectName) {
|
|
239
|
+
needsEntireObject = true;
|
|
240
|
+
}
|
|
156
241
|
// Check if the object is directly passed as an argument
|
|
157
242
|
node.arguments.forEach((arg) => {
|
|
158
243
|
if (arg.type === utils_1.AST_NODE_TYPES.Identifier && arg.name === objectName) {
|
|
@@ -183,11 +268,81 @@ context) {
|
|
|
183
268
|
}
|
|
184
269
|
else if (node.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
185
270
|
// Check if this is accessing a property of our target object
|
|
186
|
-
const
|
|
187
|
-
if
|
|
188
|
-
|
|
271
|
+
const memberExpr = node;
|
|
272
|
+
// Only process if this is the outermost member expression in a chain
|
|
273
|
+
// (i.e., its parent is not also a member expression)
|
|
274
|
+
const parent = memberExpr.parent;
|
|
275
|
+
if (parent && parent.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
276
|
+
// This is an intermediate member expression, skip it
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
// Check if this member expression involves our target object
|
|
280
|
+
let current = memberExpr;
|
|
281
|
+
let foundTargetObject = false;
|
|
282
|
+
let hasDynamicComputed = false;
|
|
283
|
+
// Walk up the member expression chain to see if it involves our target object
|
|
284
|
+
while (current.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
285
|
+
const currentMember = current;
|
|
286
|
+
// Check if this level uses dynamic computed property access
|
|
287
|
+
if (currentMember.computed &&
|
|
288
|
+
currentMember.property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
289
|
+
hasDynamicComputed = true;
|
|
290
|
+
}
|
|
291
|
+
current = currentMember.object;
|
|
292
|
+
}
|
|
293
|
+
// Check if we reached our target object
|
|
294
|
+
if (current.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
295
|
+
current.name === objectName) {
|
|
296
|
+
foundTargetObject = true;
|
|
297
|
+
}
|
|
298
|
+
if (foundTargetObject) {
|
|
299
|
+
if (hasDynamicComputed) {
|
|
300
|
+
// Dynamic computed property access means we need the entire object
|
|
301
|
+
needsEntireObject = true;
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
// Static property access - add to usages
|
|
306
|
+
const path = buildAccessPath(memberExpr);
|
|
307
|
+
if (path) {
|
|
308
|
+
usages.set(path, memberExpr.range?.[0] || 0);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
189
311
|
}
|
|
190
312
|
}
|
|
313
|
+
else if (node.type === utils_1.AST_NODE_TYPES.ChainExpression) {
|
|
314
|
+
// Handle optional chaining expressions
|
|
315
|
+
if (node.expression.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
316
|
+
const path = buildAccessPath(node.expression);
|
|
317
|
+
if (path) {
|
|
318
|
+
usages.set(path, node.range?.[0] || 0);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
else if (node.type === utils_1.AST_NODE_TYPES.BinaryExpression ||
|
|
323
|
+
node.type === utils_1.AST_NODE_TYPES.LogicalExpression) {
|
|
324
|
+
// Handle binary expressions like `userId || userData?.id`
|
|
325
|
+
visit(node.left);
|
|
326
|
+
visit(node.right);
|
|
327
|
+
}
|
|
328
|
+
else if (node.type === utils_1.AST_NODE_TYPES.ConditionalExpression) {
|
|
329
|
+
// Handle ternary expressions
|
|
330
|
+
visit(node.test);
|
|
331
|
+
visit(node.consequent);
|
|
332
|
+
visit(node.alternate);
|
|
333
|
+
}
|
|
334
|
+
else if (node.type === utils_1.AST_NODE_TYPES.VariableDeclaration) {
|
|
335
|
+
// Handle variable declarations
|
|
336
|
+
node.declarations.forEach((declaration) => {
|
|
337
|
+
if (declaration.init) {
|
|
338
|
+
visit(declaration.init);
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
else if (node.type === utils_1.AST_NODE_TYPES.AssignmentExpression) {
|
|
343
|
+
// Handle assignments
|
|
344
|
+
visit(node.right);
|
|
345
|
+
}
|
|
191
346
|
// Visit all child nodes
|
|
192
347
|
for (const key in node) {
|
|
193
348
|
if (key === 'parent')
|
|
@@ -209,24 +364,81 @@ context) {
|
|
|
209
364
|
}
|
|
210
365
|
}
|
|
211
366
|
visit(hookBody);
|
|
212
|
-
//
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
367
|
+
// Process paths and determine which ones to include
|
|
368
|
+
const paths = Array.from(usages.keys());
|
|
369
|
+
const finalPaths = new Set();
|
|
370
|
+
paths.forEach((path) => {
|
|
371
|
+
// Always include the main path
|
|
372
|
+
finalPaths.add(path);
|
|
373
|
+
// For optional chaining, include the FIRST optional chaining point as intermediate
|
|
374
|
+
if (path.includes('?.')) {
|
|
375
|
+
// Find the first optional chaining point
|
|
376
|
+
// For userData?.profile.settings.theme.primary, we want to include userData?.profile
|
|
377
|
+
const firstOptionalIndex = path.indexOf('?.');
|
|
378
|
+
if (firstOptionalIndex !== -1) {
|
|
379
|
+
// Find the end of the first property after the optional chaining
|
|
380
|
+
const afterOptional = path.substring(firstOptionalIndex + 2);
|
|
381
|
+
const nextDotIndex = afterOptional.indexOf('.');
|
|
382
|
+
if (nextDotIndex !== -1) {
|
|
383
|
+
// There are more properties after the first optional property
|
|
384
|
+
const firstOptionalPath = path.substring(0, firstOptionalIndex + 2 + nextDotIndex);
|
|
385
|
+
finalPaths.add(firstOptionalPath);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
// For array access, include the array property itself
|
|
390
|
+
if (path.includes('[') && path.includes(']')) {
|
|
391
|
+
const bracketIndex = path.indexOf('[');
|
|
392
|
+
if (bracketIndex > 0) {
|
|
393
|
+
const arrayPath = path.substring(0, bracketIndex);
|
|
394
|
+
finalPaths.add(arrayPath);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
// Convert to array for sorting
|
|
399
|
+
const pathsArray = Array.from(finalPaths);
|
|
218
400
|
// Filter out array paths when we're already accessing specific indices
|
|
219
|
-
//
|
|
220
|
-
const filteredPaths =
|
|
221
|
-
// Skip
|
|
222
|
-
const
|
|
223
|
-
// Skip array paths if we're accessing specific indices
|
|
224
|
-
const isArrayWithSpecificIndices = paths.some((otherPath) => otherPath !== path &&
|
|
401
|
+
// Exception: keep array paths with optional chaining as they represent different dependencies
|
|
402
|
+
const filteredPaths = pathsArray.filter((path) => {
|
|
403
|
+
// Skip array paths if we're accessing specific indices, unless it's optional chaining
|
|
404
|
+
const isArrayWithSpecificIndices = pathsArray.some((otherPath) => otherPath !== path &&
|
|
225
405
|
(otherPath.startsWith(path + '[') ||
|
|
226
406
|
otherPath.match(new RegExp(`^${path.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\[\\d+\\]`))));
|
|
227
|
-
|
|
407
|
+
// Keep array paths with optional chaining even if specific indices are accessed
|
|
408
|
+
if (isArrayWithSpecificIndices && path.includes('?.')) {
|
|
409
|
+
return true;
|
|
410
|
+
}
|
|
411
|
+
return !isArrayWithSpecificIndices;
|
|
228
412
|
});
|
|
229
|
-
|
|
413
|
+
// Sort paths: longer/more specific paths first, then by optional chaining preference
|
|
414
|
+
const sortedPaths = filteredPaths.sort((a, b) => {
|
|
415
|
+
const posA = usages.get(a) || 0;
|
|
416
|
+
const posB = usages.get(b) || 0;
|
|
417
|
+
// For paths with the same base, put longer ones first
|
|
418
|
+
const aDepth = a.split('.').length + (a.includes('[') ? 1 : 0);
|
|
419
|
+
const bDepth = b.split('.').length + (b.includes('[') ? 1 : 0);
|
|
420
|
+
if (aDepth !== bDepth) {
|
|
421
|
+
return bDepth - aDepth; // Longer paths first
|
|
422
|
+
}
|
|
423
|
+
// If same depth, prefer optional chaining paths first
|
|
424
|
+
const aHasOptional = a.includes('?');
|
|
425
|
+
const bHasOptional = b.includes('?');
|
|
426
|
+
if (aHasOptional && !bHasOptional) {
|
|
427
|
+
return -1; // a comes first
|
|
428
|
+
}
|
|
429
|
+
if (!aHasOptional && bHasOptional) {
|
|
430
|
+
return 1; // b comes first
|
|
431
|
+
}
|
|
432
|
+
// If same depth and same optional chaining status, sort by source position
|
|
433
|
+
return posA - posB;
|
|
434
|
+
});
|
|
435
|
+
const filteredUsages = new Set(sortedPaths);
|
|
436
|
+
const notUsed = !needsEntireObject && filteredUsages.size === 0;
|
|
437
|
+
return {
|
|
438
|
+
usages: filteredUsages,
|
|
439
|
+
needsEntireObject,
|
|
440
|
+
notUsed,
|
|
441
|
+
};
|
|
230
442
|
}
|
|
231
443
|
exports.noEntireObjectHookDeps = (0, createRule_1.createRule)({
|
|
232
444
|
name: 'no-entire-object-hook-deps',
|
|
@@ -241,13 +453,16 @@ exports.noEntireObjectHookDeps = (0, createRule_1.createRule)({
|
|
|
241
453
|
schema: [],
|
|
242
454
|
messages: {
|
|
243
455
|
avoidEntireObject: 'Avoid using entire object "{{objectName}}" in dependency array. Use specific fields: {{fields}}',
|
|
456
|
+
removeUnusedDependency: 'Remove unused dependency "{{objectName}}" from dependency array (not used in effect)',
|
|
244
457
|
},
|
|
245
458
|
},
|
|
246
459
|
defaultOptions: [],
|
|
247
460
|
create(context) {
|
|
248
461
|
// For testing purposes, we'll make the rule work without TypeScript services
|
|
249
462
|
const parserServices = context.parserServices;
|
|
250
|
-
const hasFullTypeChecking = parserServices?.program &&
|
|
463
|
+
const hasFullTypeChecking = parserServices?.program &&
|
|
464
|
+
parserServices?.esTreeNodeToTSNodeMap &&
|
|
465
|
+
typeof parserServices.program.getTypeChecker === 'function';
|
|
251
466
|
// Skip type checking if we don't have TypeScript services
|
|
252
467
|
if (hasFullTypeChecking) {
|
|
253
468
|
// This is just to make the rule work in tests without TypeScript services
|
|
@@ -278,7 +493,7 @@ exports.noEntireObjectHookDeps = (0, createRule_1.createRule)({
|
|
|
278
493
|
if (element.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
279
494
|
const objectName = element.name;
|
|
280
495
|
// Skip type checking if we don't have TypeScript services
|
|
281
|
-
if (hasFullTypeChecking) {
|
|
496
|
+
if (hasFullTypeChecking && parserServices) {
|
|
282
497
|
const checker = parserServices.program.getTypeChecker();
|
|
283
498
|
const nodeMap = parserServices.esTreeNodeToTSNodeMap;
|
|
284
499
|
// Skip if the dependency is an array or primitive type
|
|
@@ -286,11 +501,54 @@ exports.noEntireObjectHookDeps = (0, createRule_1.createRule)({
|
|
|
286
501
|
return;
|
|
287
502
|
}
|
|
288
503
|
}
|
|
289
|
-
|
|
504
|
+
// For testing without TypeScript services, we'll assume all identifiers are objects
|
|
505
|
+
const result = getObjectUsagesInHook(callbackArg.body, objectName, context);
|
|
506
|
+
// If the object is not used at all, suggest removing it
|
|
507
|
+
if (result.notUsed) {
|
|
508
|
+
context.report({
|
|
509
|
+
node: element,
|
|
510
|
+
messageId: 'removeUnusedDependency',
|
|
511
|
+
data: {
|
|
512
|
+
objectName,
|
|
513
|
+
},
|
|
514
|
+
fix(fixer) {
|
|
515
|
+
// Remove the element and handle commas properly
|
|
516
|
+
const elementIndex = depsArg.elements.indexOf(element);
|
|
517
|
+
if (elementIndex === -1)
|
|
518
|
+
return null;
|
|
519
|
+
// If this is the only element, just remove it
|
|
520
|
+
if (depsArg.elements.length === 1) {
|
|
521
|
+
return fixer.remove(element);
|
|
522
|
+
}
|
|
523
|
+
// If this is the last element, remove the preceding comma
|
|
524
|
+
if (elementIndex === depsArg.elements.length - 1) {
|
|
525
|
+
const prevElement = depsArg.elements[elementIndex - 1];
|
|
526
|
+
if (prevElement) {
|
|
527
|
+
const range = [
|
|
528
|
+
prevElement.range[1],
|
|
529
|
+
element.range[1],
|
|
530
|
+
];
|
|
531
|
+
return fixer.removeRange(range);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
// Otherwise, remove the element and the following comma
|
|
535
|
+
const nextElement = depsArg.elements[elementIndex + 1];
|
|
536
|
+
if (nextElement) {
|
|
537
|
+
const range = [
|
|
538
|
+
element.range[0],
|
|
539
|
+
nextElement.range[0],
|
|
540
|
+
];
|
|
541
|
+
return fixer.removeRange(range);
|
|
542
|
+
}
|
|
543
|
+
// Fallback to just removing the element
|
|
544
|
+
return fixer.remove(element);
|
|
545
|
+
},
|
|
546
|
+
});
|
|
547
|
+
}
|
|
290
548
|
// If we found specific field usages and the entire object is in deps
|
|
291
|
-
// Skip reporting if
|
|
292
|
-
if (usages.size > 0) {
|
|
293
|
-
const fields = Array.from(usages).join(', ');
|
|
549
|
+
// Skip reporting if needsEntireObject is true (indicates spread operator usage)
|
|
550
|
+
else if (result.usages.size > 0 && !result.needsEntireObject) {
|
|
551
|
+
const fields = Array.from(result.usages).join(', ');
|
|
294
552
|
context.report({
|
|
295
553
|
node: element,
|
|
296
554
|
messageId: 'avoidEntireObject',
|
|
@@ -299,11 +557,7 @@ exports.noEntireObjectHookDeps = (0, createRule_1.createRule)({
|
|
|
299
557
|
fields,
|
|
300
558
|
},
|
|
301
559
|
fix(fixer) {
|
|
302
|
-
|
|
303
|
-
if (usages.size > 0) {
|
|
304
|
-
return fixer.replaceText(element, Array.from(usages).join(', '));
|
|
305
|
-
}
|
|
306
|
-
return null;
|
|
560
|
+
return fixer.replaceText(element, Array.from(result.usages).join(', '));
|
|
307
561
|
},
|
|
308
562
|
});
|
|
309
563
|
}
|