@blumintinc/eslint-plugin-blumint 1.12.6 → 1.14.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 +171 -99
- package/lib/index.js +300 -17
- package/lib/rules/array-methods-this-context.js +20 -2
- package/lib/rules/avoid-utils-directory.js +11 -2
- package/lib/rules/class-methods-read-top-to-bottom.js +24 -5
- package/lib/rules/consistent-callback-naming.js +1 -1
- package/lib/rules/dynamic-https-errors.d.ts +3 -1
- package/lib/rules/dynamic-https-errors.js +48 -11
- 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} +52 -96
- 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 +820 -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 +5 -2
- package/lib/rules/enforce-console-error.d.ts +3 -0
- package/lib/rules/enforce-console-error.js +278 -0
- package/lib/rules/enforce-css-media-queries.js +24 -46
- package/lib/rules/enforce-dynamic-file-naming.d.ts +2 -0
- package/lib/rules/enforce-dynamic-file-naming.js +53 -28
- package/lib/rules/enforce-dynamic-firebase-imports.js +100 -19
- package/lib/rules/enforce-dynamic-imports.js +1 -1
- package/lib/rules/enforce-early-destructuring.d.ts +2 -0
- package/lib/rules/enforce-early-destructuring.js +980 -0
- package/lib/rules/enforce-empty-object-check.d.ts +11 -0
- package/lib/rules/enforce-empty-object-check.js +502 -0
- package/lib/rules/enforce-exported-function-types.js +4 -4
- package/lib/rules/enforce-fieldpath-syntax-in-docsetter.d.ts +2 -0
- package/lib/rules/enforce-fieldpath-syntax-in-docsetter.js +262 -0
- package/lib/rules/enforce-firestore-doc-ref-generic.js +350 -3
- package/lib/rules/enforce-firestore-facade.js +349 -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-firestore-set-merge.js +3 -4
- package/lib/rules/enforce-global-constants.d.ts +4 -1
- package/lib/rules/enforce-global-constants.js +240 -12
- package/lib/rules/enforce-identifiable-firestore-type.js +108 -58
- package/lib/rules/enforce-memoize-async.d.ts +2 -1
- package/lib/rules/enforce-memoize-async.js +92 -27
- package/lib/rules/enforce-memoize-getters.d.ts +3 -0
- package/lib/rules/enforce-memoize-getters.js +147 -0
- package/lib/rules/enforce-microdiff.js +3 -2
- package/lib/rules/enforce-object-literal-as-const.js +4 -4
- package/lib/rules/enforce-positive-naming.js +9 -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-safe-stringify.js +1 -1
- package/lib/rules/enforce-serializable-params.js +3 -3
- package/lib/rules/enforce-singular-type-names.js +10 -6
- package/lib/rules/enforce-stable-hash-spread-props.d.ts +13 -0
- package/lib/rules/enforce-stable-hash-spread-props.js +342 -0
- package/lib/rules/enforce-storage-context.d.ts +9 -0
- package/lib/rules/enforce-storage-context.js +646 -0
- package/lib/rules/enforce-timestamp-now.js +23 -7
- package/lib/rules/enforce-transform-memoization.d.ts +4 -0
- package/lib/rules/enforce-transform-memoization.js +416 -0
- package/lib/rules/enforce-typescript-markdown-code-blocks.d.ts +1 -0
- package/lib/rules/enforce-typescript-markdown-code-blocks.js +100 -0
- package/lib/rules/enforce-unique-cursor-headers.d.ts +16 -0
- package/lib/rules/enforce-unique-cursor-headers.js +365 -0
- package/lib/rules/enforce-verb-noun-naming.js +90 -14
- package/lib/rules/ensure-pointer-events-none.js +28 -2
- package/lib/rules/export-if-in-doubt.js +27 -4
- package/lib/rules/extract-global-constants.js +92 -53
- package/lib/rules/fast-deep-equal-over-microdiff.d.ts +2 -1
- package/lib/rules/fast-deep-equal-over-microdiff.js +351 -83
- package/lib/rules/firestore-transaction-reads-before-writes.d.ts +1 -0
- package/lib/rules/firestore-transaction-reads-before-writes.js +277 -0
- package/lib/rules/flatten-push-calls.d.ts +2 -0
- package/lib/rules/flatten-push-calls.js +428 -0
- package/lib/rules/generic-starts-with-t.js +8 -2
- package/lib/rules/global-const-style.js +50 -19
- package/lib/rules/jsdoc-above-field.d.ts +11 -0
- package/lib/rules/jsdoc-above-field.js +208 -0
- package/lib/rules/key-only-outermost-element.js +21 -3
- package/lib/rules/logical-top-to-bottom-grouping.d.ts +5 -0
- package/lib/rules/logical-top-to-bottom-grouping.js +1258 -0
- package/lib/rules/memo-compare-deeply-complex-props.d.ts +3 -0
- package/lib/rules/memo-compare-deeply-complex-props.js +788 -0
- package/lib/rules/memo-nested-react-components.d.ts +8 -0
- package/lib/rules/memo-nested-react-components.js +410 -0
- 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-always-true-false-conditions.d.ts +2 -1
- package/lib/rules/no-always-true-false-conditions.js +68 -8
- package/lib/rules/no-array-length-in-deps.d.ts +12 -0
- package/lib/rules/no-array-length-in-deps.js +314 -0
- package/lib/rules/no-async-array-filter.js +5 -2
- package/lib/rules/no-async-foreach.js +210 -12
- package/lib/rules/no-circular-references.js +18 -17
- package/lib/rules/no-class-instance-destructuring.js +55 -12
- package/lib/rules/no-complex-cloud-params.js +22 -5
- package/lib/rules/no-compositing-layer-props.js +6 -4
- package/lib/rules/no-conditional-literals-in-jsx.js +37 -12
- package/lib/rules/no-console-error.d.ts +9 -0
- package/lib/rules/no-console-error.js +525 -0
- package/lib/rules/no-curly-brackets-around-commented-properties.d.ts +2 -0
- package/lib/rules/no-curly-brackets-around-commented-properties.js +258 -0
- package/lib/rules/no-empty-dependency-use-callbacks.d.ts +11 -0
- package/lib/rules/no-empty-dependency-use-callbacks.js +576 -0
- package/lib/rules/no-entire-object-hook-deps.d.ts +3 -1
- package/lib/rules/no-entire-object-hook-deps.js +320 -68
- 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-explicit-return-type.d.ts +2 -1
- package/lib/rules/no-explicit-return-type.js +215 -26
- package/lib/rules/no-filter-without-return.js +5 -1
- package/lib/rules/no-firestore-jest-mock.d.ts +2 -1
- package/lib/rules/no-firestore-jest-mock.js +126 -8
- package/lib/rules/no-firestore-object-arrays.js +398 -28
- package/lib/rules/no-handler-suffix.d.ts +12 -0
- package/lib/rules/no-handler-suffix.js +305 -0
- package/lib/rules/no-hungarian.js +14 -14
- package/lib/rules/no-inline-component-prop.d.ts +10 -0
- package/lib/rules/no-inline-component-prop.js +456 -0
- package/lib/rules/no-jsx-in-hooks.js +6 -1
- package/lib/rules/no-jsx-whitespace-literal.js +8 -2
- package/lib/rules/no-margin-properties.d.ts +7 -1
- package/lib/rules/no-margin-properties.js +22 -9
- package/lib/rules/no-memoize-on-static.js +9 -1
- package/lib/rules/no-misleading-boolean-prefixes.d.ts +7 -0
- package/lib/rules/no-misleading-boolean-prefixes.js +331 -0
- package/lib/rules/no-misused-switch-case.js +22 -1
- package/lib/rules/no-mixed-firestore-transactions.d.ts +3 -1
- package/lib/rules/no-mixed-firestore-transactions.js +296 -34
- package/lib/rules/no-mock-firebase-admin.js +5 -2
- package/lib/rules/no-object-values-on-strings.js +10 -2
- package/lib/rules/no-overridable-method-calls-in-constructor.d.ts +1 -0
- package/lib/rules/no-overridable-method-calls-in-constructor.js +272 -0
- package/lib/rules/no-passthrough-getters.d.ts +4 -0
- package/lib/rules/no-passthrough-getters.js +206 -0
- package/lib/rules/no-redundant-annotation-assertion.d.ts +2 -0
- package/lib/rules/no-redundant-annotation-assertion.js +402 -0
- package/lib/rules/no-redundant-param-types.js +16 -6
- package/lib/rules/no-redundant-this-params.d.ts +3 -0
- package/lib/rules/no-redundant-this-params.js +459 -0
- package/lib/rules/no-redundant-usecallback-wrapper.d.ts +8 -0
- package/lib/rules/no-redundant-usecallback-wrapper.js +328 -0
- package/lib/rules/no-res-error-status-in-onrequest.d.ts +4 -0
- package/lib/rules/no-res-error-status-in-onrequest.js +521 -0
- package/lib/rules/no-restricted-properties-fix.d.ts +12 -0
- package/lib/rules/no-restricted-properties-fix.js +173 -0
- package/lib/rules/no-separate-loading-state.d.ts +8 -0
- package/lib/rules/no-separate-loading-state.js +133 -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 +49 -49
- package/lib/rules/no-undefined-null-passthrough.js +217 -40
- package/lib/rules/no-unmemoized-memo-without-props.d.ts +8 -0
- package/lib/rules/no-unmemoized-memo-without-props.js +426 -0
- package/lib/rules/no-unnecessary-destructuring-rename.d.ts +2 -0
- package/lib/rules/no-unnecessary-destructuring-rename.js +347 -0
- package/lib/rules/no-unnecessary-destructuring.js +14 -4
- package/lib/rules/no-unnecessary-verb-suffix.js +2 -3
- package/lib/rules/no-unpinned-dependencies.js +36 -5
- package/lib/rules/no-unsafe-firestore-spread.js +8 -5
- package/lib/rules/no-unused-props.d.ts +5 -2
- package/lib/rules/no-unused-props.js +451 -112
- package/lib/rules/no-unused-usestate.js +6 -2
- package/lib/rules/no-useless-fragment.js +28 -2
- package/lib/rules/no-useless-usememo-primitives.d.ts +9 -0
- package/lib/rules/no-useless-usememo-primitives.js +393 -0
- package/lib/rules/no-usememo-for-pass-by-value.d.ts +13 -0
- package/lib/rules/no-usememo-for-pass-by-value.js +757 -0
- package/lib/rules/no-uuidv4-base62-as-key.js +18 -13
- package/lib/rules/omit-index-html.d.ts +2 -1
- package/lib/rules/omit-index-html.js +62 -7
- package/lib/rules/optimize-object-boolean-conditions.d.ts +1 -0
- package/lib/rules/optimize-object-boolean-conditions.js +236 -0
- package/lib/rules/parallelize-async-operations.d.ts +8 -0
- package/lib/rules/parallelize-async-operations.js +394 -0
- package/lib/rules/prefer-batch-operations.d.ts +1 -3
- package/lib/rules/prefer-batch-operations.js +228 -35
- package/lib/rules/prefer-block-comments-for-declarations.js +22 -7
- package/lib/rules/prefer-clone-deep.js +3 -3
- package/lib/rules/prefer-destructuring-no-class.d.ts +2 -1
- package/lib/rules/prefer-destructuring-no-class.js +244 -59
- package/lib/rules/prefer-docsetter-setall.d.ts +2 -0
- package/lib/rules/prefer-docsetter-setall.js +243 -0
- 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 +251 -0
- package/lib/rules/prefer-fragment-component.js +4 -4
- package/lib/rules/prefer-fragment-shorthand.js +2 -1
- package/lib/rules/prefer-getter-over-parameterless-method.d.ts +13 -0
- package/lib/rules/prefer-getter-over-parameterless-method.js +648 -0
- package/lib/rules/prefer-global-router-state-key.d.ts +7 -3
- package/lib/rules/prefer-global-router-state-key.js +305 -31
- package/lib/rules/prefer-memoized-props.d.ts +3 -0
- package/lib/rules/prefer-memoized-props.js +445 -0
- package/lib/rules/prefer-next-dynamic.d.ts +9 -0
- package/lib/rules/prefer-next-dynamic.js +357 -0
- package/lib/rules/prefer-nullish-coalescing-boolean-props.d.ts +1 -0
- package/lib/rules/prefer-nullish-coalescing-boolean-props.js +435 -0
- package/lib/rules/prefer-nullish-coalescing-override.d.ts +7 -0
- package/lib/rules/prefer-nullish-coalescing-override.js +188 -0
- package/lib/rules/prefer-params-over-parent-id.d.ts +5 -0
- package/lib/rules/prefer-params-over-parent-id.js +277 -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-type-over-interface.js +7 -2
- package/lib/rules/prefer-url-tostring-over-tojson.d.ts +2 -0
- package/lib/rules/prefer-url-tostring-over-tojson.js +169 -0
- package/lib/rules/prefer-use-deep-compare-memo.d.ts +4 -0
- package/lib/rules/prefer-use-deep-compare-memo.js +431 -0
- package/lib/rules/prefer-usecallback-over-usememo-for-functions.js +25 -2
- package/lib/rules/prefer-usememo-over-useeffect-usestate.js +24 -5
- package/lib/rules/prefer-utility-function-over-private-static.js +34 -2
- package/lib/rules/prevent-children-clobber.d.ts +2 -0
- package/lib/rules/prevent-children-clobber.js +536 -0
- package/lib/rules/react-memoize-literals.d.ts +4 -0
- package/lib/rules/react-memoize-literals.js +495 -0
- 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-hooks-default-params.js +17 -16
- package/lib/rules/require-https-error-cause.d.ts +4 -0
- package/lib/rules/require-https-error-cause.js +136 -0
- package/lib/rules/require-https-error.js +43 -21
- package/lib/rules/require-image-optimized.js +2 -2
- package/lib/rules/require-memo.js +12 -2
- package/lib/rules/require-memoize-jsx-returners.d.ts +3 -0
- package/lib/rules/require-memoize-jsx-returners.js +485 -0
- package/lib/rules/require-usememo-object-literals.js +9 -4
- 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 +271 -0
- package/lib/rules/vertically-group-related-functions.d.ts +16 -0
- package/lib/rules/vertically-group-related-functions.js +480 -0
- package/lib/utils/ASTHelpers.d.ts +13 -1
- package/lib/utils/ASTHelpers.js +34 -0
- package/lib/utils/createRule.js +1 -1
- package/lib/utils/getMethodName.d.ts +27 -0
- package/lib/utils/getMethodName.js +35 -0
- package/lib/utils/graph/ClassGraphSorterReadability.js +1 -1
- package/lib/utils/ruleTester.d.ts +1 -0
- package/lib/utils/ruleTester.js +4 -1
- package/lib/utils/tsTypeClassifier.d.ts +30 -0
- package/lib/utils/tsTypeClassifier.js +149 -0
- package/package.json +23 -14
- package/lib/rules/enforce-assertSafe-object-key.d.ts +0 -2
|
@@ -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;
|
|
@@ -102,9 +153,7 @@ context) {
|
|
|
102
153
|
else {
|
|
103
154
|
// For other computed properties, use the exact expression
|
|
104
155
|
try {
|
|
105
|
-
const propertyText = context
|
|
106
|
-
.getSourceCode()
|
|
107
|
-
.getText(memberExpr.property);
|
|
156
|
+
const propertyText = context.sourceCode.getText(memberExpr.property);
|
|
108
157
|
parts.unshift(`[${propertyText}]`);
|
|
109
158
|
}
|
|
110
159
|
catch (e) {
|
|
@@ -118,9 +167,38 @@ context) {
|
|
|
118
167
|
if (memberExpr.property.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
119
168
|
return null;
|
|
120
169
|
}
|
|
121
|
-
//
|
|
170
|
+
// Check for array/string methods - these indicate usage of the entire array/string
|
|
122
171
|
if (memberExpr.property.name &&
|
|
123
|
-
ARRAY_METHODS.has(memberExpr.property.name)
|
|
172
|
+
(ARRAY_METHODS.has(memberExpr.property.name) ||
|
|
173
|
+
STRING_METHODS.has(memberExpr.property.name))) {
|
|
174
|
+
// Check if this is accessing our target object or a property of it
|
|
175
|
+
let current = memberExpr.object;
|
|
176
|
+
let pathParts = [];
|
|
177
|
+
let hasOptionalChaining = false;
|
|
178
|
+
// Build the path to the array/string being accessed
|
|
179
|
+
while (current.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
180
|
+
const currentMember = current;
|
|
181
|
+
if (currentMember.property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
182
|
+
pathParts.unshift(currentMember.property.name);
|
|
183
|
+
}
|
|
184
|
+
if (currentMember.optional) {
|
|
185
|
+
hasOptionalChaining = true;
|
|
186
|
+
}
|
|
187
|
+
current = currentMember.object;
|
|
188
|
+
}
|
|
189
|
+
if (current.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
190
|
+
current.name === objectName) {
|
|
191
|
+
if (pathParts.length === 0) {
|
|
192
|
+
// Direct method call on the object (e.g., userData.map(...))
|
|
193
|
+
needsEntireObject = true;
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
// Method call on a property (e.g., userData.items.map(...) or userData?.items?.map(...))
|
|
197
|
+
let path = objectName + (hasOptionalChaining ? '?' : '');
|
|
198
|
+
path += '.' + pathParts.join('.');
|
|
199
|
+
usages.set(path, memberExpr.range?.[0] || 0);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
124
202
|
return null;
|
|
125
203
|
}
|
|
126
204
|
parts.unshift(memberExpr.property.name);
|
|
@@ -153,6 +231,11 @@ context) {
|
|
|
153
231
|
return;
|
|
154
232
|
visited.add(node);
|
|
155
233
|
if (node.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
234
|
+
// Check if the object is being called as a function
|
|
235
|
+
if (node.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
236
|
+
node.callee.name === objectName) {
|
|
237
|
+
needsEntireObject = true;
|
|
238
|
+
}
|
|
156
239
|
// Check if the object is directly passed as an argument
|
|
157
240
|
node.arguments.forEach((arg) => {
|
|
158
241
|
if (arg.type === utils_1.AST_NODE_TYPES.Identifier && arg.name === objectName) {
|
|
@@ -183,11 +266,81 @@ context) {
|
|
|
183
266
|
}
|
|
184
267
|
else if (node.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
185
268
|
// Check if this is accessing a property of our target object
|
|
186
|
-
const
|
|
187
|
-
if
|
|
188
|
-
|
|
269
|
+
const memberExpr = node;
|
|
270
|
+
// Only process if this is the outermost member expression in a chain
|
|
271
|
+
// (i.e., its parent is not also a member expression)
|
|
272
|
+
const parent = memberExpr.parent;
|
|
273
|
+
if (parent && parent.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
274
|
+
// This is an intermediate member expression, skip it
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
// Check if this member expression involves our target object
|
|
278
|
+
let current = memberExpr;
|
|
279
|
+
let foundTargetObject = false;
|
|
280
|
+
let hasDynamicComputed = false;
|
|
281
|
+
// Walk up the member expression chain to see if it involves our target object
|
|
282
|
+
while (current.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
283
|
+
const currentMember = current;
|
|
284
|
+
// Check if this level uses dynamic computed property access
|
|
285
|
+
if (currentMember.computed &&
|
|
286
|
+
currentMember.property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
287
|
+
hasDynamicComputed = true;
|
|
288
|
+
}
|
|
289
|
+
current = currentMember.object;
|
|
290
|
+
}
|
|
291
|
+
// Check if we reached our target object
|
|
292
|
+
if (current.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
293
|
+
current.name === objectName) {
|
|
294
|
+
foundTargetObject = true;
|
|
295
|
+
}
|
|
296
|
+
if (foundTargetObject) {
|
|
297
|
+
if (hasDynamicComputed) {
|
|
298
|
+
// Dynamic computed property access means we need the entire object
|
|
299
|
+
needsEntireObject = true;
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
// Static property access - add to usages
|
|
304
|
+
const path = buildAccessPath(memberExpr);
|
|
305
|
+
if (path) {
|
|
306
|
+
usages.set(path, memberExpr.range?.[0] || 0);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
189
309
|
}
|
|
190
310
|
}
|
|
311
|
+
else if (node.type === utils_1.AST_NODE_TYPES.ChainExpression) {
|
|
312
|
+
// Handle optional chaining expressions
|
|
313
|
+
if (node.expression.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
314
|
+
const path = buildAccessPath(node.expression);
|
|
315
|
+
if (path) {
|
|
316
|
+
usages.set(path, node.range?.[0] || 0);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
else if (node.type === utils_1.AST_NODE_TYPES.BinaryExpression ||
|
|
321
|
+
node.type === utils_1.AST_NODE_TYPES.LogicalExpression) {
|
|
322
|
+
// Handle binary expressions like `userId || userData?.id`
|
|
323
|
+
visit(node.left);
|
|
324
|
+
visit(node.right);
|
|
325
|
+
}
|
|
326
|
+
else if (node.type === utils_1.AST_NODE_TYPES.ConditionalExpression) {
|
|
327
|
+
// Handle ternary expressions
|
|
328
|
+
visit(node.test);
|
|
329
|
+
visit(node.consequent);
|
|
330
|
+
visit(node.alternate);
|
|
331
|
+
}
|
|
332
|
+
else if (node.type === utils_1.AST_NODE_TYPES.VariableDeclaration) {
|
|
333
|
+
// Handle variable declarations
|
|
334
|
+
node.declarations.forEach((declaration) => {
|
|
335
|
+
if (declaration.init) {
|
|
336
|
+
visit(declaration.init);
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
else if (node.type === utils_1.AST_NODE_TYPES.AssignmentExpression) {
|
|
341
|
+
// Handle assignments
|
|
342
|
+
visit(node.right);
|
|
343
|
+
}
|
|
191
344
|
// Visit all child nodes
|
|
192
345
|
for (const key in node) {
|
|
193
346
|
if (key === 'parent')
|
|
@@ -209,45 +362,105 @@ context) {
|
|
|
209
362
|
}
|
|
210
363
|
}
|
|
211
364
|
visit(hookBody);
|
|
212
|
-
//
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
365
|
+
// Process paths and determine which ones to include
|
|
366
|
+
const paths = Array.from(usages.keys());
|
|
367
|
+
const finalPaths = new Set();
|
|
368
|
+
paths.forEach((path) => {
|
|
369
|
+
// Always include the main path
|
|
370
|
+
finalPaths.add(path);
|
|
371
|
+
// For optional chaining, include the FIRST optional chaining point as intermediate
|
|
372
|
+
if (path.includes('?.')) {
|
|
373
|
+
// Find the first optional chaining point
|
|
374
|
+
// For userData?.profile.settings.theme.primary, we want to include userData?.profile
|
|
375
|
+
const firstOptionalIndex = path.indexOf('?.');
|
|
376
|
+
if (firstOptionalIndex !== -1) {
|
|
377
|
+
// Find the end of the first property after the optional chaining
|
|
378
|
+
const afterOptional = path.substring(firstOptionalIndex + 2);
|
|
379
|
+
const nextDotIndex = afterOptional.indexOf('.');
|
|
380
|
+
if (nextDotIndex !== -1) {
|
|
381
|
+
// There are more properties after the first optional property
|
|
382
|
+
const firstOptionalPath = path.substring(0, firstOptionalIndex + 2 + nextDotIndex);
|
|
383
|
+
finalPaths.add(firstOptionalPath);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
// For array access, include the array property itself
|
|
388
|
+
if (path.includes('[') && path.includes(']')) {
|
|
389
|
+
const bracketIndex = path.indexOf('[');
|
|
390
|
+
if (bracketIndex > 0) {
|
|
391
|
+
const arrayPath = path.substring(0, bracketIndex);
|
|
392
|
+
finalPaths.add(arrayPath);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
// Convert to array for sorting
|
|
397
|
+
const pathsArray = Array.from(finalPaths);
|
|
218
398
|
// 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 &&
|
|
399
|
+
// Exception: keep array paths with optional chaining as they represent different dependencies
|
|
400
|
+
const filteredPaths = pathsArray.filter((path) => {
|
|
401
|
+
// Skip array paths if we're accessing specific indices, unless it's optional chaining
|
|
402
|
+
const isArrayWithSpecificIndices = pathsArray.some((otherPath) => otherPath !== path &&
|
|
225
403
|
(otherPath.startsWith(path + '[') ||
|
|
226
404
|
otherPath.match(new RegExp(`^${path.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\[\\d+\\]`))));
|
|
227
|
-
|
|
405
|
+
// Keep array paths with optional chaining even if specific indices are accessed
|
|
406
|
+
if (isArrayWithSpecificIndices && path.includes('?.')) {
|
|
407
|
+
return true;
|
|
408
|
+
}
|
|
409
|
+
return !isArrayWithSpecificIndices;
|
|
228
410
|
});
|
|
229
|
-
|
|
411
|
+
// Sort paths: longer/more specific paths first, then by optional chaining preference
|
|
412
|
+
const sortedPaths = filteredPaths.sort((a, b) => {
|
|
413
|
+
const posA = usages.get(a) || 0;
|
|
414
|
+
const posB = usages.get(b) || 0;
|
|
415
|
+
// For paths with the same base, put longer ones first
|
|
416
|
+
const aDepth = a.split('.').length + (a.includes('[') ? 1 : 0);
|
|
417
|
+
const bDepth = b.split('.').length + (b.includes('[') ? 1 : 0);
|
|
418
|
+
if (aDepth !== bDepth) {
|
|
419
|
+
return bDepth - aDepth; // Longer paths first
|
|
420
|
+
}
|
|
421
|
+
// If same depth, prefer optional chaining paths first
|
|
422
|
+
const aHasOptional = a.includes('?');
|
|
423
|
+
const bHasOptional = b.includes('?');
|
|
424
|
+
if (aHasOptional && !bHasOptional) {
|
|
425
|
+
return -1; // a comes first
|
|
426
|
+
}
|
|
427
|
+
if (!aHasOptional && bHasOptional) {
|
|
428
|
+
return 1; // b comes first
|
|
429
|
+
}
|
|
430
|
+
// If same depth and same optional chaining status, sort by source position
|
|
431
|
+
return posA - posB;
|
|
432
|
+
});
|
|
433
|
+
const filteredUsages = new Set(sortedPaths);
|
|
434
|
+
const notUsed = !needsEntireObject && filteredUsages.size === 0;
|
|
435
|
+
return {
|
|
436
|
+
usages: filteredUsages,
|
|
437
|
+
needsEntireObject,
|
|
438
|
+
notUsed,
|
|
439
|
+
};
|
|
230
440
|
}
|
|
231
441
|
exports.noEntireObjectHookDeps = (0, createRule_1.createRule)({
|
|
232
442
|
name: 'no-entire-object-hook-deps',
|
|
233
443
|
meta: {
|
|
234
444
|
type: 'suggestion',
|
|
235
445
|
docs: {
|
|
236
|
-
description: 'Avoid using entire objects in React hook dependency arrays
|
|
446
|
+
description: 'Avoid using entire objects in React hook dependency arrays.',
|
|
237
447
|
recommended: 'error',
|
|
238
448
|
requiresTypeChecking: true,
|
|
239
449
|
},
|
|
240
450
|
fixable: 'code',
|
|
241
451
|
schema: [],
|
|
242
452
|
messages: {
|
|
243
|
-
avoidEntireObject: '
|
|
453
|
+
avoidEntireObject: 'What\'s wrong: Dependency array includes entire object "{{objectName}}". Why it matters: Any change to its other properties reruns the hook even though the hook reads only {{fields}}, creating extra renders and stale memoized values. How to fix: Depend on those fields instead.',
|
|
454
|
+
removeUnusedDependency: 'What\'s wrong: Dependency "{{objectName}}" is listed in the array but never read inside the hook body. Why it matters: The hook reruns when "{{objectName}}" changes without affecting the result and can hide the real missing dependency. How to fix: Remove it or add the specific value that actually drives the hook.',
|
|
244
455
|
},
|
|
245
456
|
},
|
|
246
457
|
defaultOptions: [],
|
|
247
458
|
create(context) {
|
|
248
459
|
// For testing purposes, we'll make the rule work without TypeScript services
|
|
249
460
|
const parserServices = context.parserServices;
|
|
250
|
-
const hasFullTypeChecking = parserServices?.program &&
|
|
461
|
+
const hasFullTypeChecking = parserServices?.program &&
|
|
462
|
+
parserServices?.esTreeNodeToTSNodeMap &&
|
|
463
|
+
typeof parserServices.program.getTypeChecker === 'function';
|
|
251
464
|
// Skip type checking if we don't have TypeScript services
|
|
252
465
|
if (hasFullTypeChecking) {
|
|
253
466
|
// This is just to make the rule work in tests without TypeScript services
|
|
@@ -278,7 +491,7 @@ exports.noEntireObjectHookDeps = (0, createRule_1.createRule)({
|
|
|
278
491
|
if (element.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
279
492
|
const objectName = element.name;
|
|
280
493
|
// Skip type checking if we don't have TypeScript services
|
|
281
|
-
if (hasFullTypeChecking) {
|
|
494
|
+
if (hasFullTypeChecking && parserServices) {
|
|
282
495
|
const checker = parserServices.program.getTypeChecker();
|
|
283
496
|
const nodeMap = parserServices.esTreeNodeToTSNodeMap;
|
|
284
497
|
// Skip if the dependency is an array or primitive type
|
|
@@ -286,11 +499,54 @@ exports.noEntireObjectHookDeps = (0, createRule_1.createRule)({
|
|
|
286
499
|
return;
|
|
287
500
|
}
|
|
288
501
|
}
|
|
289
|
-
|
|
502
|
+
// For testing without TypeScript services, we'll assume all identifiers are objects
|
|
503
|
+
const result = getObjectUsagesInHook(callbackArg.body, objectName, context);
|
|
504
|
+
// If the object is not used at all, suggest removing it
|
|
505
|
+
if (result.notUsed) {
|
|
506
|
+
context.report({
|
|
507
|
+
node: element,
|
|
508
|
+
messageId: 'removeUnusedDependency',
|
|
509
|
+
data: {
|
|
510
|
+
objectName,
|
|
511
|
+
},
|
|
512
|
+
fix(fixer) {
|
|
513
|
+
// Remove the element and handle commas properly
|
|
514
|
+
const elementIndex = depsArg.elements.indexOf(element);
|
|
515
|
+
if (elementIndex === -1)
|
|
516
|
+
return null;
|
|
517
|
+
// If this is the only element, just remove it
|
|
518
|
+
if (depsArg.elements.length === 1) {
|
|
519
|
+
return fixer.remove(element);
|
|
520
|
+
}
|
|
521
|
+
// If this is the last element, remove the preceding comma
|
|
522
|
+
if (elementIndex === depsArg.elements.length - 1) {
|
|
523
|
+
const prevElement = depsArg.elements[elementIndex - 1];
|
|
524
|
+
if (prevElement) {
|
|
525
|
+
const range = [
|
|
526
|
+
prevElement.range[1],
|
|
527
|
+
element.range[1],
|
|
528
|
+
];
|
|
529
|
+
return fixer.removeRange(range);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
// Otherwise, remove the element and the following comma
|
|
533
|
+
const nextElement = depsArg.elements[elementIndex + 1];
|
|
534
|
+
if (nextElement) {
|
|
535
|
+
const range = [
|
|
536
|
+
element.range[0],
|
|
537
|
+
nextElement.range[0],
|
|
538
|
+
];
|
|
539
|
+
return fixer.removeRange(range);
|
|
540
|
+
}
|
|
541
|
+
// Fallback to just removing the element
|
|
542
|
+
return fixer.remove(element);
|
|
543
|
+
},
|
|
544
|
+
});
|
|
545
|
+
}
|
|
290
546
|
// 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(', ');
|
|
547
|
+
// Skip reporting if needsEntireObject is true (indicates spread operator usage)
|
|
548
|
+
else if (result.usages.size > 0 && !result.needsEntireObject) {
|
|
549
|
+
const fields = Array.from(result.usages).join(', ');
|
|
294
550
|
context.report({
|
|
295
551
|
node: element,
|
|
296
552
|
messageId: 'avoidEntireObject',
|
|
@@ -299,11 +555,7 @@ exports.noEntireObjectHookDeps = (0, createRule_1.createRule)({
|
|
|
299
555
|
fields,
|
|
300
556
|
},
|
|
301
557
|
fix(fixer) {
|
|
302
|
-
|
|
303
|
-
if (usages.size > 0) {
|
|
304
|
-
return fixer.replaceText(element, Array.from(usages).join(', '));
|
|
305
|
-
}
|
|
306
|
-
return null;
|
|
558
|
+
return fixer.replaceText(element, Array.from(result.usages).join(', '));
|
|
307
559
|
},
|
|
308
560
|
});
|
|
309
561
|
}
|