@blumintinc/eslint-plugin-blumint 1.13.0 → 1.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +169 -139
- package/lib/index.js +200 -9
- package/lib/rules/array-methods-this-context.js +1 -1
- package/lib/rules/avoid-utils-directory.js +0 -4
- package/lib/rules/class-methods-read-top-to-bottom.js +24 -5
- package/lib/rules/consistent-callback-naming.js +43 -4
- package/lib/rules/dynamic-https-errors.d.ts +1 -1
- package/lib/rules/dynamic-https-errors.js +134 -44
- package/lib/rules/enforce-assert-safe-object-key.js +11 -11
- package/lib/rules/enforce-boolean-naming-prefixes.js +11 -235
- package/lib/rules/enforce-callback-memo.js +1 -1
- package/lib/rules/enforce-centralized-mock-firestore.js +1 -1
- package/lib/rules/enforce-console-error.js +35 -7
- package/lib/rules/enforce-date-ttime.d.ts +1 -0
- package/lib/rules/enforce-date-ttime.js +156 -0
- 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.d.ts +2 -1
- package/lib/rules/enforce-dynamic-firebase-imports.js +3 -2
- 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 +1 -1
- package/lib/rules/enforce-f-extension-for-entry-points.d.ts +8 -0
- package/lib/rules/enforce-f-extension-for-entry-points.js +283 -0
- package/lib/rules/enforce-fieldpath-syntax-in-docsetter.js +100 -50
- package/lib/rules/enforce-firestore-doc-ref-generic.js +15 -3
- package/lib/rules/enforce-firestore-facade.js +4 -2
- package/lib/rules/enforce-firestore-set-merge.js +3 -4
- package/lib/rules/enforce-global-constants.js +4 -4
- package/lib/rules/enforce-id-capitalization.js +1 -1
- package/lib/rules/enforce-identifiable-firestore-type.js +108 -58
- package/lib/rules/enforce-memoize-async.js +71 -21
- package/lib/rules/enforce-memoize-getters.js +39 -15
- package/lib/rules/enforce-microdiff.js +1 -1
- package/lib/rules/enforce-object-literal-as-const.js +4 -4
- package/lib/rules/enforce-positive-naming.js +4 -0
- package/lib/rules/enforce-props-argument-name.js +44 -18
- 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 +1 -1
- 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.js +7 -3
- 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 +3818 -4644
- 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 +40 -13
- package/lib/rules/fast-deep-equal-over-microdiff.js +29 -39
- package/lib/rules/firestore-transaction-reads-before-writes.js +50 -4
- package/lib/rules/flatten-push-calls.d.ts +2 -0
- package/lib/rules/flatten-push-calls.js +428 -0
- package/lib/rules/global-const-style.js +66 -23
- 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 +801 -0
- package/lib/rules/memo-nested-react-components.d.ts +8 -0
- package/lib/rules/memo-nested-react-components.js +415 -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.js +9 -3
- package/lib/rules/no-async-foreach.js +215 -12
- package/lib/rules/no-circular-references.d.ts +2 -1
- package/lib/rules/no-circular-references.js +31 -32
- package/lib/rules/no-class-instance-destructuring.js +55 -12
- package/lib/rules/no-complex-cloud-params.js +7 -3
- package/lib/rules/no-compositing-layer-props.js +2 -2
- 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 +527 -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.js +52 -7
- package/lib/rules/no-excessive-parent-chain.js +4 -1
- 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 +67 -12
- 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 +1 -1
- package/lib/rules/no-inline-component-prop.d.ts +10 -0
- package/lib/rules/no-inline-component-prop.js +465 -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.js +6 -6
- package/lib/rules/no-memoize-on-static.js +9 -1
- package/lib/rules/no-misleading-boolean-prefixes.js +7 -3
- 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.js +29 -17
- package/lib/rules/no-passthrough-getters.d.ts +2 -2
- package/lib/rules/no-passthrough-getters.js +120 -2
- 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 +508 -0
- package/lib/rules/no-redundant-usecallback-wrapper.js +16 -3
- 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.js +11 -10
- package/lib/rules/no-separate-loading-state.js +7 -16
- package/lib/rules/no-stale-state-across-await.js +1 -1
- package/lib/rules/no-type-assertion-returns.js +43 -49
- package/lib/rules/no-undefined-null-passthrough.js +61 -31
- 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 +344 -0
- package/lib/rules/no-unnecessary-destructuring.js +14 -4
- package/lib/rules/no-unnecessary-verb-suffix.js +2 -1
- package/lib/rules/no-unpinned-dependencies.js +36 -5
- package/lib/rules/no-unused-props.d.ts +2 -2
- package/lib/rules/no-unused-props.js +295 -91
- 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.js +6 -4
- package/lib/rules/parallelize-async-operations.js +143 -59
- package/lib/rules/prefer-batch-operations.d.ts +1 -3
- package/lib/rules/prefer-batch-operations.js +32 -5
- package/lib/rules/prefer-block-comments-for-declarations.js +9 -4
- 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-field-paths-in-transforms.js +4 -3
- package/lib/rules/prefer-fragment-component.js +1 -1
- 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.js +88 -13
- 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.js +60 -69
- package/lib/rules/prefer-nullish-coalescing-boolean-props.js +118 -8
- package/lib/rules/prefer-nullish-coalescing-override.d.ts +2 -1
- package/lib/rules/prefer-nullish-coalescing-override.js +11 -5
- package/lib/rules/prefer-params-over-parent-id.js +221 -172
- package/lib/rules/prefer-settings-object.js +27 -10
- package/lib/rules/prefer-type-alias-over-typeof-constant.js +9 -0
- package/lib/rules/prefer-type-over-interface.js +7 -2
- package/lib/rules/prefer-url-tostring-over-tojson.js +6 -3
- package/lib/rules/prefer-use-deep-compare-memo.js +8 -5
- package/lib/rules/prefer-usecallback-over-usememo-for-functions.js +1 -1
- package/lib/rules/prefer-usememo-over-useeffect-usestate.js +1 -1
- 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 +540 -0
- package/lib/rules/react-memoize-literals.d.ts +4 -0
- package/lib/rules/react-memoize-literals.js +614 -0
- package/lib/rules/react-usememo-should-be-component.js +2 -2
- 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 +155 -0
- package/lib/rules/require-https-error.js +43 -21
- package/lib/rules/require-image-optimized.js +1 -1
- package/lib/rules/require-memo.js +18 -10
- 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 +2 -3
- package/lib/rules/test-file-location-enforcement.js +1 -1
- package/lib/rules/use-latest-callback.js +6 -5
- 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 +47 -2
- package/lib/utils/ASTHelpers.js +370 -112
- package/lib/utils/getMethodName.d.ts +27 -0
- package/lib/utils/getMethodName.js +35 -0
- package/lib/utils/tsTypeClassifier.d.ts +30 -0
- package/lib/utils/tsTypeClassifier.js +149 -0
- package/package.json +3 -1
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.noEmptyDependencyUseCallbacks = void 0;
|
|
4
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
5
|
+
const minimatch_1 = require("minimatch");
|
|
6
|
+
const ASTHelpers_1 = require("../utils/ASTHelpers");
|
|
7
|
+
const createRule_1 = require("../utils/createRule");
|
|
8
|
+
const DEFAULT_TEST_PATTERNS = ['**/__tests__/**', '**/*.test.*', '**/*.spec.*'];
|
|
9
|
+
const PREFER_UTILITY_FUNCTION_MESSAGE = [
|
|
10
|
+
'What\'s wrong: "{{name}}" uses useCallback([]) but never reads component/hook state',
|
|
11
|
+
'Why it matters: empty-deps callbacks are already stable, so hook bookkeeping adds overhead without benefit',
|
|
12
|
+
'How to fix: move "{{name}}" to a module-level utility (or add an eslint-disable with a short justification if you intentionally keep it for memoized children).',
|
|
13
|
+
].join(' -> ');
|
|
14
|
+
const PREFER_UTILITY_LATEST_MESSAGE = [
|
|
15
|
+
'What\'s wrong: useLatestCallback wraps "{{name}}" even though it never reads component/hook state',
|
|
16
|
+
'Why it matters: the hook wrapper adds indirection without preventing stale closures',
|
|
17
|
+
'How to fix: extract "{{name}}" to a module-level utility (or disable with a brief note if you rely on HMR or a stale-closure pattern).',
|
|
18
|
+
].join(' -> ');
|
|
19
|
+
function isHookCallee(callee, hookName) {
|
|
20
|
+
if (callee.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
21
|
+
return callee.name === hookName;
|
|
22
|
+
}
|
|
23
|
+
return (callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
24
|
+
!callee.computed &&
|
|
25
|
+
callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
26
|
+
callee.property.name === hookName);
|
|
27
|
+
}
|
|
28
|
+
function isUseCallbackCallee(callee) {
|
|
29
|
+
return isHookCallee(callee, 'useCallback');
|
|
30
|
+
}
|
|
31
|
+
function isUseLatestCallbackCallee(callee) {
|
|
32
|
+
return isHookCallee(callee, 'useLatestCallback');
|
|
33
|
+
}
|
|
34
|
+
function isFunctionExpression(node) {
|
|
35
|
+
return (!!node &&
|
|
36
|
+
(node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
37
|
+
node.type === utils_1.AST_NODE_TYPES.FunctionExpression));
|
|
38
|
+
}
|
|
39
|
+
function isPropertyKey(parent, identifier) {
|
|
40
|
+
if (!parent)
|
|
41
|
+
return false;
|
|
42
|
+
if (parent.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
43
|
+
parent.property === identifier &&
|
|
44
|
+
!parent.computed) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
if (parent.type === utils_1.AST_NODE_TYPES.Property &&
|
|
48
|
+
parent.key === identifier &&
|
|
49
|
+
!parent.computed &&
|
|
50
|
+
!parent.shorthand) {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
function collectNearestBlockTypeBindings(node) {
|
|
56
|
+
const localTypes = new Set();
|
|
57
|
+
let current = node.parent ?? undefined;
|
|
58
|
+
const addTypeParameters = (maybeNode) => {
|
|
59
|
+
if (!maybeNode)
|
|
60
|
+
return;
|
|
61
|
+
const typeParameters = maybeNode.typeParameters;
|
|
62
|
+
if (typeParameters &&
|
|
63
|
+
typeParameters.type === utils_1.AST_NODE_TYPES.TSTypeParameterDeclaration) {
|
|
64
|
+
for (const param of typeParameters.params) {
|
|
65
|
+
const name = typeof param.name === 'string' ? param.name : param.name.name;
|
|
66
|
+
localTypes.add(name);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
while (current) {
|
|
71
|
+
addTypeParameters(current);
|
|
72
|
+
if (current.type === utils_1.AST_NODE_TYPES.BlockStatement) {
|
|
73
|
+
for (const statement of current.body) {
|
|
74
|
+
if (statement.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration ||
|
|
75
|
+
statement.type === utils_1.AST_NODE_TYPES.TSInterfaceDeclaration ||
|
|
76
|
+
statement.type === utils_1.AST_NODE_TYPES.ClassDeclaration ||
|
|
77
|
+
statement.type === utils_1.AST_NODE_TYPES.TSEnumDeclaration) {
|
|
78
|
+
localTypes.add(statement.id.name);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (statement.type === utils_1.AST_NODE_TYPES.TSModuleDeclaration &&
|
|
82
|
+
(statement.id.type === utils_1.AST_NODE_TYPES.Identifier ||
|
|
83
|
+
statement.id.type === utils_1.AST_NODE_TYPES.Literal)) {
|
|
84
|
+
const name = statement.id.type === utils_1.AST_NODE_TYPES.Identifier
|
|
85
|
+
? statement.id.name
|
|
86
|
+
: String(statement.id.value);
|
|
87
|
+
localTypes.add(name);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (current.type === utils_1.AST_NODE_TYPES.Program) {
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
current = current.parent ?? undefined;
|
|
95
|
+
}
|
|
96
|
+
return localTypes;
|
|
97
|
+
}
|
|
98
|
+
function usesLocalTypeBindings(typeRoots, localTypes, sourceCode) {
|
|
99
|
+
if (!typeRoots.length || localTypes.size === 0) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
const visitorKeys = sourceCode.visitorKeys;
|
|
103
|
+
const stack = [...typeRoots];
|
|
104
|
+
while (stack.length) {
|
|
105
|
+
const current = stack.pop();
|
|
106
|
+
if (current.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
107
|
+
localTypes.has(current.name)) {
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
if (current.type === utils_1.AST_NODE_TYPES.TSQualifiedName) {
|
|
111
|
+
let left = current.left;
|
|
112
|
+
while (left.type === utils_1.AST_NODE_TYPES.TSQualifiedName) {
|
|
113
|
+
left = left.left;
|
|
114
|
+
}
|
|
115
|
+
if (left.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
116
|
+
localTypes.has(left.name)) {
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
// Qualified names (e.g. External.LocalType) are scoped by their leftmost
|
|
120
|
+
// namespace. Traversing into the right side can falsely match an
|
|
121
|
+
// unrelated local type with the same name.
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const keys = visitorKeys[current.type];
|
|
125
|
+
if (!keys)
|
|
126
|
+
continue;
|
|
127
|
+
for (const key of keys) {
|
|
128
|
+
const value = current[key];
|
|
129
|
+
if (Array.isArray(value)) {
|
|
130
|
+
for (const element of value) {
|
|
131
|
+
if (element && typeof element.type === 'string') {
|
|
132
|
+
stack.push(element);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
else if (value && typeof value.type === 'string') {
|
|
137
|
+
stack.push(value);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
function collectBodyTypeAnnotations(node, sourceCode) {
|
|
144
|
+
const typeNodes = [];
|
|
145
|
+
const visitorKeys = sourceCode.visitorKeys;
|
|
146
|
+
const stack = [node];
|
|
147
|
+
while (stack.length) {
|
|
148
|
+
const current = stack.pop();
|
|
149
|
+
if (current.type === utils_1.AST_NODE_TYPES.TSAsExpression ||
|
|
150
|
+
current.type === utils_1.AST_NODE_TYPES.TSSatisfiesExpression) {
|
|
151
|
+
typeNodes.push(current.typeAnnotation);
|
|
152
|
+
}
|
|
153
|
+
else if (current.type === utils_1.AST_NODE_TYPES.TSTypeAssertion) {
|
|
154
|
+
typeNodes.push(current.typeAnnotation);
|
|
155
|
+
}
|
|
156
|
+
const typeAnnotation = current
|
|
157
|
+
.typeAnnotation;
|
|
158
|
+
if (typeAnnotation) {
|
|
159
|
+
if (typeAnnotation.type === utils_1.AST_NODE_TYPES.TSTypeAnnotation) {
|
|
160
|
+
typeNodes.push(typeAnnotation.typeAnnotation);
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
typeNodes.push(typeAnnotation);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const typeParameters = current.typeParameters;
|
|
167
|
+
if (typeParameters) {
|
|
168
|
+
typeNodes.push(typeParameters);
|
|
169
|
+
}
|
|
170
|
+
const returnType = current.returnType;
|
|
171
|
+
if (returnType) {
|
|
172
|
+
typeNodes.push(returnType);
|
|
173
|
+
}
|
|
174
|
+
const keys = visitorKeys[current.type];
|
|
175
|
+
if (!keys)
|
|
176
|
+
continue;
|
|
177
|
+
for (const key of keys) {
|
|
178
|
+
const value = current[key];
|
|
179
|
+
if (Array.isArray(value)) {
|
|
180
|
+
for (const element of value) {
|
|
181
|
+
if (element && typeof element.type === 'string') {
|
|
182
|
+
stack.push(element);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
else if (value && typeof value.type === 'string') {
|
|
187
|
+
stack.push(value);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return typeNodes;
|
|
192
|
+
}
|
|
193
|
+
function findHoistTarget(node) {
|
|
194
|
+
let current = node;
|
|
195
|
+
while (current?.parent) {
|
|
196
|
+
if (current.parent.type === utils_1.AST_NODE_TYPES.Program) {
|
|
197
|
+
return current;
|
|
198
|
+
}
|
|
199
|
+
if (current.parent.type === utils_1.AST_NODE_TYPES.ExportNamedDeclaration ||
|
|
200
|
+
current.parent.type === utils_1.AST_NODE_TYPES.ExportDefaultDeclaration) {
|
|
201
|
+
return current.parent;
|
|
202
|
+
}
|
|
203
|
+
current = current.parent;
|
|
204
|
+
}
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
function getCallbackArg(node) {
|
|
208
|
+
const [maybeFn] = node.arguments;
|
|
209
|
+
if (isFunctionExpression(maybeFn)) {
|
|
210
|
+
return maybeFn;
|
|
211
|
+
}
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
function hasEmptyDependencyArray(node) {
|
|
215
|
+
if (node.arguments.length < 2)
|
|
216
|
+
return false;
|
|
217
|
+
const deps = node.arguments[1];
|
|
218
|
+
return (deps.type === utils_1.AST_NODE_TYPES.ArrayExpression && deps.elements.length === 0);
|
|
219
|
+
}
|
|
220
|
+
function filenameMatchesPatterns(filename, patterns) {
|
|
221
|
+
const normalized = filename.replace(/\\/g, '/');
|
|
222
|
+
const basename = normalized.split('/').pop() ?? normalized;
|
|
223
|
+
return patterns.some((pattern) => (0, minimatch_1.minimatch)(normalized, pattern) || (0, minimatch_1.minimatch)(basename, pattern));
|
|
224
|
+
}
|
|
225
|
+
function usesThisOrSuper(node, sourceCode) {
|
|
226
|
+
const visitorKeys = sourceCode.visitorKeys;
|
|
227
|
+
const stack = [node];
|
|
228
|
+
while (stack.length) {
|
|
229
|
+
const current = stack.pop();
|
|
230
|
+
if (current.type === utils_1.AST_NODE_TYPES.ThisExpression ||
|
|
231
|
+
current.type === utils_1.AST_NODE_TYPES.Super) {
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
const keys = visitorKeys[current.type];
|
|
235
|
+
if (!keys)
|
|
236
|
+
continue;
|
|
237
|
+
for (const key of keys) {
|
|
238
|
+
const value = current[key];
|
|
239
|
+
if (Array.isArray(value)) {
|
|
240
|
+
for (const element of value) {
|
|
241
|
+
if (element && typeof element.type === 'string') {
|
|
242
|
+
stack.push(element);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
else if (value && typeof value.type === 'string') {
|
|
247
|
+
stack.push(value);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
function analyzeExternalReferences(context, fn, extraTypeRoots = []) {
|
|
254
|
+
const sourceCode = context.getSourceCode();
|
|
255
|
+
if (usesThisOrSuper(fn.body, sourceCode)) {
|
|
256
|
+
return { hasComponentScopeRef: true };
|
|
257
|
+
}
|
|
258
|
+
let hasComponentScopeRef = false;
|
|
259
|
+
const scopeManager = sourceCode.scopeManager;
|
|
260
|
+
if (!scopeManager) {
|
|
261
|
+
return { hasComponentScopeRef: true };
|
|
262
|
+
}
|
|
263
|
+
const scope = scopeManager.acquire(fn, true) ?? scopeManager.acquire(fn);
|
|
264
|
+
if (!scope) {
|
|
265
|
+
return { hasComponentScopeRef: true };
|
|
266
|
+
}
|
|
267
|
+
if (!hasComponentScopeRef) {
|
|
268
|
+
for (const ref of scope.through) {
|
|
269
|
+
const identifier = ref.identifier;
|
|
270
|
+
if (isPropertyKey(identifier.parent, identifier)) {
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
const resolved = ref.resolved;
|
|
274
|
+
if (!resolved) {
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
const def = resolved.defs[0];
|
|
278
|
+
const scopeType = resolved.scope.type;
|
|
279
|
+
const isImport = def?.type === 'ImportBinding';
|
|
280
|
+
const isModuleOrGlobal = scopeType === 'module' || scopeType === 'global';
|
|
281
|
+
if (!isImport && !isModuleOrGlobal) {
|
|
282
|
+
hasComponentScopeRef = true;
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if (!hasComponentScopeRef) {
|
|
288
|
+
const typeRoots = [];
|
|
289
|
+
if (fn.returnType) {
|
|
290
|
+
typeRoots.push(fn.returnType.typeAnnotation);
|
|
291
|
+
}
|
|
292
|
+
if (fn.typeParameters) {
|
|
293
|
+
typeRoots.push(fn.typeParameters);
|
|
294
|
+
}
|
|
295
|
+
for (const param of fn.params) {
|
|
296
|
+
if ('typeAnnotation' in param && param.typeAnnotation) {
|
|
297
|
+
typeRoots.push(param.typeAnnotation.typeAnnotation);
|
|
298
|
+
}
|
|
299
|
+
else if (param.type === utils_1.AST_NODE_TYPES.AssignmentPattern &&
|
|
300
|
+
'typeAnnotation' in param.left &&
|
|
301
|
+
param.left.typeAnnotation) {
|
|
302
|
+
typeRoots.push(param.left.typeAnnotation.typeAnnotation);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
typeRoots.push(...collectBodyTypeAnnotations(fn.body, sourceCode));
|
|
306
|
+
typeRoots.push(...extraTypeRoots);
|
|
307
|
+
if (typeRoots.some((typeRoot) => usesThisOrSuper(typeRoot, sourceCode))) {
|
|
308
|
+
hasComponentScopeRef = true;
|
|
309
|
+
}
|
|
310
|
+
if (!hasComponentScopeRef) {
|
|
311
|
+
const localTypes = collectNearestBlockTypeBindings(fn);
|
|
312
|
+
if (localTypes.size > 0 &&
|
|
313
|
+
usesLocalTypeBindings(typeRoots, localTypes, sourceCode)) {
|
|
314
|
+
hasComponentScopeRef = true;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return { hasComponentScopeRef };
|
|
319
|
+
}
|
|
320
|
+
function getProgramNode(node) {
|
|
321
|
+
let current = node;
|
|
322
|
+
while (current && current.type !== utils_1.AST_NODE_TYPES.Program) {
|
|
323
|
+
current = current.parent ?? undefined;
|
|
324
|
+
}
|
|
325
|
+
return current?.type === utils_1.AST_NODE_TYPES.Program ? current : null;
|
|
326
|
+
}
|
|
327
|
+
function addPatternIdentifiersToSet(pattern, names) {
|
|
328
|
+
switch (pattern.type) {
|
|
329
|
+
case utils_1.AST_NODE_TYPES.Identifier:
|
|
330
|
+
names.add(pattern.name);
|
|
331
|
+
return;
|
|
332
|
+
case utils_1.AST_NODE_TYPES.ObjectPattern:
|
|
333
|
+
for (const property of pattern.properties) {
|
|
334
|
+
if (property.type === utils_1.AST_NODE_TYPES.Property) {
|
|
335
|
+
addPatternIdentifiersToSet(property.value, names);
|
|
336
|
+
}
|
|
337
|
+
else if (property.type === utils_1.AST_NODE_TYPES.RestElement) {
|
|
338
|
+
addPatternIdentifiersToSet(property.argument, names);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return;
|
|
342
|
+
case utils_1.AST_NODE_TYPES.ArrayPattern:
|
|
343
|
+
for (const element of pattern.elements) {
|
|
344
|
+
if (!element)
|
|
345
|
+
continue;
|
|
346
|
+
if (element.type === utils_1.AST_NODE_TYPES.RestElement) {
|
|
347
|
+
addPatternIdentifiersToSet(element.argument, names);
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
350
|
+
addPatternIdentifiersToSet(element, names);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return;
|
|
354
|
+
case utils_1.AST_NODE_TYPES.RestElement:
|
|
355
|
+
addPatternIdentifiersToSet(pattern.argument, names);
|
|
356
|
+
return;
|
|
357
|
+
case utils_1.AST_NODE_TYPES.AssignmentPattern:
|
|
358
|
+
addPatternIdentifiersToSet(pattern.left, names);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
function getModuleScopeValueBindings(program) {
|
|
363
|
+
const names = new Set();
|
|
364
|
+
for (const statement of program.body) {
|
|
365
|
+
const target = statement.type === utils_1.AST_NODE_TYPES.ExportNamedDeclaration ||
|
|
366
|
+
statement.type === utils_1.AST_NODE_TYPES.ExportDefaultDeclaration
|
|
367
|
+
? statement.declaration
|
|
368
|
+
: statement;
|
|
369
|
+
if (!target)
|
|
370
|
+
continue;
|
|
371
|
+
if (target.type === utils_1.AST_NODE_TYPES.ImportDeclaration) {
|
|
372
|
+
for (const specifier of target.specifiers) {
|
|
373
|
+
names.add(specifier.local.name);
|
|
374
|
+
}
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
if (target.type === utils_1.AST_NODE_TYPES.VariableDeclaration) {
|
|
378
|
+
for (const declaration of target.declarations) {
|
|
379
|
+
addPatternIdentifiersToSet(declaration.id, names);
|
|
380
|
+
}
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
if (target.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
|
|
384
|
+
target.type === utils_1.AST_NODE_TYPES.ClassDeclaration ||
|
|
385
|
+
target.type === utils_1.AST_NODE_TYPES.TSEnumDeclaration) {
|
|
386
|
+
if (target.id) {
|
|
387
|
+
names.add(target.id.name);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return names;
|
|
392
|
+
}
|
|
393
|
+
function buildHoistFixes(context, callExpression, callback, hoistedIdentifierCache) {
|
|
394
|
+
if (!callExpression.parent ||
|
|
395
|
+
callExpression.parent.type !== utils_1.AST_NODE_TYPES.VariableDeclarator) {
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
const declarator = callExpression.parent;
|
|
399
|
+
if (declarator.id.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
const varDecl = declarator.parent;
|
|
403
|
+
if (!varDecl ||
|
|
404
|
+
varDecl.type !== utils_1.AST_NODE_TYPES.VariableDeclaration ||
|
|
405
|
+
varDecl.declarations.length !== 1) {
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
if (varDecl.kind !== 'const') {
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
if (!varDecl.parent ||
|
|
412
|
+
varDecl.parent.type !== utils_1.AST_NODE_TYPES.BlockStatement) {
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
const hoistTarget = findHoistTarget(varDecl.parent);
|
|
416
|
+
if (!hoistTarget) {
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
const programNode = getProgramNode(hoistTarget);
|
|
420
|
+
let moduleBindings = null;
|
|
421
|
+
if (programNode) {
|
|
422
|
+
const cachedBindings = hoistedIdentifierCache.get(programNode);
|
|
423
|
+
moduleBindings = cachedBindings ?? getModuleScopeValueBindings(programNode);
|
|
424
|
+
if (!cachedBindings) {
|
|
425
|
+
hoistedIdentifierCache.set(programNode, moduleBindings);
|
|
426
|
+
}
|
|
427
|
+
if (moduleBindings.has(declarator.id.name)) {
|
|
428
|
+
return null;
|
|
429
|
+
}
|
|
430
|
+
// Reserve the identifier so later callbacks in the same file with the same
|
|
431
|
+
// name do not attempt to hoist and produce duplicate declarations.
|
|
432
|
+
moduleBindings.add(declarator.id.name);
|
|
433
|
+
}
|
|
434
|
+
const sourceCode = context.getSourceCode();
|
|
435
|
+
// We use the full range of the id to ensure we capture any type annotations.
|
|
436
|
+
// In @typescript-eslint/parser, the Identifier range already includes the
|
|
437
|
+
// type annotation, but we explicitly calculate the end range for robustness.
|
|
438
|
+
const idRangeEnd = declarator.id.typeAnnotation &&
|
|
439
|
+
declarator.id.typeAnnotation.range[1] > declarator.id.range[1]
|
|
440
|
+
? declarator.id.typeAnnotation.range[1]
|
|
441
|
+
: declarator.id.range[1];
|
|
442
|
+
const identifierText = sourceCode
|
|
443
|
+
.getText()
|
|
444
|
+
.slice(declarator.id.range[0], idRangeEnd);
|
|
445
|
+
const functionText = sourceCode.getText(callback);
|
|
446
|
+
const hoisted = `const ${identifierText} = ${functionText};\n`;
|
|
447
|
+
const fileText = sourceCode.getText();
|
|
448
|
+
let removeStart = varDecl.range[0];
|
|
449
|
+
const lineStart = fileText.lastIndexOf('\n', removeStart - 1) + 1;
|
|
450
|
+
const leadingSegment = fileText.slice(lineStart, removeStart);
|
|
451
|
+
const hasOnlyIndentBefore = /^[ \t]*$/.test(leadingSegment);
|
|
452
|
+
if (hasOnlyIndentBefore) {
|
|
453
|
+
removeStart = lineStart;
|
|
454
|
+
}
|
|
455
|
+
let removeEnd = varDecl.range[1];
|
|
456
|
+
const lineEnd = fileText.indexOf('\n', removeEnd);
|
|
457
|
+
const segmentEnd = lineEnd === -1 ? fileText.length : lineEnd;
|
|
458
|
+
const trailingSegment = fileText.slice(removeEnd, segmentEnd);
|
|
459
|
+
const hasOnlyWhitespaceAfter = /^[ \t]*$/.test(trailingSegment);
|
|
460
|
+
if (hasOnlyWhitespaceAfter) {
|
|
461
|
+
removeEnd = segmentEnd;
|
|
462
|
+
}
|
|
463
|
+
if (hasOnlyIndentBefore && hasOnlyWhitespaceAfter && lineEnd !== -1) {
|
|
464
|
+
removeEnd = lineEnd + 1;
|
|
465
|
+
}
|
|
466
|
+
return (fixer) => [
|
|
467
|
+
fixer.insertTextBefore(hoistTarget, hoisted),
|
|
468
|
+
fixer.removeRange([removeStart, removeEnd]),
|
|
469
|
+
];
|
|
470
|
+
}
|
|
471
|
+
exports.noEmptyDependencyUseCallbacks = (0, createRule_1.createRule)({
|
|
472
|
+
name: 'no-empty-dependency-use-callbacks',
|
|
473
|
+
meta: {
|
|
474
|
+
type: 'suggestion',
|
|
475
|
+
docs: {
|
|
476
|
+
description: 'Discourage useCallback([]) or useLatestCallback around static functions. Static callbacks do not need hook machinery—extract them to module-level utilities for clarity and to avoid unnecessary hook overhead.',
|
|
477
|
+
recommended: 'error',
|
|
478
|
+
},
|
|
479
|
+
fixable: 'code',
|
|
480
|
+
schema: [
|
|
481
|
+
{
|
|
482
|
+
type: 'object',
|
|
483
|
+
properties: {
|
|
484
|
+
ignoreTestFiles: { type: 'boolean', default: true },
|
|
485
|
+
testFilePatterns: {
|
|
486
|
+
type: 'array',
|
|
487
|
+
items: { type: 'string' },
|
|
488
|
+
default: DEFAULT_TEST_PATTERNS,
|
|
489
|
+
},
|
|
490
|
+
ignoreUseLatestCallback: { type: 'boolean', default: false },
|
|
491
|
+
},
|
|
492
|
+
additionalProperties: false,
|
|
493
|
+
},
|
|
494
|
+
],
|
|
495
|
+
messages: {
|
|
496
|
+
preferUtilityFunction: PREFER_UTILITY_FUNCTION_MESSAGE,
|
|
497
|
+
preferUtilityLatest: PREFER_UTILITY_LATEST_MESSAGE,
|
|
498
|
+
},
|
|
499
|
+
},
|
|
500
|
+
defaultOptions: [{}],
|
|
501
|
+
create(context) {
|
|
502
|
+
const [options] = context.options;
|
|
503
|
+
const ignoreTestFiles = options?.ignoreTestFiles !== false;
|
|
504
|
+
const testPatterns = options?.testFilePatterns ?? DEFAULT_TEST_PATTERNS;
|
|
505
|
+
const ignoreUseLatestCallback = options?.ignoreUseLatestCallback === true;
|
|
506
|
+
const hoistedIdentifierCache = new WeakMap();
|
|
507
|
+
const filename = context.getFilename();
|
|
508
|
+
const isTest = ignoreTestFiles && filenameMatchesPatterns(filename, testPatterns);
|
|
509
|
+
function reportIfStaticCallback(callExpression, messageId) {
|
|
510
|
+
const callee = callExpression.callee;
|
|
511
|
+
if (callee.type !== utils_1.AST_NODE_TYPES.Identifier &&
|
|
512
|
+
callee.type !== utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
const callback = getCallbackArg(callExpression);
|
|
516
|
+
if (!callback)
|
|
517
|
+
return;
|
|
518
|
+
if (ASTHelpers_1.ASTHelpers.returnsJSX(callback.body, context))
|
|
519
|
+
return;
|
|
520
|
+
const extraTypeRoots = [];
|
|
521
|
+
if (callExpression.parent &&
|
|
522
|
+
callExpression.parent.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
523
|
+
'typeAnnotation' in callExpression.parent.id &&
|
|
524
|
+
callExpression.parent.id.typeAnnotation) {
|
|
525
|
+
const typeAnnotation = callExpression.parent.id.typeAnnotation;
|
|
526
|
+
if (typeAnnotation.type === utils_1.AST_NODE_TYPES.TSTypeAnnotation) {
|
|
527
|
+
extraTypeRoots.push(typeAnnotation.typeAnnotation);
|
|
528
|
+
}
|
|
529
|
+
else {
|
|
530
|
+
extraTypeRoots.push(typeAnnotation);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
const { hasComponentScopeRef } = analyzeExternalReferences(context, callback, extraTypeRoots);
|
|
534
|
+
if (hasComponentScopeRef) {
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
const callbackName = callExpression.parent &&
|
|
538
|
+
callExpression.parent.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
539
|
+
callExpression.parent.id.type === utils_1.AST_NODE_TYPES.Identifier
|
|
540
|
+
? callExpression.parent.id.name
|
|
541
|
+
: 'this callback';
|
|
542
|
+
const fix = buildHoistFixes(context, callExpression, callback, hoistedIdentifierCache);
|
|
543
|
+
context.report({
|
|
544
|
+
node: callExpression,
|
|
545
|
+
messageId,
|
|
546
|
+
data: { name: callbackName },
|
|
547
|
+
fix,
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
return {
|
|
551
|
+
CallExpression(node) {
|
|
552
|
+
if (isTest)
|
|
553
|
+
return;
|
|
554
|
+
const { callee } = node;
|
|
555
|
+
if (callee.type !== utils_1.AST_NODE_TYPES.Identifier &&
|
|
556
|
+
callee.type !== utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
if (isUseCallbackCallee(callee)) {
|
|
560
|
+
if (!hasEmptyDependencyArray(node)) {
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
reportIfStaticCallback(node, 'preferUtilityFunction');
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
if (ignoreUseLatestCallback)
|
|
567
|
+
return;
|
|
568
|
+
if (isUseLatestCallbackCallee(callee)) {
|
|
569
|
+
reportIfStaticCallback(node, 'preferUtilityLatest');
|
|
570
|
+
}
|
|
571
|
+
},
|
|
572
|
+
};
|
|
573
|
+
},
|
|
574
|
+
});
|
|
575
|
+
exports.default = exports.noEmptyDependencyUseCallbacks;
|
|
576
|
+
//# sourceMappingURL=no-empty-dependency-use-callbacks.js.map
|
|
@@ -61,6 +61,7 @@ context) {
|
|
|
61
61
|
const usages = new Map(); // Track usage and its position
|
|
62
62
|
const visited = new Set();
|
|
63
63
|
let needsEntireObject = false;
|
|
64
|
+
let isUsed = false;
|
|
64
65
|
// Built-in array methods that indicate usage of the entire array
|
|
65
66
|
const ARRAY_METHODS = new Set([
|
|
66
67
|
'map',
|
|
@@ -153,9 +154,7 @@ context) {
|
|
|
153
154
|
else {
|
|
154
155
|
// For other computed properties, use the exact expression
|
|
155
156
|
try {
|
|
156
|
-
const propertyText = context
|
|
157
|
-
.getSourceCode()
|
|
158
|
-
.getText(memberExpr.property);
|
|
157
|
+
const propertyText = context.sourceCode.getText(memberExpr.property);
|
|
159
158
|
parts.unshift(`[${propertyText}]`);
|
|
160
159
|
}
|
|
161
160
|
catch (e) {
|
|
@@ -232,6 +231,52 @@ context) {
|
|
|
232
231
|
if (!node || visited.has(node))
|
|
233
232
|
return;
|
|
234
233
|
visited.add(node);
|
|
234
|
+
// Direct usage of the target identifier to distinguish between:
|
|
235
|
+
// 1. Never used (not even present) → suggest removal
|
|
236
|
+
// 2. Used in ways requiring entire object → no suggestion
|
|
237
|
+
// 3. Used only via specific fields → suggest replacing with fields
|
|
238
|
+
if (node.type === utils_1.AST_NODE_TYPES.Identifier && node.name === objectName) {
|
|
239
|
+
const parent = node.parent;
|
|
240
|
+
// Exclude: property name in `other.objectName` (not our target object)
|
|
241
|
+
const isMemberProperty = parent?.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
242
|
+
parent.property === node &&
|
|
243
|
+
!parent.computed;
|
|
244
|
+
// Exclude: object in `objectName.prop` (handled by MemberExpression visitor for field tracking)
|
|
245
|
+
const isMemberObject = parent?.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
246
|
+
parent.object === node;
|
|
247
|
+
// Exclude: key in `{ objectName: value }` (not usage, just a label)
|
|
248
|
+
// Include: shorthand `{ objectName }` (actual usage)
|
|
249
|
+
const isPropertyKey = parent?.type === utils_1.AST_NODE_TYPES.Property &&
|
|
250
|
+
parent.key === node &&
|
|
251
|
+
!parent.computed &&
|
|
252
|
+
!parent.shorthand;
|
|
253
|
+
if (!isMemberProperty && !isMemberObject && !isPropertyKey) {
|
|
254
|
+
isUsed = true;
|
|
255
|
+
// Patterns that require the entire object (cannot refactor to specific fields)
|
|
256
|
+
const isTypeAUsage = parent?.type === utils_1.AST_NODE_TYPES.ReturnStatement ||
|
|
257
|
+
parent?.type === utils_1.AST_NODE_TYPES.ArrayExpression ||
|
|
258
|
+
parent?.type === utils_1.AST_NODE_TYPES.BinaryExpression ||
|
|
259
|
+
parent?.type === utils_1.AST_NODE_TYPES.LogicalExpression ||
|
|
260
|
+
parent?.type === utils_1.AST_NODE_TYPES.ConditionalExpression ||
|
|
261
|
+
parent?.type === utils_1.AST_NODE_TYPES.UnaryExpression ||
|
|
262
|
+
(parent?.type === utils_1.AST_NODE_TYPES.Property &&
|
|
263
|
+
(parent.value === node ||
|
|
264
|
+
parent.shorthand ||
|
|
265
|
+
(parent.key === node && parent.computed))) ||
|
|
266
|
+
parent?.type === utils_1.AST_NODE_TYPES.TemplateLiteral ||
|
|
267
|
+
parent?.type === utils_1.AST_NODE_TYPES.VariableDeclarator ||
|
|
268
|
+
parent?.type === utils_1.AST_NODE_TYPES.AssignmentExpression ||
|
|
269
|
+
parent?.type === utils_1.AST_NODE_TYPES.JSXExpressionContainer ||
|
|
270
|
+
parent?.type === utils_1.AST_NODE_TYPES.JSXSpreadAttribute ||
|
|
271
|
+
parent?.type === utils_1.AST_NODE_TYPES.SpreadElement ||
|
|
272
|
+
parent?.type === utils_1.AST_NODE_TYPES.ForInStatement ||
|
|
273
|
+
parent?.type === utils_1.AST_NODE_TYPES.ForOfStatement ||
|
|
274
|
+
parent?.type === utils_1.AST_NODE_TYPES.CallExpression;
|
|
275
|
+
if (isTypeAUsage) {
|
|
276
|
+
needsEntireObject = true;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
235
280
|
if (node.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
236
281
|
// Check if the object is being called as a function
|
|
237
282
|
if (node.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
@@ -433,7 +478,7 @@ context) {
|
|
|
433
478
|
return posA - posB;
|
|
434
479
|
});
|
|
435
480
|
const filteredUsages = new Set(sortedPaths);
|
|
436
|
-
const notUsed = !needsEntireObject && filteredUsages.size === 0;
|
|
481
|
+
const notUsed = !needsEntireObject && !isUsed && filteredUsages.size === 0;
|
|
437
482
|
return {
|
|
438
483
|
usages: filteredUsages,
|
|
439
484
|
needsEntireObject,
|
|
@@ -445,15 +490,15 @@ exports.noEntireObjectHookDeps = (0, createRule_1.createRule)({
|
|
|
445
490
|
meta: {
|
|
446
491
|
type: 'suggestion',
|
|
447
492
|
docs: {
|
|
448
|
-
description: 'Avoid using entire objects in React hook dependency arrays
|
|
493
|
+
description: 'Avoid using entire objects in React hook dependency arrays.',
|
|
449
494
|
recommended: 'error',
|
|
450
495
|
requiresTypeChecking: true,
|
|
451
496
|
},
|
|
452
497
|
fixable: 'code',
|
|
453
498
|
schema: [],
|
|
454
499
|
messages: {
|
|
455
|
-
avoidEntireObject: '
|
|
456
|
-
removeUnusedDependency: '
|
|
500
|
+
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.',
|
|
501
|
+
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.',
|
|
457
502
|
},
|
|
458
503
|
},
|
|
459
504
|
defaultOptions: [],
|