Skip to content

Commit 7f5ea1b

Browse files
authored
[compiler] More useMemo validation (#34868)
Two additional validations for useMemo: * Disallow reassigning to values declared outside the useMemo callback (always on) * Disallow unused useMemo calls (part of the validateNoVoidUseMemo feature flag, which in turn is off by default) We should probably enable this flag though! --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34868). * #34855 * #34882 * __->__ #34868
1 parent 0e32da7 commit 7f5ea1b

File tree

8 files changed

+182
-11
lines changed

8 files changed

+182
-11
lines changed

compiler/packages/babel-plugin-react-compiler/src/Inference/DropManualMemoization.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -458,9 +458,9 @@ export function dropManualMemoization(
458458
reason: 'useMemo() callbacks must return a value',
459459
description: `This ${
460460
manualMemo.loadInstr.value.kind === 'PropertyLoad'
461-
? 'React.useMemo'
462-
: 'useMemo'
463-
} callback doesn't return a value. useMemo is for computing and caching values, not for arbitrary side effects`,
461+
? 'React.useMemo()'
462+
: 'useMemo()'
463+
} callback doesn't return a value. useMemo() is for computing and caching values, not for arbitrary side effects`,
464464
suggestions: null,
465465
}).withDetails({
466466
kind: 'error',

compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateUseMemo.ts

Lines changed: 86 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,36 @@ import {
1010
CompilerError,
1111
ErrorCategory,
1212
} from '../CompilerError';
13-
import {FunctionExpression, HIRFunction, IdentifierId} from '../HIR';
13+
import {
14+
FunctionExpression,
15+
HIRFunction,
16+
IdentifierId,
17+
SourceLocation,
18+
} from '../HIR';
19+
import {
20+
eachInstructionValueOperand,
21+
eachTerminalOperand,
22+
} from '../HIR/visitors';
1423
import {Result} from '../Utils/Result';
1524

1625
export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
1726
const errors = new CompilerError();
1827
const useMemos = new Set<IdentifierId>();
1928
const react = new Set<IdentifierId>();
2029
const functions = new Map<IdentifierId, FunctionExpression>();
30+
const unusedUseMemos = new Map<IdentifierId, SourceLocation>();
2131
for (const [, block] of fn.body.blocks) {
2232
for (const {lvalue, value} of block.instructions) {
33+
if (unusedUseMemos.size !== 0) {
34+
/**
35+
* Most of the time useMemo results are referenced immediately. Don't bother
36+
* scanning instruction operands for useMemos unless there is an as-yet-unused
37+
* useMemo.
38+
*/
39+
for (const operand of eachInstructionValueOperand(value)) {
40+
unusedUseMemos.delete(operand.identifier.id);
41+
}
42+
}
2343
switch (value.kind) {
2444
case 'LoadGlobal': {
2545
if (value.binding.name === 'useMemo') {
@@ -45,10 +65,8 @@ export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
4565
case 'CallExpression': {
4666
// Is the function being called useMemo, with at least 1 argument?
4767
const callee =
48-
value.kind === 'CallExpression'
49-
? value.callee.identifier.id
50-
: value.property.identifier.id;
51-
const isUseMemo = useMemos.has(callee);
68+
value.kind === 'CallExpression' ? value.callee : value.property;
69+
const isUseMemo = useMemos.has(callee.identifier.id);
5270
if (!isUseMemo || value.args.length === 0) {
5371
continue;
5472
}
@@ -104,10 +122,73 @@ export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
104122
);
105123
}
106124

125+
validateNoContextVariableAssignment(body.loweredFunc.func, errors);
126+
127+
if (fn.env.config.validateNoVoidUseMemo) {
128+
unusedUseMemos.set(lvalue.identifier.id, callee.loc);
129+
}
107130
break;
108131
}
109132
}
110133
}
134+
if (unusedUseMemos.size !== 0) {
135+
for (const operand of eachTerminalOperand(block.terminal)) {
136+
unusedUseMemos.delete(operand.identifier.id);
137+
}
138+
}
139+
}
140+
if (unusedUseMemos.size !== 0) {
141+
/**
142+
* Basic check for unused memos, where the result of the call is never referenced. This runs
143+
* before DCE so it's more of an AST-level check that something, _anything_, cares about the value.
144+
*
145+
* This is easy to defeat with e.g. `const _ = useMemo(...)` but it at least gives us something to teach.
146+
* Even a DCE-based version could be bypassed with `noop(useMemo(...))`.
147+
*/
148+
for (const loc of unusedUseMemos.values()) {
149+
errors.pushDiagnostic(
150+
CompilerDiagnostic.create({
151+
category: ErrorCategory.VoidUseMemo,
152+
reason: 'Unused useMemo()',
153+
description: `This useMemo() value is unused. useMemo() is for computing and caching values, not for arbitrary side effects`,
154+
suggestions: null,
155+
}).withDetails({
156+
kind: 'error',
157+
loc,
158+
message: 'useMemo() result is unused',
159+
}),
160+
);
161+
}
111162
}
112163
return errors.asResult();
113164
}
165+
166+
function validateNoContextVariableAssignment(
167+
fn: HIRFunction,
168+
errors: CompilerError,
169+
): void {
170+
for (const block of fn.body.blocks.values()) {
171+
for (const instr of block.instructions) {
172+
const value = instr.value;
173+
switch (value.kind) {
174+
case 'StoreContext': {
175+
errors.pushDiagnostic(
176+
CompilerDiagnostic.create({
177+
category: ErrorCategory.UseMemo,
178+
reason:
179+
'useMemo() callbacks may not reassign variables declared outside of the callback',
180+
description:
181+
'useMemo() callbacks must be pure functions and cannot reassign variables defined outside of the callback function',
182+
suggestions: null,
183+
}).withDetails({
184+
kind: 'error',
185+
loc: value.lvalue.place.loc,
186+
message: 'Cannot reassign variable',
187+
}),
188+
);
189+
break;
190+
}
191+
}
192+
}
193+
}
194+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
2+
## Input
3+
4+
```javascript
5+
function Component() {
6+
let x;
7+
const y = useMemo(() => {
8+
let z;
9+
x = [];
10+
z = true;
11+
return z;
12+
}, []);
13+
return [x, y];
14+
}
15+
16+
```
17+
18+
19+
## Error
20+
21+
```
22+
Found 1 error:
23+
24+
Error: useMemo() callbacks may not reassign variables declared outside of the callback
25+
26+
useMemo() callbacks must be pure functions and cannot reassign variables defined outside of the callback function.
27+
28+
error.invalid-reassign-variable-in-usememo.ts:5:4
29+
3 | const y = useMemo(() => {
30+
4 | let z;
31+
> 5 | x = [];
32+
| ^ Cannot reassign variable
33+
6 | z = true;
34+
7 | return z;
35+
8 | }, []);
36+
```
37+
38+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
function Component() {
2+
let x;
3+
const y = useMemo(() => {
4+
let z;
5+
x = [];
6+
z = true;
7+
return z;
8+
}, []);
9+
return [x, y];
10+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
2+
## Input
3+
4+
```javascript
5+
// @validateNoVoidUseMemo
6+
function Component() {
7+
useMemo(() => {
8+
return [];
9+
}, []);
10+
return <div />;
11+
}
12+
13+
```
14+
15+
16+
## Error
17+
18+
```
19+
Found 1 error:
20+
21+
Error: Unused useMemo()
22+
23+
This useMemo() value is unused. useMemo() is for computing and caching values, not for arbitrary side effects.
24+
25+
error.invalid-unused-usememo.ts:3:2
26+
1 | // @validateNoVoidUseMemo
27+
2 | function Component() {
28+
> 3 | useMemo(() => {
29+
| ^^^^^^^ useMemo() result is unused
30+
4 | return [];
31+
5 | }, []);
32+
6 | return <div />;
33+
```
34+
35+
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// @validateNoVoidUseMemo
2+
function Component() {
3+
useMemo(() => {
4+
return [];
5+
}, []);
6+
return <div />;
7+
}

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.useMemo-no-return-value.expect.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ Found 2 errors:
2828
2929
Error: useMemo() callbacks must return a value
3030
31-
This useMemo callback doesn't return a value. useMemo is for computing and caching values, not for arbitrary side effects.
31+
This useMemo() callback doesn't return a value. useMemo() is for computing and caching values, not for arbitrary side effects.
3232
3333
error.useMemo-no-return-value.ts:3:16
3434
1 | // @validateNoVoidUseMemo
@@ -45,7 +45,7 @@ error.useMemo-no-return-value.ts:3:16
4545
4646
Error: useMemo() callbacks must return a value
4747
48-
This React.useMemo callback doesn't return a value. useMemo is for computing and caching values, not for arbitrary side effects.
48+
This React.useMemo() callback doesn't return a value. useMemo() is for computing and caching values, not for arbitrary side effects.
4949
5050
error.useMemo-no-return-value.ts:6:17
5151
4 | console.log('computing');

compiler/packages/eslint-plugin-react-compiler/__tests__/PluginTest-test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ testRule('plugin-recommended', TestRecommendedRules, {
120120
121121
return <Child x={state} />;
122122
}`,
123-
errors: [makeTestCaseError('useMemo() callbacks must return a value')],
123+
errors: [makeTestCaseError('Unused useMemo()')],
124124
},
125125
{
126126
name: 'Pipeline errors are reported',

0 commit comments

Comments
 (0)