@blumintinc/eslint-plugin-blumint 1.13.0 → 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 +170 -139
- package/lib/index.js +190 -1
- package/lib/rules/array-methods-this-context.js +1 -1
- 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.js +3 -4
- package/lib/rules/enforce-assert-safe-object-key.js +11 -11
- package/lib/rules/enforce-boolean-naming-prefixes.js +11 -23
- 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-dynamic-file-naming.d.ts +2 -0
- package/lib/rules/enforce-dynamic-file-naming.js +53 -28
- 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-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 +1 -1
- package/lib/rules/enforce-identifiable-firestore-type.js +108 -58
- package/lib/rules/enforce-memoize-async.js +2 -6
- 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 +2 -2
- 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 +5 -4
- 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 +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/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 +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 +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 +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.js +4 -6
- package/lib/rules/no-excessive-parent-chain.js +1 -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 +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.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.js +38 -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 +459 -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 +347 -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 +24 -5
- 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 +9 -4
- 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 +220 -171
- package/lib/rules/prefer-settings-object.js +2 -2
- 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-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.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 +136 -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 +1 -1
- 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 +13 -1
- package/lib/utils/ASTHelpers.js +24 -0
- 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,495 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.reactMemoizeLiterals = void 0;
|
|
4
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
5
|
+
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
const LITERAL_DESCRIPTOR_BY_TYPE = {
|
|
7
|
+
[utils_1.AST_NODE_TYPES.ObjectExpression]: {
|
|
8
|
+
literalType: 'object literal',
|
|
9
|
+
memoHook: 'useMemo',
|
|
10
|
+
},
|
|
11
|
+
[utils_1.AST_NODE_TYPES.ArrayExpression]: {
|
|
12
|
+
literalType: 'array literal',
|
|
13
|
+
memoHook: 'useMemo',
|
|
14
|
+
},
|
|
15
|
+
[utils_1.AST_NODE_TYPES.ArrowFunctionExpression]: {
|
|
16
|
+
literalType: 'inline function',
|
|
17
|
+
memoHook: 'useCallback',
|
|
18
|
+
},
|
|
19
|
+
[utils_1.AST_NODE_TYPES.FunctionExpression]: {
|
|
20
|
+
literalType: 'inline function',
|
|
21
|
+
memoHook: 'useCallback',
|
|
22
|
+
},
|
|
23
|
+
[utils_1.AST_NODE_TYPES.FunctionDeclaration]: {
|
|
24
|
+
literalType: 'inline function',
|
|
25
|
+
memoHook: 'useCallback',
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
const SAFE_HOOK_ARGUMENTS = new Set([
|
|
29
|
+
'useMemo',
|
|
30
|
+
'useCallback',
|
|
31
|
+
'useEffect',
|
|
32
|
+
'useLayoutEffect',
|
|
33
|
+
'useInsertionEffect',
|
|
34
|
+
'useImperativeHandle',
|
|
35
|
+
'useState',
|
|
36
|
+
'useReducer',
|
|
37
|
+
'useRef',
|
|
38
|
+
'useSyncExternalStore',
|
|
39
|
+
'useDeferredValue',
|
|
40
|
+
'useTransition',
|
|
41
|
+
'useId',
|
|
42
|
+
]);
|
|
43
|
+
const MEMOIZATION_DEPS_TODO_PLACEHOLDER = '__TODO_MEMOIZATION_DEPENDENCIES__';
|
|
44
|
+
const TODO_DEPS_COMMENT = `/* ${MEMOIZATION_DEPS_TODO_PLACEHOLDER} */`;
|
|
45
|
+
const PARENTHESIZED_EXPRESSION_TYPE = utils_1.AST_NODE_TYPES.ParenthesizedExpression ??
|
|
46
|
+
'ParenthesizedExpression';
|
|
47
|
+
/**
|
|
48
|
+
* Detects React hook-style identifiers prefixed with "use".
|
|
49
|
+
* @param name Candidate identifier name.
|
|
50
|
+
* @returns True when the name follows the hook naming convention.
|
|
51
|
+
*/
|
|
52
|
+
function isHookName(name) {
|
|
53
|
+
return !!name && /^use[A-Z]/.test(name);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Detects PascalCase identifiers commonly used for React components.
|
|
57
|
+
* @param name Candidate identifier name.
|
|
58
|
+
* @returns True when the name begins with an uppercase character.
|
|
59
|
+
*/
|
|
60
|
+
function isComponentName(name) {
|
|
61
|
+
return !!name && /^[A-Z]/.test(name);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Type guard for parenthesized expressions to unwrap safely.
|
|
65
|
+
* @param node Node to evaluate.
|
|
66
|
+
* @returns True when the node is a parenthesized expression wrapper.
|
|
67
|
+
*/
|
|
68
|
+
function isParenthesizedExpression(node) {
|
|
69
|
+
return node.type === PARENTHESIZED_EXPRESSION_TYPE;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Extracts an identifier name from variable, assignment, or property nodes.
|
|
73
|
+
* @param node AST node that may carry an identifier.
|
|
74
|
+
* @returns Identifier name when present, otherwise null.
|
|
75
|
+
*/
|
|
76
|
+
function getNameFromNode(node) {
|
|
77
|
+
if (!node)
|
|
78
|
+
return null;
|
|
79
|
+
if (node.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
80
|
+
node.id.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
81
|
+
return node.id.name;
|
|
82
|
+
}
|
|
83
|
+
if (node.type === utils_1.AST_NODE_TYPES.AssignmentExpression &&
|
|
84
|
+
node.left.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
85
|
+
return node.left.name;
|
|
86
|
+
}
|
|
87
|
+
if (node.type === utils_1.AST_NODE_TYPES.Property &&
|
|
88
|
+
node.key.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
89
|
+
return node.key.name;
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Determines whether a node should be traversed through when resolving names.
|
|
95
|
+
* @param node Node to evaluate for transparency.
|
|
96
|
+
* @returns True when the node does not introduce a binding boundary.
|
|
97
|
+
*/
|
|
98
|
+
function isTransparentNode(node) {
|
|
99
|
+
return (node.type === utils_1.AST_NODE_TYPES.CallExpression ||
|
|
100
|
+
node.type === utils_1.AST_NODE_TYPES.MemberExpression ||
|
|
101
|
+
isExpressionWrapper(node));
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Walks ancestors through transparent nodes to find the nearest identifier name.
|
|
105
|
+
* @param startNode Node where the search begins.
|
|
106
|
+
* @returns Resolved identifier name or null if none is located.
|
|
107
|
+
*/
|
|
108
|
+
function findNameInAncestors(startNode) {
|
|
109
|
+
let current = startNode;
|
|
110
|
+
while (current) {
|
|
111
|
+
const name = getNameFromNode(current);
|
|
112
|
+
if (name) {
|
|
113
|
+
return name;
|
|
114
|
+
}
|
|
115
|
+
if (!isTransparentNode(current)) {
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
current = current.parent;
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Resolves a function's display name from its declaration or surrounding
|
|
124
|
+
* assignment/property wrappers so HOC-wrapped and asserted components still map
|
|
125
|
+
* to their intended identifiers.
|
|
126
|
+
*/
|
|
127
|
+
function getFunctionName(fn) {
|
|
128
|
+
if (fn.type === utils_1.AST_NODE_TYPES.FunctionDeclaration) {
|
|
129
|
+
return fn.id?.name ?? null;
|
|
130
|
+
}
|
|
131
|
+
if (fn.type === utils_1.AST_NODE_TYPES.FunctionExpression && fn.id?.name) {
|
|
132
|
+
return fn.id.name;
|
|
133
|
+
}
|
|
134
|
+
const parent = fn.parent;
|
|
135
|
+
if (!parent) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
const immediateName = getNameFromNode(parent);
|
|
139
|
+
if (immediateName) {
|
|
140
|
+
return immediateName;
|
|
141
|
+
}
|
|
142
|
+
return findNameInAncestors(parent);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Checks whether a function should be treated as a React component or hook
|
|
146
|
+
* based on naming conventions. Enables the rule to limit reports to user-facing
|
|
147
|
+
* components and hooks rather than arbitrary functions.
|
|
148
|
+
*/
|
|
149
|
+
function isComponentOrHookFunction(fn) {
|
|
150
|
+
const name = getFunctionName(fn);
|
|
151
|
+
return isComponentName(name) || isHookName(name);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Checks whether a function node represents a hook by name.
|
|
155
|
+
* @param fn Function node to inspect.
|
|
156
|
+
* @returns True when the function follows hook naming.
|
|
157
|
+
*/
|
|
158
|
+
function isHookFunction(fn) {
|
|
159
|
+
return isHookName(getFunctionName(fn));
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Narrows nodes to the supported function-like shapes.
|
|
163
|
+
* @param node Node to test.
|
|
164
|
+
* @returns True when the node is any function declaration or expression.
|
|
165
|
+
*/
|
|
166
|
+
function isFunctionNode(node) {
|
|
167
|
+
return (node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
|
|
168
|
+
node.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
169
|
+
node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Extracts a hook name from simple identifier or member-expression callees.
|
|
173
|
+
* @param callee Callee expression from a call.
|
|
174
|
+
* @returns Hook name when available, otherwise null.
|
|
175
|
+
*/
|
|
176
|
+
function getHookNameFromCallee(callee) {
|
|
177
|
+
const resolvedCallee = unwrapNestedExpressions(callee);
|
|
178
|
+
if (resolvedCallee.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
179
|
+
return resolvedCallee.name;
|
|
180
|
+
}
|
|
181
|
+
if (resolvedCallee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
182
|
+
!resolvedCallee.computed &&
|
|
183
|
+
resolvedCallee.property.type ===
|
|
184
|
+
utils_1.AST_NODE_TYPES.Identifier) {
|
|
185
|
+
const property = resolvedCallee.property;
|
|
186
|
+
return property.name;
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Detects hook call expressions and returns their hook name.
|
|
192
|
+
* @param node Call expression to inspect.
|
|
193
|
+
* @returns Hook name when the call targets a hook, otherwise null.
|
|
194
|
+
*/
|
|
195
|
+
function isHookCall(node) {
|
|
196
|
+
const hookName = getHookNameFromCallee(node.callee);
|
|
197
|
+
return hookName && isHookName(hookName) ? hookName : null;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Maps literal node types to memoization metadata used for reporting.
|
|
201
|
+
* @param node Candidate literal node.
|
|
202
|
+
* @returns Descriptor with literal type and memo hook, or null.
|
|
203
|
+
*/
|
|
204
|
+
function getLiteralDescriptor(node) {
|
|
205
|
+
const descriptor = LITERAL_DESCRIPTOR_BY_TYPE[node.type] ?? null;
|
|
206
|
+
return descriptor;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Finds the nearest ancestor function considered a React component or hook.
|
|
210
|
+
* @param node Starting node for the search.
|
|
211
|
+
* @returns Owning component/hook function or null.
|
|
212
|
+
*/
|
|
213
|
+
function findEnclosingComponentOrHook(node) {
|
|
214
|
+
let current = node.parent;
|
|
215
|
+
while (current) {
|
|
216
|
+
if (isFunctionNode(current) && isComponentOrHookFunction(current)) {
|
|
217
|
+
return current;
|
|
218
|
+
}
|
|
219
|
+
current = current.parent;
|
|
220
|
+
}
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Checks whether a node is inside an allowed hook callback that should be skipped.
|
|
225
|
+
* @param node Starting node for traversal.
|
|
226
|
+
* @returns True when enclosed by a callback to an allowed hook.
|
|
227
|
+
*/
|
|
228
|
+
function isInsideAllowedHookCallback(node) {
|
|
229
|
+
let current = node;
|
|
230
|
+
while (current) {
|
|
231
|
+
if (current.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
232
|
+
current.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
233
|
+
let parent = current.parent;
|
|
234
|
+
while (parent && isExpressionWrapper(parent)) {
|
|
235
|
+
parent = parent.parent;
|
|
236
|
+
}
|
|
237
|
+
if (parent?.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
238
|
+
const hookName = getHookNameFromCallee(parent.callee);
|
|
239
|
+
const matchesCallback = parent.arguments.some((arg) => unwrapNestedExpressions(arg) === current);
|
|
240
|
+
if (hookName && SAFE_HOOK_ARGUMENTS.has(hookName) && matchesCallback) {
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
current = current.parent;
|
|
246
|
+
}
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Type guard for nodes that wrap an underlying expression (e.g. assertions,
|
|
251
|
+
* parentheses, optional chaining wrappers).
|
|
252
|
+
*/
|
|
253
|
+
function isExpressionWrapper(node) {
|
|
254
|
+
return (node.type === utils_1.AST_NODE_TYPES.TSAsExpression ||
|
|
255
|
+
node.type === utils_1.AST_NODE_TYPES.TSTypeAssertion ||
|
|
256
|
+
node.type === utils_1.AST_NODE_TYPES.TSSatisfiesExpression ||
|
|
257
|
+
node.type === utils_1.AST_NODE_TYPES.TSNonNullExpression ||
|
|
258
|
+
node.type === utils_1.AST_NODE_TYPES.ChainExpression ||
|
|
259
|
+
isParenthesizedExpression(node));
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Unwraps TypeScript assertions, optional chaining, and parentheses to reach
|
|
263
|
+
* the underlying expression node for stable identity comparisons.
|
|
264
|
+
*/
|
|
265
|
+
function unwrapNestedExpressions(node) {
|
|
266
|
+
let current = node;
|
|
267
|
+
while (isExpressionWrapper(current)) {
|
|
268
|
+
current = current.expression;
|
|
269
|
+
}
|
|
270
|
+
return current;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Finds the nearest enclosing hook call for a literal and whether the literal
|
|
274
|
+
* is passed directly as an argument (versus nested inside another expression).
|
|
275
|
+
*/
|
|
276
|
+
function findEnclosingHookCall(node) {
|
|
277
|
+
const unwrappedTarget = unwrapNestedExpressions(node);
|
|
278
|
+
let current = node.parent;
|
|
279
|
+
while (current) {
|
|
280
|
+
if (current.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
281
|
+
const hookName = isHookCall(current);
|
|
282
|
+
if (hookName) {
|
|
283
|
+
const isDirectArgument = current.arguments.some((arg) => {
|
|
284
|
+
const unwrappedArg = unwrapNestedExpressions(arg);
|
|
285
|
+
return unwrappedArg === unwrappedTarget;
|
|
286
|
+
});
|
|
287
|
+
return { hookName, isDirectArgument };
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
current = current.parent;
|
|
291
|
+
}
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Finds the nearest owning function for the provided node.
|
|
296
|
+
* @param node Node to start from.
|
|
297
|
+
* @returns Enclosing function declaration/expression or null.
|
|
298
|
+
*/
|
|
299
|
+
function findOwningFunction(node) {
|
|
300
|
+
let current = node.parent;
|
|
301
|
+
while (current) {
|
|
302
|
+
if (isFunctionNode(current)) {
|
|
303
|
+
return current;
|
|
304
|
+
}
|
|
305
|
+
current = current.parent;
|
|
306
|
+
}
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Locates the closest return statement ancestor, skipping assertion wrappers.
|
|
311
|
+
* @param node Node to start from.
|
|
312
|
+
* @returns Enclosing return statement or null.
|
|
313
|
+
*/
|
|
314
|
+
function findEnclosingReturnStatement(node) {
|
|
315
|
+
let current = node.parent;
|
|
316
|
+
while (current) {
|
|
317
|
+
if (current.type === utils_1.AST_NODE_TYPES.ReturnStatement) {
|
|
318
|
+
return current;
|
|
319
|
+
}
|
|
320
|
+
if (isExpressionWrapper(current)) {
|
|
321
|
+
current = current.parent;
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Determines whether a node is returned from the provided hook function.
|
|
330
|
+
* @param node Expression candidate for return.
|
|
331
|
+
* @param owner Hook function candidate.
|
|
332
|
+
* @returns True when the expression is the hook's return value.
|
|
333
|
+
*/
|
|
334
|
+
function isReturnValueFromHook(node, owner) {
|
|
335
|
+
if (!isHookFunction(owner))
|
|
336
|
+
return false;
|
|
337
|
+
if (owner.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression &&
|
|
338
|
+
owner.body.type !== utils_1.AST_NODE_TYPES.BlockStatement &&
|
|
339
|
+
unwrapNestedExpressions(owner.body) === unwrapNestedExpressions(node)) {
|
|
340
|
+
return true;
|
|
341
|
+
}
|
|
342
|
+
if (node.parent?.type !== utils_1.AST_NODE_TYPES.ReturnStatement ||
|
|
343
|
+
node.parent.argument !== node) {
|
|
344
|
+
const returnStatement = findEnclosingReturnStatement(node);
|
|
345
|
+
if (!returnStatement?.argument) {
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
if (unwrapNestedExpressions(returnStatement.argument) !==
|
|
349
|
+
unwrapNestedExpressions(node)) {
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
const owningFunction = findOwningFunction(returnStatement);
|
|
353
|
+
return owningFunction === owner;
|
|
354
|
+
}
|
|
355
|
+
const owningFunction = findOwningFunction(node.parent);
|
|
356
|
+
return owningFunction === owner;
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Builds memoization suggestions with dependency placeholders for developers.
|
|
360
|
+
* @param node Literal node to wrap.
|
|
361
|
+
* @param descriptor Literal metadata including memo hook.
|
|
362
|
+
* @param sourceCode Source code utility for text extraction.
|
|
363
|
+
* @returns Suggestion array encouraging memoization with explicit deps TODO.
|
|
364
|
+
*/
|
|
365
|
+
function buildMemoSuggestions(node, descriptor, sourceCode) {
|
|
366
|
+
const initializerText = sourceCode.getText(node);
|
|
367
|
+
const wrappedInitializer = descriptor.literalType === 'object literal'
|
|
368
|
+
? `(${initializerText})`
|
|
369
|
+
: initializerText;
|
|
370
|
+
if (descriptor.literalType === 'inline function') {
|
|
371
|
+
return [
|
|
372
|
+
{
|
|
373
|
+
messageId: 'memoizeLiteralSuggestion',
|
|
374
|
+
data: {
|
|
375
|
+
literalType: descriptor.literalType,
|
|
376
|
+
memoHook: descriptor.memoHook,
|
|
377
|
+
},
|
|
378
|
+
fix(fixer) {
|
|
379
|
+
return fixer.replaceText(node, `${descriptor.memoHook}(${initializerText}, [${TODO_DEPS_COMMENT}])`);
|
|
380
|
+
},
|
|
381
|
+
},
|
|
382
|
+
];
|
|
383
|
+
}
|
|
384
|
+
return [
|
|
385
|
+
{
|
|
386
|
+
messageId: 'memoizeLiteralSuggestion',
|
|
387
|
+
data: {
|
|
388
|
+
literalType: descriptor.literalType,
|
|
389
|
+
memoHook: descriptor.memoHook,
|
|
390
|
+
},
|
|
391
|
+
fix(fixer) {
|
|
392
|
+
return fixer.replaceText(node, `${descriptor.memoHook}(() => ${wrappedInitializer}, [${TODO_DEPS_COMMENT}])`);
|
|
393
|
+
},
|
|
394
|
+
},
|
|
395
|
+
];
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Formats a readable label for diagnostics based on the owning function.
|
|
399
|
+
* @param fn Owning component or hook function.
|
|
400
|
+
* @returns Human-friendly label for error messages.
|
|
401
|
+
*/
|
|
402
|
+
function formatContextLabel(fn) {
|
|
403
|
+
const name = getFunctionName(fn);
|
|
404
|
+
if (!name)
|
|
405
|
+
return 'this component or hook';
|
|
406
|
+
if (isHookName(name))
|
|
407
|
+
return `hook "${name}"`;
|
|
408
|
+
return `component "${name}"`;
|
|
409
|
+
}
|
|
410
|
+
exports.reactMemoizeLiterals = (0, createRule_1.createRule)({
|
|
411
|
+
name: 'react-memoize-literals',
|
|
412
|
+
meta: {
|
|
413
|
+
type: 'suggestion',
|
|
414
|
+
docs: {
|
|
415
|
+
description: 'Detect object, array, and function literals created in React components or hooks that create new references every render. Prefer memoized values (useMemo/useCallback) or module-level constants to keep referential stability.',
|
|
416
|
+
recommended: 'error',
|
|
417
|
+
},
|
|
418
|
+
hasSuggestions: true,
|
|
419
|
+
schema: [],
|
|
420
|
+
messages: {
|
|
421
|
+
componentLiteral: 'New {{literalType}} inside {{context}} is created on every render → Breaks referential stability for hooks/props and can re-run effects or re-render children → Hoist it to a module-level constant or wrap it in {{memoHook}} with the right dependencies.',
|
|
422
|
+
nestedHookLiteral: 'Nested {{literalType}} inside {{hookName}} arguments is recreated on every render → Dependency/reference comparisons change each time and defeat memoization/caching → Extract it into a memoized value (useMemo/useCallback) or hoist it to a module constant before passing it to {{hookName}}.',
|
|
423
|
+
hookReturnLiteral: '{{hookName}} returns a {{literalType}} literal on each render → Callers receive a fresh reference and may re-render or re-run effects → Memoize the returned value with useMemo/useCallback or return pre-memoized pieces so callers see stable references.',
|
|
424
|
+
memoizeLiteralSuggestion: 'This {{literalType}} is created inline → It produces a new reference each render → Wrap it in {{memoHook}} and include every closed-over value in the dependency array.',
|
|
425
|
+
},
|
|
426
|
+
},
|
|
427
|
+
defaultOptions: [],
|
|
428
|
+
create(context) {
|
|
429
|
+
const sourceCode = context.getSourceCode();
|
|
430
|
+
function reportLiteral(node) {
|
|
431
|
+
const descriptor = getLiteralDescriptor(node);
|
|
432
|
+
if (!descriptor)
|
|
433
|
+
return;
|
|
434
|
+
if (isInsideAllowedHookCallback(node)) {
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
const owner = findEnclosingComponentOrHook(node);
|
|
438
|
+
if (!owner)
|
|
439
|
+
return;
|
|
440
|
+
const hookCall = findEnclosingHookCall(node);
|
|
441
|
+
if (hookCall) {
|
|
442
|
+
if (hookCall.isDirectArgument) {
|
|
443
|
+
// Top-level literal passed directly to a hook argument is allowed.
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
context.report({
|
|
447
|
+
node,
|
|
448
|
+
messageId: 'nestedHookLiteral',
|
|
449
|
+
data: {
|
|
450
|
+
literalType: descriptor.literalType,
|
|
451
|
+
hookName: hookCall.hookName,
|
|
452
|
+
},
|
|
453
|
+
});
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
if (isReturnValueFromHook(node, owner)) {
|
|
457
|
+
const hookName = getFunctionName(owner) ?? 'this hook';
|
|
458
|
+
context.report({
|
|
459
|
+
node,
|
|
460
|
+
messageId: 'hookReturnLiteral',
|
|
461
|
+
data: {
|
|
462
|
+
literalType: descriptor.literalType,
|
|
463
|
+
hookName,
|
|
464
|
+
},
|
|
465
|
+
});
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
const contextLabel = formatContextLabel(owner);
|
|
469
|
+
// Only emit auto-fix suggestions for simple variable initializers; other
|
|
470
|
+
// contexts (returns, JSX props, nested expressions) risk unsafe rewrites.
|
|
471
|
+
const suggestions = node.parent?.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
472
|
+
node.parent.init === node
|
|
473
|
+
? buildMemoSuggestions(node, descriptor, sourceCode)
|
|
474
|
+
: undefined;
|
|
475
|
+
context.report({
|
|
476
|
+
node,
|
|
477
|
+
messageId: 'componentLiteral',
|
|
478
|
+
data: {
|
|
479
|
+
literalType: descriptor.literalType,
|
|
480
|
+
context: contextLabel,
|
|
481
|
+
memoHook: descriptor.memoHook,
|
|
482
|
+
},
|
|
483
|
+
suggest: suggestions,
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
return {
|
|
487
|
+
ObjectExpression: reportLiteral,
|
|
488
|
+
ArrayExpression: reportLiteral,
|
|
489
|
+
ArrowFunctionExpression: reportLiteral,
|
|
490
|
+
FunctionExpression: reportLiteral,
|
|
491
|
+
FunctionDeclaration: reportLiteral,
|
|
492
|
+
};
|
|
493
|
+
},
|
|
494
|
+
});
|
|
495
|
+
//# sourceMappingURL=react-memoize-literals.js.map
|
|
@@ -47,7 +47,7 @@ const isUsedAsComponentProp = (context, variableName, node) => {
|
|
|
47
47
|
break;
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
|
-
const sourceCode = context.
|
|
50
|
+
const sourceCode = context.sourceCode;
|
|
51
51
|
const scopeManager = sourceCode.scopeManager;
|
|
52
52
|
const scope = scopeManager &&
|
|
53
53
|
((fn && scopeManager.acquire(fn)) || scopeManager.acquire(node));
|
|
@@ -75,7 +75,7 @@ const isUsedAsComponentProp = (context, variableName, node) => {
|
|
|
75
75
|
const isUsedAsJsxChild = (context, variableName, node) => {
|
|
76
76
|
// Traverse the nearest function (or program) to see if the identifier appears
|
|
77
77
|
// as a JSX child expression.
|
|
78
|
-
const sourceCode = context.
|
|
78
|
+
const sourceCode = context.sourceCode;
|
|
79
79
|
let container = sourceCode.ast;
|
|
80
80
|
let current = node;
|
|
81
81
|
while (current?.parent) {
|
|
@@ -14,7 +14,7 @@ exports.requireHooksDefaultParams = (0, createRule_1.createRule)({
|
|
|
14
14
|
fixable: 'code',
|
|
15
15
|
schema: [],
|
|
16
16
|
messages: {
|
|
17
|
-
requireDefaultParams: '
|
|
17
|
+
requireDefaultParams: 'Hook "{{hookName}}" accepts an options object where every property is optional, but the parameter is not defaulted. When callers omit the argument the hook receives undefined, so destructuring or property access throws even though the fields are optional. Default the parameter to an empty object (e.g., "({ option } = {})") so the hook stays safe to call with no arguments.',
|
|
18
18
|
},
|
|
19
19
|
},
|
|
20
20
|
defaultOptions: [],
|
|
@@ -81,8 +81,10 @@ exports.requireHooksDefaultParams = (0, createRule_1.createRule)({
|
|
|
81
81
|
'ArrowFunctionExpression, FunctionDeclaration'(node) {
|
|
82
82
|
// Check if it's a hook function
|
|
83
83
|
let isHook = false;
|
|
84
|
+
let hookName;
|
|
84
85
|
if (node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration) {
|
|
85
|
-
|
|
86
|
+
hookName = node.id?.name;
|
|
87
|
+
isHook = hookName ? isHookName(hookName) : false;
|
|
86
88
|
}
|
|
87
89
|
else {
|
|
88
90
|
const parent = node.parent;
|
|
@@ -90,12 +92,14 @@ exports.requireHooksDefaultParams = (0, createRule_1.createRule)({
|
|
|
90
92
|
parent.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
91
93
|
parent.id &&
|
|
92
94
|
parent.id.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
95
|
+
hookName = parent.id.name;
|
|
93
96
|
isHook = isHookName(parent.id.name);
|
|
94
97
|
}
|
|
95
98
|
}
|
|
96
99
|
if (!isHook) {
|
|
97
100
|
return;
|
|
98
101
|
}
|
|
102
|
+
const messageData = { hookName: hookName ?? 'this hook' };
|
|
99
103
|
// Check if it has exactly one parameter
|
|
100
104
|
if (node.params.length !== 1) {
|
|
101
105
|
return;
|
|
@@ -121,10 +125,9 @@ exports.requireHooksDefaultParams = (0, createRule_1.createRule)({
|
|
|
121
125
|
context.report({
|
|
122
126
|
node: param,
|
|
123
127
|
messageId: 'requireDefaultParams',
|
|
128
|
+
data: messageData,
|
|
124
129
|
fix(fixer) {
|
|
125
|
-
const paramText = context
|
|
126
|
-
.getSourceCode()
|
|
127
|
-
.getText(param);
|
|
130
|
+
const paramText = context.sourceCode.getText(param);
|
|
128
131
|
return fixer.replaceText(param, `${paramText} = {}`);
|
|
129
132
|
},
|
|
130
133
|
});
|
|
@@ -140,10 +143,9 @@ exports.requireHooksDefaultParams = (0, createRule_1.createRule)({
|
|
|
140
143
|
context.report({
|
|
141
144
|
node: param,
|
|
142
145
|
messageId: 'requireDefaultParams',
|
|
146
|
+
data: messageData,
|
|
143
147
|
fix(fixer) {
|
|
144
|
-
const paramText = context
|
|
145
|
-
.getSourceCode()
|
|
146
|
-
.getText(param);
|
|
148
|
+
const paramText = context.sourceCode.getText(param);
|
|
147
149
|
return fixer.replaceText(param, `${paramText} = {}`);
|
|
148
150
|
},
|
|
149
151
|
});
|
|
@@ -152,7 +154,7 @@ exports.requireHooksDefaultParams = (0, createRule_1.createRule)({
|
|
|
152
154
|
}
|
|
153
155
|
else {
|
|
154
156
|
// If we can't find the type definition, check if it's defined in the same file
|
|
155
|
-
const program = context.
|
|
157
|
+
const program = context.sourceCode.ast;
|
|
156
158
|
const typeDefinitions = program.body.filter((node) => {
|
|
157
159
|
if (node.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration ||
|
|
158
160
|
node.type === utils_1.AST_NODE_TYPES.TSInterfaceDeclaration) {
|
|
@@ -172,10 +174,9 @@ exports.requireHooksDefaultParams = (0, createRule_1.createRule)({
|
|
|
172
174
|
context.report({
|
|
173
175
|
node: param,
|
|
174
176
|
messageId: 'requireDefaultParams',
|
|
177
|
+
data: messageData,
|
|
175
178
|
fix(fixer) {
|
|
176
|
-
const paramText = context
|
|
177
|
-
.getSourceCode()
|
|
178
|
-
.getText(param);
|
|
179
|
+
const paramText = context.sourceCode.getText(param);
|
|
179
180
|
return fixer.replaceText(param, `${paramText} = {}`);
|
|
180
181
|
},
|
|
181
182
|
});
|
|
@@ -191,10 +192,9 @@ exports.requireHooksDefaultParams = (0, createRule_1.createRule)({
|
|
|
191
192
|
context.report({
|
|
192
193
|
node: param,
|
|
193
194
|
messageId: 'requireDefaultParams',
|
|
195
|
+
data: messageData,
|
|
194
196
|
fix(fixer) {
|
|
195
|
-
const paramText = context
|
|
196
|
-
.getSourceCode()
|
|
197
|
-
.getText(param);
|
|
197
|
+
const paramText = context.sourceCode.getText(param);
|
|
198
198
|
return fixer.replaceText(param, `${paramText} = {}`);
|
|
199
199
|
},
|
|
200
200
|
});
|
|
@@ -214,8 +214,9 @@ exports.requireHooksDefaultParams = (0, createRule_1.createRule)({
|
|
|
214
214
|
context.report({
|
|
215
215
|
node: param,
|
|
216
216
|
messageId: 'requireDefaultParams',
|
|
217
|
+
data: messageData,
|
|
217
218
|
fix(fixer) {
|
|
218
|
-
const paramText = context.
|
|
219
|
+
const paramText = context.sourceCode.getText(param);
|
|
219
220
|
return fixer.replaceText(param, `${paramText} = {}`);
|
|
220
221
|
},
|
|
221
222
|
});
|