Skip to content

Commit b5c1637

Browse files
authored
[compiler] Reuse DropManualMemoization for ValidateNoVoidUseMemo (#34001)
Much of the logic in the new validation pass is already implemented in DropManualMemoization, so let's combine them. I opted to keep the environment flag so we can more precisely control the rollout. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34001). * #34022 * #34002 * __->__ #34001
1 parent c60eebf commit b5c1637

File tree

5 files changed

+94
-169
lines changed

5 files changed

+94
-169
lines changed

compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,6 @@ import {
8282
import {inferTypes} from '../TypeInference';
8383
import {
8484
validateContextVariableLValues,
85-
validateNoVoidUseMemo,
8685
validateHooksUsage,
8786
validateMemoizedEffectDependencies,
8887
validateNoCapitalizedCalls,
@@ -168,17 +167,14 @@ function runWithEnvironment(
168167

169168
validateContextVariableLValues(hir);
170169
validateUseMemo(hir).unwrap();
171-
if (env.config.validateNoVoidUseMemo) {
172-
validateNoVoidUseMemo(hir).unwrap();
173-
}
174170

175171
if (
176172
env.isInferredMemoEnabled &&
177173
!env.config.enablePreserveExistingManualUseMemo &&
178174
!env.config.disableMemoizationForDebugging &&
179175
!env.config.enableChangeDetectionForDebugging
180176
) {
181-
dropManualMemoization(hir);
177+
dropManualMemoization(hir).unwrap();
182178
log({kind: 'hir', name: 'DropManualMemoization', value: hir});
183179
}
184180

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

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8-
import {CompilerError, SourceLocation} from '..';
8+
import {
9+
CompilerDiagnostic,
10+
CompilerError,
11+
ErrorSeverity,
12+
SourceLocation,
13+
} from '..';
914
import {
1015
CallExpression,
1116
Effect,
@@ -30,6 +35,7 @@ import {
3035
makeInstructionId,
3136
} from '../HIR';
3237
import {createTemporaryPlace, markInstructionIds} from '../HIR/HIRBuilder';
38+
import {Result} from '../Utils/Result';
3339

3440
type ManualMemoCallee = {
3541
kind: 'useMemo' | 'useCallback';
@@ -341,8 +347,14 @@ function extractManualMemoizationArgs(
341347
* rely on type inference to find useMemo/useCallback invocations, and instead does basic tracking
342348
* of globals and property loads to find both direct calls as well as usage via the React namespace,
343349
* eg `React.useMemo()`.
350+
*
351+
* This pass also validates that useMemo callbacks return a value (not void), ensuring that useMemo
352+
* is only used for memoizing values and not for running arbitrary side effects.
344353
*/
345-
export function dropManualMemoization(func: HIRFunction): void {
354+
export function dropManualMemoization(
355+
func: HIRFunction,
356+
): Result<void, CompilerError> {
357+
const errors = new CompilerError();
346358
const isValidationEnabled =
347359
func.env.config.validatePreserveExistingMemoizationGuarantees ||
348360
func.env.config.validateNoSetStateInRender ||
@@ -390,6 +402,41 @@ export function dropManualMemoization(func: HIRFunction): void {
390402
manualMemo.kind,
391403
sidemap,
392404
);
405+
406+
/**
407+
* Bailout on void return useMemos. This is an anti-pattern where code might be using
408+
* useMemo like useEffect: running arbirtary side-effects synced to changes in specific
409+
* values.
410+
*/
411+
if (
412+
func.env.config.validateNoVoidUseMemo &&
413+
manualMemo.kind === 'useMemo'
414+
) {
415+
const funcToCheck = sidemap.functions.get(
416+
fnPlace.identifier.id,
417+
)?.value;
418+
if (funcToCheck !== undefined && funcToCheck.loweredFunc.func) {
419+
if (!hasNonVoidReturn(funcToCheck.loweredFunc.func)) {
420+
errors.pushDiagnostic(
421+
CompilerDiagnostic.create({
422+
severity: ErrorSeverity.InvalidReact,
423+
category: 'useMemo() callbacks must return a value',
424+
description: `This ${
425+
manualMemo.loadInstr.value.kind === 'PropertyLoad'
426+
? 'React.useMemo'
427+
: 'useMemo'
428+
} callback doesn't return a value. useMemo is for computing and caching values, not for arbitrary side effects.`,
429+
suggestions: null,
430+
}).withDetail({
431+
kind: 'error',
432+
loc: instr.value.loc,
433+
message: 'useMemo() callbacks must return a value',
434+
}),
435+
);
436+
}
437+
}
438+
}
439+
393440
instr.value = getManualMemoizationReplacement(
394441
fnPlace,
395442
instr.value.loc,
@@ -486,6 +533,8 @@ export function dropManualMemoization(func: HIRFunction): void {
486533
markInstructionIds(func.body);
487534
}
488535
}
536+
537+
return errors.asResult();
489538
}
490539

491540
function findOptionalPlaces(fn: HIRFunction): Set<IdentifierId> {
@@ -530,3 +579,17 @@ function findOptionalPlaces(fn: HIRFunction): Set<IdentifierId> {
530579
}
531580
return optionals;
532581
}
582+
583+
function hasNonVoidReturn(func: HIRFunction): boolean {
584+
for (const [, block] of func.body.blocks) {
585+
if (block.terminal.kind === 'return') {
586+
if (
587+
block.terminal.returnVariant === 'Explicit' ||
588+
block.terminal.returnVariant === 'Implicit'
589+
) {
590+
return true;
591+
}
592+
}
593+
}
594+
return false;
595+
}

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

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

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
*/
77

88
export {validateContextVariableLValues} from './ValidateContextVariableLValues';
9-
export {validateNoVoidUseMemo} from './ValidateNoVoidUseMemo';
109
export {validateHooksUsage} from './ValidateHooksUsage';
1110
export {validateMemoizedEffectDependencies} from './ValidateMemoizedEffectDependencies';
1211
export {validateNoCapitalizedCalls} from './ValidateNoCapitalizedCalls';

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

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,41 @@ function Component() {
2424
## Error
2525

2626
```
27-
Found 1 error:
27+
Found 2 errors:
2828
29-
Error: React Compiler has skipped optimizing this component because useMemo doesn't return a value. useMemo should only be used for memoizing values, not running arbitrary side effects.
29+
Error: useMemo() callbacks must return a value
30+
31+
This useMemo callback doesn't return a value. useMemo is for computing and caching values, not for arbitrary side effects.
3032
3133
error.useMemo-no-return-value.ts:3:16
3234
1 | // @validateNoVoidUseMemo
3335
2 | function Component() {
3436
> 3 | const value = useMemo(() => {
35-
| ^^^^^^^ React Compiler has skipped optimizing this component because useMemo doesn't return a value. useMemo should only be used for memoizing values, not running arbitrary side effects.
36-
4 | console.log('computing');
37-
5 | }, []);
37+
| ^^^^^^^^^^^^^^^
38+
> 4 | console.log('computing');
39+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
40+
> 5 | }, []);
41+
| ^^^^^^^^^ useMemo() callbacks must return a value
3842
6 | const value2 = React.useMemo(() => {
43+
7 | console.log('computing');
44+
8 | }, []);
45+
46+
Error: useMemo() callbacks must return a value
47+
48+
This React.useMemo callback doesn't return a value. useMemo is for computing and caching values, not for arbitrary side effects.
49+
50+
error.useMemo-no-return-value.ts:6:17
51+
4 | console.log('computing');
52+
5 | }, []);
53+
> 6 | const value2 = React.useMemo(() => {
54+
| ^^^^^^^^^^^^^^^^^^^^^
55+
> 7 | console.log('computing');
56+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
57+
> 8 | }, []);
58+
| ^^^^^^^^^ useMemo() callbacks must return a value
59+
9 | return (
60+
10 | <div>
61+
11 | {value}
3962
```
4063
4164

0 commit comments

Comments
 (0)