@blumintinc/eslint-plugin-blumint 1.20.68 → 1.20.70
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/lib/index.js +1 -1
- package/lib/rules/class-methods-read-top-to-bottom.js +36 -19
- package/lib/rules/enforce-early-destructuring.js +20 -1
- package/lib/rules/enforce-render-hits-memoization.d.ts +2 -1
- package/lib/rules/enforce-render-hits-memoization.js +149 -93
- package/lib/rules/enforce-transform-memoization.js +27 -1
- package/lib/rules/no-useless-usememo-primitives.js +16 -0
- package/lib/rules/optimize-object-boolean-conditions.js +19 -1
- package/lib/rules/parallelize-async-operations.js +66 -3
- package/lib/rules/prefer-map-over-conditional-dispatch.js +270 -5
- package/lib/rules/prefer-type-over-interface.js +38 -0
- package/lib/rules/use-latest-callback.d.ts +6 -1
- package/lib/rules/use-latest-callback.js +235 -46
- package/package.json +1 -1
- package/release-manifest.json +94 -0
package/lib/index.js
CHANGED
|
@@ -80,25 +80,42 @@ exports.classMethodsReadTopToBottom = (0, createRule_1.createRule)({
|
|
|
80
80
|
if (actualMember !== expectedMember) {
|
|
81
81
|
const classNameReport = className || 'this class';
|
|
82
82
|
const sourceCode = context.getSourceCode();
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
83
|
+
const sourceText = sourceCode.getText();
|
|
84
|
+
// A member's block spans its leading comments through its own end,
|
|
85
|
+
// so documentation travels with the member it describes. Because
|
|
86
|
+
// every comment in the body is thereby absorbed into some block,
|
|
87
|
+
// the text between two adjacent blocks is pure whitespace.
|
|
88
|
+
const memberBlocks = node.body.map((member) => {
|
|
89
|
+
const comments = sourceCode.getCommentsBefore(member) || [];
|
|
90
|
+
const start = Math.min(member.range[0], ...comments.map((comment) => comment.range[0]));
|
|
91
|
+
return {
|
|
92
|
+
name: getMemberName(member),
|
|
93
|
+
text: sourceText.slice(start, member.range[1]),
|
|
94
|
+
start,
|
|
95
|
+
end: member.range[1],
|
|
96
|
+
};
|
|
97
|
+
});
|
|
98
|
+
// Reuse those whitespace runs positionally instead of joining with
|
|
99
|
+
// a bare '\n'. The blank lines between members are the part that
|
|
100
|
+
// matters: prettier preserves existing blank lines but never
|
|
101
|
+
// inserts new ones, so collapsing them is irreversible (#1592).
|
|
102
|
+
// Carrying the runs verbatim also reproduces the newline and
|
|
103
|
+
// indentation after `{` and the newline before `}` for free, since
|
|
104
|
+
// every member sits at the same depth.
|
|
105
|
+
const separators = memberBlocks
|
|
106
|
+
.slice(1)
|
|
107
|
+
.map((block, index) => sourceText.slice(memberBlocks[index].end, block.start));
|
|
108
|
+
const prefix = sourceText.slice(node.range[0] + 1, memberBlocks[0].start);
|
|
109
|
+
const suffix = sourceText.slice(memberBlocks[memberBlocks.length - 1].end, node.range[1] - 1);
|
|
110
|
+
const newClassBody = prefix +
|
|
111
|
+
sortedOrder
|
|
112
|
+
.map((n) => {
|
|
113
|
+
const block = memberBlocks.find(({ name }) => name === n);
|
|
114
|
+
return block ? block.text : '';
|
|
115
|
+
})
|
|
116
|
+
.map((text, index) => index === 0 ? text : separators[index - 1] + text)
|
|
117
|
+
.join('') +
|
|
118
|
+
suffix;
|
|
102
119
|
return context.report({
|
|
103
120
|
node,
|
|
104
121
|
messageId: 'classMethodsReadTopToBottom',
|
|
@@ -3,6 +3,25 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.enforceEarlyDestructuring = void 0;
|
|
4
4
|
const utils_1 = require("@typescript-eslint/utils");
|
|
5
5
|
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
/**
|
|
7
|
+
* Source forms that already bind tighter than `??`, so wrapping them in the
|
|
8
|
+
* hoisted `(obj) ?? {}` initializer adds parentheses a formatter then strips.
|
|
9
|
+
*
|
|
10
|
+
* The set is deliberately limited to what this rule can actually emit: only an
|
|
11
|
+
* identifier-rooted source is ever hoisted, so a call, conditional or logical
|
|
12
|
+
* source never reaches here. Anything absent keeps its parentheses, which is the
|
|
13
|
+
* safe direction — a stray pair is cosmetic, a missing pair changes what the
|
|
14
|
+
* initializer evaluates. `as` and `satisfies` are excluded on purpose: TypeScript
|
|
15
|
+
* rejects them beside `??` unparenthesized.
|
|
16
|
+
*/
|
|
17
|
+
const TIGHTER_THAN_NULLISH = new Set([
|
|
18
|
+
utils_1.AST_NODE_TYPES.Identifier,
|
|
19
|
+
utils_1.AST_NODE_TYPES.ThisExpression,
|
|
20
|
+
utils_1.AST_NODE_TYPES.MemberExpression,
|
|
21
|
+
utils_1.AST_NODE_TYPES.ChainExpression,
|
|
22
|
+
utils_1.AST_NODE_TYPES.TSNonNullExpression,
|
|
23
|
+
]);
|
|
24
|
+
const nullishSourceText = (objectText, init) => init && TIGHTER_THAN_NULLISH.has(init.type) ? objectText : `(${objectText})`;
|
|
6
25
|
const HOOK_NAMES = new Set([
|
|
7
26
|
'useEffect',
|
|
8
27
|
'useMemo',
|
|
@@ -875,7 +894,7 @@ function generateHoistingFixes(groups, callback, depsArray, depTexts, insertionS
|
|
|
875
894
|
for (const group of groups.values()) {
|
|
876
895
|
const sortedProps = Array.from(group.properties.values()).sort((a, b) => a.order - b.order);
|
|
877
896
|
const pattern = `{ ${sortedProps.map((p) => p.text).join(', ')} }`;
|
|
878
|
-
hoistedLines.push(`${indent}const ${pattern} =
|
|
897
|
+
hoistedLines.push(`${indent}const ${pattern} = ${nullishSourceText(group.objectText, group.inits[0])} ?? {};`);
|
|
879
898
|
}
|
|
880
899
|
reservedNamesByScope.set(scope, updatedReservedNames);
|
|
881
900
|
const newDepSet = new Set(newDepTexts);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { TSESLint } from '@typescript-eslint/utils';
|
|
1
2
|
type MessageIds = 'requireMemoizedTransformBefore' | 'requireMemoizedRender' | 'requireMemoizedRenderHits' | 'noDirectComponentInRender';
|
|
2
|
-
export declare const enforceRenderHitsMemoization:
|
|
3
|
+
export declare const enforceRenderHitsMemoization: TSESLint.RuleModule<MessageIds, [], TSESLint.RuleListener>;
|
|
3
4
|
export {};
|
|
@@ -3,6 +3,31 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.enforceRenderHitsMemoization = void 0;
|
|
4
4
|
const utils_1 = require("@typescript-eslint/utils");
|
|
5
5
|
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
const LATEST_CALLBACK_MODULE = 'use-latest-callback';
|
|
7
|
+
const LATEST_CALLBACK_HOOK = 'useLatestCallback';
|
|
8
|
+
/**
|
|
9
|
+
* Declaration forms whose binding is created once for the program's lifetime.
|
|
10
|
+
*
|
|
11
|
+
* `let` and `var` are deliberately excluded: a reassignable binding can hand
|
|
12
|
+
* `useRenderHits` a different function on a later render, which is precisely
|
|
13
|
+
* the instability this rule exists to catch.
|
|
14
|
+
*/
|
|
15
|
+
function isStableDeclaration(def) {
|
|
16
|
+
switch (def.type) {
|
|
17
|
+
case utils_1.TSESLint.Scope.DefinitionType.FunctionName:
|
|
18
|
+
return true;
|
|
19
|
+
case utils_1.TSESLint.Scope.DefinitionType.ImportBinding:
|
|
20
|
+
// A type-only import binds no value, so its local name names nothing
|
|
21
|
+
// callable — the same reason the memoization-callee set rejects one.
|
|
22
|
+
return (def.parent.importKind !== 'type' &&
|
|
23
|
+
(def.node.type !== utils_1.AST_NODE_TYPES.ImportSpecifier ||
|
|
24
|
+
def.node.importKind !== 'type'));
|
|
25
|
+
case utils_1.TSESLint.Scope.DefinitionType.Variable:
|
|
26
|
+
return def.parent.kind === 'const';
|
|
27
|
+
default:
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
6
31
|
exports.enforceRenderHitsMemoization = (0, createRule_1.createRule)({
|
|
7
32
|
name: 'enforce-render-hits-memoization',
|
|
8
33
|
meta: {
|
|
@@ -13,14 +38,36 @@ exports.enforceRenderHitsMemoization = (0, createRule_1.createRule)({
|
|
|
13
38
|
},
|
|
14
39
|
schema: [],
|
|
15
40
|
messages: {
|
|
16
|
-
requireMemoizedTransformBefore: 'transformBefore
|
|
17
|
-
requireMemoizedRender: 'render
|
|
18
|
-
requireMemoizedRenderHits: 'renderHits
|
|
41
|
+
requireMemoizedTransformBefore: 'transformBefore is recreated on every render, so useRenderHits sees a new transform identity each pass and re-derives (and re-renders) the whole hit list even when the hits did not change. Memoize it with useCallback, useMemo, or useLatestCallback so the reference stays stable across renders.',
|
|
42
|
+
requireMemoizedRender: 'render is recreated on every render, so useRenderHits sees a new render identity each pass and re-renders every hit even when the hits did not change. Memoize it with useCallback, useMemo, or useLatestCallback so the reference stays stable across renders.',
|
|
43
|
+
requireMemoizedRenderHits: 'renderHits builds a fresh element for every hit, so calling it outside a memoization boundary re-creates the entire list on each render. Wrap the call in useCallback, useMemo, or useLatestCallback so the elements are rebuilt only when the hits they came from change.',
|
|
19
44
|
noDirectComponentInRender: 'Do not pass React components directly to render prop, use a memoized arrow function instead',
|
|
20
45
|
},
|
|
21
46
|
},
|
|
22
47
|
defaultOptions: [],
|
|
23
48
|
create(context) {
|
|
49
|
+
// Every callee this rule accepts as a memoization boundary. `useCallback`
|
|
50
|
+
// and `useMemo` are seeded bare because the rule never resolves React's
|
|
51
|
+
// import either; local names bound from `use-latest-callback` are added as
|
|
52
|
+
// its imports are visited.
|
|
53
|
+
//
|
|
54
|
+
// `useLatestCallback` belongs in the same set rather than a separate one:
|
|
55
|
+
// nothing here inspects a dependency array, so the hook taking none (it
|
|
56
|
+
// keeps the latest callback behind a ref that is stable for the component's
|
|
57
|
+
// whole life) costs the rule no precision. It has to be here because
|
|
58
|
+
// `use-latest-callback` — 'error' in the same recommended config, and
|
|
59
|
+
// fixable — rewrites every `useCallback` into it, and ESLint re-lints until
|
|
60
|
+
// the output settles, so one `eslint --fix` run does both steps. Without
|
|
61
|
+
// this entry correctly memoized code goes in and a demand to memoize it
|
|
62
|
+
// comes out, and the demand names the very hook the sibling fixer just
|
|
63
|
+
// removed, so following it loops forever (issue #1585).
|
|
64
|
+
const memoizationCallees = new Set([
|
|
65
|
+
'useCallback',
|
|
66
|
+
'useMemo',
|
|
67
|
+
LATEST_CALLBACK_HOOK,
|
|
68
|
+
]);
|
|
69
|
+
const isMemoizationCallee = (node) => node.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
70
|
+
memoizationCallees.has(node.name);
|
|
24
71
|
const isReactComponent = (node) => {
|
|
25
72
|
if (node.type !== utils_1.AST_NODE_TYPES.Identifier)
|
|
26
73
|
return false;
|
|
@@ -29,17 +76,30 @@ exports.enforceRenderHitsMemoization = (0, createRule_1.createRule)({
|
|
|
29
76
|
const isMemoizedCall = (node) => {
|
|
30
77
|
if (node.type !== utils_1.AST_NODE_TYPES.CallExpression)
|
|
31
78
|
return false;
|
|
32
|
-
if (!node.callee
|
|
79
|
+
if (!node.callee)
|
|
33
80
|
return false;
|
|
34
|
-
return (node.callee
|
|
81
|
+
return isMemoizationCallee(node.callee);
|
|
82
|
+
};
|
|
83
|
+
const isWithinMemoizationCall = (node) => {
|
|
84
|
+
let current = node;
|
|
85
|
+
while (current?.parent) {
|
|
86
|
+
if (current.parent.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
87
|
+
isMemoizationCallee(current.parent.callee)) {
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
current = current.parent;
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
35
93
|
};
|
|
36
94
|
const isMemoizedVariable = (node) => {
|
|
37
95
|
if (node.type !== utils_1.AST_NODE_TYPES.Identifier)
|
|
38
96
|
return false;
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
97
|
+
// The whole scope chain has to be searched rather than the current
|
|
98
|
+
// scope's own variable list: a useRenderHits call sitting inside a nested
|
|
99
|
+
// block or a nested component reaches its memoized declaration through an
|
|
100
|
+
// enclosing scope, and reading one scope's `variables` would miss it and
|
|
101
|
+
// demand a useCallback around a value that already has one.
|
|
102
|
+
const variable = utils_1.ASTUtils.findVariable(context.getScope(), node);
|
|
43
103
|
if (!variable)
|
|
44
104
|
return false;
|
|
45
105
|
// Check if the variable is initialized with a memoized call
|
|
@@ -55,6 +115,45 @@ exports.enforceRenderHitsMemoization = (0, createRule_1.createRule)({
|
|
|
55
115
|
}
|
|
56
116
|
return false;
|
|
57
117
|
};
|
|
118
|
+
/**
|
|
119
|
+
* A prop pointing at a declaration that lives outside every component body.
|
|
120
|
+
*
|
|
121
|
+
* Module scope creates the binding once for the program's lifetime, so its
|
|
122
|
+
* identity is strictly more stable than anything a hook can hand back:
|
|
123
|
+
* demanding a `useCallback` wrapper around it asks for work that can only
|
|
124
|
+
* make the reference less stable, never more.
|
|
125
|
+
*
|
|
126
|
+
* Both `module` and `global` count. Under `sourceType: 'script'` — the
|
|
127
|
+
* parser default, and what a consumer's config may well leave in place — a
|
|
128
|
+
* top-level declaration binds to the *global* scope and no module scope
|
|
129
|
+
* exists at all (issue #1578), so keying on `module` alone would silently
|
|
130
|
+
* drop the carve-out for exactly the consumers who never opted into module
|
|
131
|
+
* parsing.
|
|
132
|
+
*
|
|
133
|
+
* The shape is not hypothetical:
|
|
134
|
+
* `no-empty-dependency-use-callbacks` — 'error' in the same recommended
|
|
135
|
+
* config, and fixable — hoists a dependency-free callback to module scope
|
|
136
|
+
* and drops the hook, so one `eslint --fix` run rewrites memoized code into
|
|
137
|
+
* exactly this form. Without the carve-out the config demands the very hook
|
|
138
|
+
* its own fixer just removed (issue #1586).
|
|
139
|
+
*/
|
|
140
|
+
const isStableOuterScopeBinding = (node) => {
|
|
141
|
+
if (node.type !== utils_1.AST_NODE_TYPES.Identifier)
|
|
142
|
+
return false;
|
|
143
|
+
// The scope chain has to be walked rather than a single scope's variable
|
|
144
|
+
// list read: the useRenderHits call sits inside the component, so a
|
|
145
|
+
// module-scope declaration is never among the current scope's own
|
|
146
|
+
// variables.
|
|
147
|
+
const variable = utils_1.ASTUtils.findVariable(context.getScope(), node);
|
|
148
|
+
if (!variable)
|
|
149
|
+
return false;
|
|
150
|
+
const scopeType = variable.scope.type;
|
|
151
|
+
if (scopeType !== utils_1.TSESLint.Scope.ScopeType.module &&
|
|
152
|
+
scopeType !== utils_1.TSESLint.Scope.ScopeType.global) {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
return variable.defs.some(isStableDeclaration);
|
|
156
|
+
};
|
|
58
157
|
const isInsideMemoizedCall = (node) => {
|
|
59
158
|
// Handle the case when node is already a memoized call
|
|
60
159
|
if (isMemoizedCall(node))
|
|
@@ -62,18 +161,13 @@ exports.enforceRenderHitsMemoization = (0, createRule_1.createRule)({
|
|
|
62
161
|
// Check if the node is a reference to a memoized variable
|
|
63
162
|
if (isMemoizedVariable(node))
|
|
64
163
|
return true;
|
|
164
|
+
// A declaration outside every component body needs no memoization: it is
|
|
165
|
+
// already as stable as a reference can be.
|
|
166
|
+
if (isStableOuterScopeBinding(node))
|
|
167
|
+
return true;
|
|
65
168
|
// Check if the node is inside a memoization hook call
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (current.parent.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
69
|
-
const callee = current.parent.callee;
|
|
70
|
-
if (callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
71
|
-
(callee.name === 'useCallback' || callee.name === 'useMemo')) {
|
|
72
|
-
return true;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
current = current.parent;
|
|
76
|
-
}
|
|
169
|
+
if (isWithinMemoizationCall(node))
|
|
170
|
+
return true;
|
|
77
171
|
// Check if the node is a reference to a memoized value
|
|
78
172
|
const scope = context.getScope();
|
|
79
173
|
// Make sure node is an Identifier before accessing name property
|
|
@@ -88,44 +182,22 @@ exports.enforceRenderHitsMemoization = (0, createRule_1.createRule)({
|
|
|
88
182
|
for (const def of variable.defs) {
|
|
89
183
|
const parent = def.node.parent;
|
|
90
184
|
if (parent?.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
91
|
-
parent.init?.type === utils_1.AST_NODE_TYPES.CallExpression
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
(callee.name === 'useCallback' || callee.name === 'useMemo')) {
|
|
95
|
-
return true;
|
|
96
|
-
}
|
|
185
|
+
parent.init?.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
186
|
+
isMemoizationCallee(parent.init.callee)) {
|
|
187
|
+
return true;
|
|
97
188
|
}
|
|
98
189
|
}
|
|
99
190
|
// Check if any reference is inside a memoized call
|
|
100
191
|
for (const ref of variable.references) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (current.parent.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
104
|
-
const callee = current.parent.callee;
|
|
105
|
-
if (callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
106
|
-
(callee.name === 'useCallback' || callee.name === 'useMemo')) {
|
|
107
|
-
return true;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
current = current.parent;
|
|
111
|
-
}
|
|
192
|
+
if (isWithinMemoizationCall(ref.identifier))
|
|
193
|
+
return true;
|
|
112
194
|
}
|
|
113
195
|
// Check if the node is a property of an object that is memoized
|
|
114
196
|
const parent = node.parent;
|
|
115
197
|
if (parent?.type === utils_1.AST_NODE_TYPES.Property &&
|
|
116
198
|
parent.parent?.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
while (current?.parent) {
|
|
120
|
-
if (current.parent.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
121
|
-
const callee = current.parent.callee;
|
|
122
|
-
if (callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
123
|
-
(callee.name === 'useCallback' || callee.name === 'useMemo')) {
|
|
124
|
-
return true;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
current = current.parent;
|
|
128
|
-
}
|
|
199
|
+
if (isWithinMemoizationCall(parent.parent))
|
|
200
|
+
return true;
|
|
129
201
|
}
|
|
130
202
|
return false;
|
|
131
203
|
};
|
|
@@ -133,6 +205,23 @@ exports.enforceRenderHitsMemoization = (0, createRule_1.createRule)({
|
|
|
133
205
|
let renderHitsName = 'renderHits';
|
|
134
206
|
return {
|
|
135
207
|
ImportDeclaration(node) {
|
|
208
|
+
// The module's sole export is the hook, so its DEFAULT specifier binds
|
|
209
|
+
// it under whatever local name the file chose — a shape a set of bare
|
|
210
|
+
// hook names cannot see. `use-latest-callback`'s own fixer picks that
|
|
211
|
+
// name, falling back to `useLatestCallback2` when `useLatestCallback`
|
|
212
|
+
// is already taken in the file, so the alias is not hypothetical.
|
|
213
|
+
if (node.source.value === LATEST_CALLBACK_MODULE &&
|
|
214
|
+
(!node.importKind || node.importKind === 'value')) {
|
|
215
|
+
for (const specifier of node.specifiers) {
|
|
216
|
+
if (specifier.type === utils_1.AST_NODE_TYPES.ImportDefaultSpecifier ||
|
|
217
|
+
(specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
|
|
218
|
+
specifier.importKind !== 'type' &&
|
|
219
|
+
specifier.imported.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
220
|
+
specifier.imported.name === LATEST_CALLBACK_HOOK)) {
|
|
221
|
+
memoizationCallees.add(specifier.local.name);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
136
225
|
if (node.source.value.endsWith('useRenderHits')) {
|
|
137
226
|
for (const specifier of node.specifiers) {
|
|
138
227
|
if (specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
|
|
@@ -160,43 +249,19 @@ exports.enforceRenderHitsMemoization = (0, createRule_1.createRule)({
|
|
|
160
249
|
const options = node.arguments[0];
|
|
161
250
|
if (options.type !== utils_1.AST_NODE_TYPES.ObjectExpression)
|
|
162
251
|
return;
|
|
163
|
-
//
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
//
|
|
169
|
-
for (
|
|
170
|
-
if (prop.type !== utils_1.AST_NODE_TYPES.Property)
|
|
171
|
-
continue;
|
|
172
|
-
if (prop.key.type !== utils_1.AST_NODE_TYPES.Identifier)
|
|
173
|
-
continue;
|
|
174
|
-
// If it's shorthand property syntax like { transformBefore } and already a memoized variable
|
|
175
|
-
if (prop.key.name === 'transformBefore' &&
|
|
176
|
-
prop.shorthand &&
|
|
177
|
-
prop.key.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
178
|
-
checkProps.transformBefore = !isMemoizedVariable(prop.key);
|
|
179
|
-
}
|
|
180
|
-
else if (prop.key.name === 'render' &&
|
|
181
|
-
prop.shorthand &&
|
|
182
|
-
prop.key.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
183
|
-
checkProps.render = !isMemoizedVariable(prop.key);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
// Second pass: Check non-shorthand properties
|
|
252
|
+
// Shorthand props are checked exactly like written-out ones. `{ render }`
|
|
253
|
+
// and `render: render` describe the same value, and the config's own
|
|
254
|
+
// fixable `object-shorthand: ['error', 'always']` rewrites the second
|
|
255
|
+
// into the first, so exempting the shorthand form would let a single
|
|
256
|
+
// `eslint --fix` erase every report this rule makes about a prop whose
|
|
257
|
+
// variable happens to share the API's name — the shape idiomatic code
|
|
258
|
+
// reaches for first (issue #1588).
|
|
187
259
|
for (const prop of options.properties) {
|
|
188
260
|
if (prop.type !== utils_1.AST_NODE_TYPES.Property)
|
|
189
261
|
continue;
|
|
190
262
|
if (prop.key.type !== utils_1.AST_NODE_TYPES.Identifier)
|
|
191
263
|
continue;
|
|
192
|
-
|
|
193
|
-
if (prop.shorthand)
|
|
194
|
-
continue;
|
|
195
|
-
if (prop.key.name === 'transformBefore' &&
|
|
196
|
-
checkProps.transformBefore) {
|
|
197
|
-
// Skip if the value is already a memoized call
|
|
198
|
-
if (isMemoizedCall(prop.value))
|
|
199
|
-
continue;
|
|
264
|
+
if (prop.key.name === 'transformBefore') {
|
|
200
265
|
if (!isInsideMemoizedCall(prop.value)) {
|
|
201
266
|
context.report({
|
|
202
267
|
node: prop.value,
|
|
@@ -204,7 +269,7 @@ exports.enforceRenderHitsMemoization = (0, createRule_1.createRule)({
|
|
|
204
269
|
});
|
|
205
270
|
}
|
|
206
271
|
}
|
|
207
|
-
else if (prop.key.name === 'render'
|
|
272
|
+
else if (prop.key.name === 'render') {
|
|
208
273
|
if (isReactComponent(prop.value)) {
|
|
209
274
|
context.report({
|
|
210
275
|
node: prop.value,
|
|
@@ -222,17 +287,8 @@ exports.enforceRenderHitsMemoization = (0, createRule_1.createRule)({
|
|
|
222
287
|
}
|
|
223
288
|
if (node.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
224
289
|
node.callee.name === renderHitsName) {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
if (current.parent.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
228
|
-
const callee = current.parent.callee;
|
|
229
|
-
if (callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
230
|
-
(callee.name === 'useCallback' || callee.name === 'useMemo')) {
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
current = current.parent;
|
|
235
|
-
}
|
|
290
|
+
if (isWithinMemoizationCall(node))
|
|
291
|
+
return;
|
|
236
292
|
context.report({
|
|
237
293
|
node,
|
|
238
294
|
messageId: 'requireMemoizedRenderHits',
|
|
@@ -4,6 +4,8 @@ exports.enforceTransformMemoization = void 0;
|
|
|
4
4
|
const utils_1 = require("@typescript-eslint/utils");
|
|
5
5
|
const createRule_1 = require("../utils/createRule");
|
|
6
6
|
const ASTHelpers_1 = require("../utils/ASTHelpers");
|
|
7
|
+
const LATEST_CALLBACK_MODULE = 'use-latest-callback';
|
|
8
|
+
const LATEST_CALLBACK_HOOK = 'useLatestCallback';
|
|
7
9
|
exports.enforceTransformMemoization = (0, createRule_1.createRule)({
|
|
8
10
|
name: 'enforce-transform-memoization',
|
|
9
11
|
meta: {
|
|
@@ -26,7 +28,14 @@ exports.enforceTransformMemoization = (0, createRule_1.createRule)({
|
|
|
26
28
|
const scopeManager = sourceCode.scopeManager;
|
|
27
29
|
const adaptValueNames = new Set(['adaptValue']);
|
|
28
30
|
const memoizingHooks = new Set(['useMemo', 'useCallback']);
|
|
29
|
-
|
|
31
|
+
// Hooks that hand back a reference stable for the component's whole life and
|
|
32
|
+
// take no dependency array, so there is none to audit. `useLatestCallback`
|
|
33
|
+
// belongs here because `use-latest-callback` — 'error' in the same
|
|
34
|
+
// recommended config, and fixable — rewrites every `useCallback` into it and
|
|
35
|
+
// drops the array. ESLint re-lints until the output settles, so one
|
|
36
|
+
// `eslint --fix` run does both steps: without this entry, correctly
|
|
37
|
+
// memoized code goes in and a demand to memoize it comes out (issue #1584).
|
|
38
|
+
const stabilizingUtilities = new Set(['useEvent', LATEST_CALLBACK_HOOK]);
|
|
30
39
|
const getPropertyName = (key) => {
|
|
31
40
|
if (key.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
32
41
|
return key.name;
|
|
@@ -378,6 +387,23 @@ exports.enforceTransformMemoization = (0, createRule_1.createRule)({
|
|
|
378
387
|
return {
|
|
379
388
|
ImportDeclaration(node) {
|
|
380
389
|
const sourceValue = typeof node.source.value === 'string' ? node.source.value : '';
|
|
390
|
+
// The module's sole export is the hook, so its DEFAULT specifier binds
|
|
391
|
+
// it under whatever local name the file chose — a shape a set of bare
|
|
392
|
+
// hook names cannot see. `use-latest-callback`'s own fixer picks that
|
|
393
|
+
// name, and falls back to `useLatestCallback2` when `useLatestCallback`
|
|
394
|
+
// is already taken in the file, so the alias is not hypothetical.
|
|
395
|
+
if (sourceValue === LATEST_CALLBACK_MODULE &&
|
|
396
|
+
(!node.importKind || node.importKind === 'value')) {
|
|
397
|
+
for (const specifier of node.specifiers) {
|
|
398
|
+
if (specifier.type === utils_1.AST_NODE_TYPES.ImportDefaultSpecifier ||
|
|
399
|
+
(specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
|
|
400
|
+
specifier.importKind !== 'type' &&
|
|
401
|
+
specifier.imported.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
402
|
+
specifier.imported.name === LATEST_CALLBACK_HOOK)) {
|
|
403
|
+
stabilizingUtilities.add(specifier.local.name);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
381
407
|
for (const specifier of node.specifiers) {
|
|
382
408
|
if (specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
|
|
383
409
|
specifier.imported.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
@@ -381,6 +381,22 @@ exports.noUselessUsememoPrimitives = (0, createRule_1.createRule)({
|
|
|
381
381
|
valueKind,
|
|
382
382
|
},
|
|
383
383
|
fix(fixer) {
|
|
384
|
+
// Inlining replaces the entire useMemo(...) call with the returned
|
|
385
|
+
// expression's text, so any comment inside the call but outside
|
|
386
|
+
// that expression — an eslint-disable-next-line directive on the
|
|
387
|
+
// return statement among them — has no representation in the
|
|
388
|
+
// replacement and would be silently destroyed, changing which
|
|
389
|
+
// rules report on the file (#1591). The inlined expression lands
|
|
390
|
+
// mid-line (e.g. `const label = <expr>;`), where a -next-line
|
|
391
|
+
// directive cannot be hosted, so the autofix declines and leaves
|
|
392
|
+
// the report for a manual fix.
|
|
393
|
+
const strandedComments = sourceCode
|
|
394
|
+
.getCommentsInside(node)
|
|
395
|
+
.filter((comment) => comment.range[0] < returnedExpression.range[0] ||
|
|
396
|
+
comment.range[1] > returnedExpression.range[1]);
|
|
397
|
+
if (strandedComments.length > 0) {
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
384
400
|
const replacement = `(${sourceCode.getText(returnedExpression)})`;
|
|
385
401
|
return fixer.replaceText(node, replacement);
|
|
386
402
|
},
|
|
@@ -101,6 +101,12 @@ function isPrimitiveTypeNode(node) {
|
|
|
101
101
|
* operands: comparisons and arithmetic yield numbers/strings/booleans, and `!`,
|
|
102
102
|
* `typeof` and friends yield booleans/strings/numbers.
|
|
103
103
|
*/
|
|
104
|
+
/** `as const` — a type reference whose name is the `const` contextual keyword. */
|
|
105
|
+
function isConstAssertion(typeNode) {
|
|
106
|
+
return (typeNode.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
|
|
107
|
+
typeNode.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
108
|
+
typeNode.typeName.name === 'const');
|
|
109
|
+
}
|
|
104
110
|
function isPrimitiveExpression(node) {
|
|
105
111
|
if (!node) {
|
|
106
112
|
return false;
|
|
@@ -120,7 +126,19 @@ function isPrimitiveExpression(node) {
|
|
|
120
126
|
case utils_1.AST_NODE_TYPES.BinaryExpression:
|
|
121
127
|
return true;
|
|
122
128
|
case utils_1.AST_NODE_TYPES.TSAsExpression:
|
|
123
|
-
|
|
129
|
+
// `as const` narrows rather than retypes, so the asserted expression
|
|
130
|
+
// decides. Recursing keeps `{ a: 1 } as const` an object while accepting
|
|
131
|
+
// `0 as const` — the form global-const-style and
|
|
132
|
+
// enforce-object-literal-as-const rewrite bare constants into, so without
|
|
133
|
+
// this the plugin's own fixers manufacture the report (#1581).
|
|
134
|
+
return isConstAssertion(node.typeAnnotation)
|
|
135
|
+
? isPrimitiveExpression(node.expression)
|
|
136
|
+
: isPrimitiveTypeNode(node.typeAnnotation);
|
|
137
|
+
// `satisfies` never changes the value, only checks it.
|
|
138
|
+
case utils_1.AST_NODE_TYPES.TSSatisfiesExpression:
|
|
139
|
+
return isPrimitiveExpression(node.expression);
|
|
140
|
+
case utils_1.AST_NODE_TYPES.TSNonNullExpression:
|
|
141
|
+
return isPrimitiveExpression(node.expression);
|
|
124
142
|
default:
|
|
125
143
|
return false;
|
|
126
144
|
}
|