Skip to content

Commit 1324e1b

Browse files
authored
[compiler] Cleanup and enable validateNoVoidUseMemo (facebook#34882)
This is a great validation, so let's enable by default. Changes: * Move the validation logic into ValidateUseMemo alongside the new check that the useMemo result is used * Update the lint description * Make the void memo errors lint-only, they don't require us to skip compilation (as evidenced by the fact that we've had this validation off) --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34882). * facebook#34855 * __->__ facebook#34882
1 parent 7f5ea1b commit 1324e1b

18 files changed

+195
-179
lines changed

compiler/packages/babel-plugin-react-compiler/src/CompilerError.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -988,7 +988,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
988988
severity: ErrorSeverity.Error,
989989
name: 'void-use-memo',
990990
description:
991-
'Validates that useMemos always return a value. See [`useMemo()` docs](https://react.dev/reference/react/useMemo) for more information.',
991+
'Validates that useMemos always return a value and that the result of the useMemo is used by the component/hook. See [`useMemo()` docs](https://react.dev/reference/react/useMemo) for more information.',
992992
preset: LintRulePreset.RecommendedLatest,
993993
};
994994
}

compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -659,7 +659,7 @@ export const EnvironmentConfigSchema = z.object({
659659
* Invalid:
660660
* useMemo(() => { ... }, [...]);
661661
*/
662-
validateNoVoidUseMemo: z.boolean().default(false),
662+
validateNoVoidUseMemo: z.boolean().default(true),
663663

664664
/**
665665
* Validates that Components/Hooks are always defined at module level. This prevents scope

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

Lines changed: 0 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -438,40 +438,6 @@ export function dropManualMemoization(
438438
continue;
439439
}
440440

441-
/**
442-
* Bailout on void return useMemos. This is an anti-pattern where code might be using
443-
* useMemo like useEffect: running arbirtary side-effects synced to changes in specific
444-
* values.
445-
*/
446-
if (
447-
func.env.config.validateNoVoidUseMemo &&
448-
manualMemo.kind === 'useMemo'
449-
) {
450-
const funcToCheck = sidemap.functions.get(
451-
fnPlace.identifier.id,
452-
)?.value;
453-
if (funcToCheck !== undefined && funcToCheck.loweredFunc.func) {
454-
if (!hasNonVoidReturn(funcToCheck.loweredFunc.func)) {
455-
errors.pushDiagnostic(
456-
CompilerDiagnostic.create({
457-
category: ErrorCategory.VoidUseMemo,
458-
reason: 'useMemo() callbacks must return a value',
459-
description: `This ${
460-
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`,
464-
suggestions: null,
465-
}).withDetails({
466-
kind: 'error',
467-
loc: instr.value.loc,
468-
message: 'useMemo() callbacks must return a value',
469-
}),
470-
);
471-
}
472-
}
473-
}
474-
475441
instr.value = getManualMemoizationReplacement(
476442
fnPlace,
477443
instr.value.loc,
@@ -629,17 +595,3 @@ function findOptionalPlaces(fn: HIRFunction): Set<IdentifierId> {
629595
}
630596
return optionals;
631597
}
632-
633-
function hasNonVoidReturn(func: HIRFunction): boolean {
634-
for (const [, block] of func.body.blocks) {
635-
if (block.terminal.kind === 'return') {
636-
if (
637-
block.terminal.returnVariant === 'Explicit' ||
638-
block.terminal.returnVariant === 'Implicit'
639-
) {
640-
return true;
641-
}
642-
}
643-
}
644-
return false;
645-
}

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

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {Result} from '../Utils/Result';
2424

2525
export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
2626
const errors = new CompilerError();
27+
const voidMemoErrors = new CompilerError();
2728
const useMemos = new Set<IdentifierId>();
2829
const react = new Set<IdentifierId>();
2930
const functions = new Map<IdentifierId, FunctionExpression>();
@@ -125,7 +126,22 @@ export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
125126
validateNoContextVariableAssignment(body.loweredFunc.func, errors);
126127

127128
if (fn.env.config.validateNoVoidUseMemo) {
128-
unusedUseMemos.set(lvalue.identifier.id, callee.loc);
129+
if (!hasNonVoidReturn(body.loweredFunc.func)) {
130+
voidMemoErrors.pushDiagnostic(
131+
CompilerDiagnostic.create({
132+
category: ErrorCategory.VoidUseMemo,
133+
reason: 'useMemo() callbacks must return a value',
134+
description: `This useMemo() callback doesn't return a value. useMemo() is for computing and caching values, not for arbitrary side effects`,
135+
suggestions: null,
136+
}).withDetails({
137+
kind: 'error',
138+
loc: body.loc,
139+
message: 'useMemo() callbacks must return a value',
140+
}),
141+
);
142+
} else {
143+
unusedUseMemos.set(lvalue.identifier.id, callee.loc);
144+
}
129145
}
130146
break;
131147
}
@@ -146,10 +162,10 @@ export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
146162
* Even a DCE-based version could be bypassed with `noop(useMemo(...))`.
147163
*/
148164
for (const loc of unusedUseMemos.values()) {
149-
errors.pushDiagnostic(
165+
voidMemoErrors.pushDiagnostic(
150166
CompilerDiagnostic.create({
151167
category: ErrorCategory.VoidUseMemo,
152-
reason: 'Unused useMemo()',
168+
reason: 'useMemo() result is unused',
153169
description: `This useMemo() value is unused. useMemo() is for computing and caching values, not for arbitrary side effects`,
154170
suggestions: null,
155171
}).withDetails({
@@ -160,6 +176,7 @@ export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
160176
);
161177
}
162178
}
179+
fn.env.logErrors(voidMemoErrors.asResult());
163180
return errors.asResult();
164181
}
165182

@@ -192,3 +209,17 @@ function validateNoContextVariableAssignment(
192209
}
193210
}
194211
}
212+
213+
function hasNonVoidReturn(func: HIRFunction): boolean {
214+
for (const [, block] of func.body.blocks) {
215+
if (block.terminal.kind === 'return') {
216+
if (
217+
block.terminal.returnVariant === 'Explicit' ||
218+
block.terminal.returnVariant === 'Implicit'
219+
) {
220+
return true;
221+
}
222+
}
223+
}
224+
return false;
225+
}

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-unused-usememo.expect.md

Lines changed: 0 additions & 35 deletions
This file was deleted.

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

Lines changed: 0 additions & 64 deletions
This file was deleted.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
2+
## Input
3+
4+
```javascript
5+
// @validateNoVoidUseMemo @loggerTestOnly
6+
function Component() {
7+
useMemo(() => {
8+
return [];
9+
}, []);
10+
return <div />;
11+
}
12+
13+
```
14+
15+
## Code
16+
17+
```javascript
18+
import { c as _c } from "react/compiler-runtime"; // @validateNoVoidUseMemo @loggerTestOnly
19+
function Component() {
20+
const $ = _c(1);
21+
let t0;
22+
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
23+
t0 = <div />;
24+
$[0] = t0;
25+
} else {
26+
t0 = $[0];
27+
}
28+
return t0;
29+
}
30+
31+
```
32+
33+
## Logs
34+
35+
```
36+
{"kind":"CompileError","detail":{"options":{"category":"VoidUseMemo","reason":"useMemo() result is unused","description":"This useMemo() value is unused. useMemo() is for computing and caching values, not for arbitrary side effects","suggestions":null,"details":[{"kind":"error","loc":{"start":{"line":3,"column":2,"index":67},"end":{"line":3,"column":9,"index":74},"filename":"invalid-unused-usememo.ts","identifierName":"useMemo"},"message":"useMemo() result is unused"}]}},"fnLoc":null}
37+
{"kind":"CompileSuccess","fnLoc":{"start":{"line":2,"column":0,"index":42},"end":{"line":7,"column":1,"index":127},"filename":"invalid-unused-usememo.ts"},"fnName":"Component","memoSlots":1,"memoBlocks":1,"memoValues":1,"prunedMemoBlocks":0,"prunedMemoValues":0}
38+
```
39+
40+
### Eval output
41+
(kind: exception) Fixture not implemented
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// @validateNoVoidUseMemo
1+
// @validateNoVoidUseMemo @loggerTestOnly
22
function Component() {
33
useMemo(() => {
44
return [];
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
2+
## Input
3+
4+
```javascript
5+
// @validateNoVoidUseMemo @loggerTestOnly
6+
function Component() {
7+
const value = useMemo(() => {
8+
console.log('computing');
9+
}, []);
10+
const value2 = React.useMemo(() => {
11+
console.log('computing');
12+
}, []);
13+
return (
14+
<div>
15+
{value}
16+
{value2}
17+
</div>
18+
);
19+
}
20+
21+
```
22+
23+
## Code
24+
25+
```javascript
26+
import { c as _c } from "react/compiler-runtime"; // @validateNoVoidUseMemo @loggerTestOnly
27+
function Component() {
28+
const $ = _c(1);
29+
30+
console.log("computing");
31+
32+
console.log("computing");
33+
let t0;
34+
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
35+
t0 = (
36+
<div>
37+
{undefined}
38+
{undefined}
39+
</div>
40+
);
41+
$[0] = t0;
42+
} else {
43+
t0 = $[0];
44+
}
45+
return t0;
46+
}
47+
48+
```
49+
50+
## Logs
51+
52+
```
53+
{"kind":"CompileError","detail":{"options":{"category":"VoidUseMemo","reason":"useMemo() callbacks must return a value","description":"This useMemo() callback doesn't return a value. useMemo() is for computing and caching values, not for arbitrary side effects","suggestions":null,"details":[{"kind":"error","loc":{"start":{"line":3,"column":24,"index":89},"end":{"line":5,"column":3,"index":130},"filename":"invalid-useMemo-no-return-value.ts"},"message":"useMemo() callbacks must return a value"}]}},"fnLoc":null}
54+
{"kind":"CompileError","detail":{"options":{"category":"VoidUseMemo","reason":"useMemo() callbacks must return a value","description":"This useMemo() callback doesn't return a value. useMemo() is for computing and caching values, not for arbitrary side effects","suggestions":null,"details":[{"kind":"error","loc":{"start":{"line":6,"column":31,"index":168},"end":{"line":8,"column":3,"index":209},"filename":"invalid-useMemo-no-return-value.ts"},"message":"useMemo() callbacks must return a value"}]}},"fnLoc":null}
55+
{"kind":"CompileSuccess","fnLoc":{"start":{"line":2,"column":0,"index":42},"end":{"line":15,"column":1,"index":283},"filename":"invalid-useMemo-no-return-value.ts"},"fnName":"Component","memoSlots":1,"memoBlocks":1,"memoValues":1,"prunedMemoBlocks":0,"prunedMemoValues":0}
56+
```
57+
58+
### Eval output
59+
(kind: exception) Fixture not implemented
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// @validateNoVoidUseMemo
1+
// @validateNoVoidUseMemo @loggerTestOnly
22
function Component() {
33
const value = useMemo(() => {
44
console.log('computing');

0 commit comments

Comments
 (0)