Skip to content

Commit 3f01c45

Browse files
committed
[compiler] Reuse DropManualMemoization for ValidateNoVoidUseMemo
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.
1 parent c18e566 commit 3f01c45

File tree

5 files changed

+79
-168
lines changed

5 files changed

+79
-168
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.enableValidateNoVoidUseMemo) {
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: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8-
import {CompilerError, SourceLocation} from '..';
8+
import {CompilerError, ErrorSeverity, SourceLocation} from '..';
99
import {
1010
CallExpression,
1111
Effect,
@@ -30,6 +30,7 @@ import {
3030
makeInstructionId,
3131
} from '../HIR';
3232
import {createTemporaryPlace, markInstructionIds} from '../HIR/HIRBuilder';
33+
import {Result} from '../Utils/Result';
3334

3435
type ManualMemoCallee = {
3536
kind: 'useMemo' | 'useCallback';
@@ -341,8 +342,14 @@ function extractManualMemoizationArgs(
341342
* rely on type inference to find useMemo/useCallback invocations, and instead does basic tracking
342343
* of globals and property loads to find both direct calls as well as usage via the React namespace,
343344
* eg `React.useMemo()`.
345+
*
346+
* This pass also validates that useMemo callbacks return a value (not void), ensuring that useMemo
347+
* is only used for memoizing values and not for running arbitrary side effects.
344348
*/
345-
export function dropManualMemoization(func: HIRFunction): void {
349+
export function dropManualMemoization(
350+
func: HIRFunction,
351+
): Result<void, CompilerError> {
352+
const errors = new CompilerError();
346353
const isValidationEnabled =
347354
func.env.config.validatePreserveExistingMemoizationGuarantees ||
348355
func.env.config.validateNoSetStateInRender ||
@@ -390,6 +397,36 @@ export function dropManualMemoization(func: HIRFunction): void {
390397
manualMemo.kind,
391398
sidemap,
392399
);
400+
401+
/**
402+
* Bailout on void return useMemos. This is an anti-pattern where code might be using
403+
* useMemo like useEffect: running arbirtary side-effects synced to changes in specific
404+
* values.
405+
*/
406+
if (
407+
func.env.config.enableValidateNoVoidUseMemo &&
408+
manualMemo.kind === 'useMemo'
409+
) {
410+
const funcToCheck = sidemap.functions.get(
411+
fnPlace.identifier.id,
412+
)?.value;
413+
if (funcToCheck !== undefined && funcToCheck.loweredFunc.func) {
414+
if (doesFunctionHaveVoidReturn(funcToCheck.loweredFunc.func)) {
415+
errors.push({
416+
severity: ErrorSeverity.InvalidReact,
417+
reason: `React Compiler has skipped optimizing this component because ${
418+
manualMemo.loadInstr.value.kind === 'PropertyLoad'
419+
? 'React.useMemo'
420+
: 'useMemo'
421+
} doesn't return a value. useMemo should only be used for memoizing values, not running arbitrary side effects.`,
422+
loc: instr.value.loc,
423+
suggestions: null,
424+
description: null,
425+
});
426+
}
427+
}
428+
}
429+
393430
instr.value = getManualMemoizationReplacement(
394431
fnPlace,
395432
instr.value.loc,
@@ -486,6 +523,8 @@ export function dropManualMemoization(func: HIRFunction): void {
486523
markInstructionIds(func.body);
487524
}
488525
}
526+
527+
return errors.asResult();
489528
}
490529

491530
function findOptionalPlaces(fn: HIRFunction): Set<IdentifierId> {
@@ -530,3 +569,17 @@ function findOptionalPlaces(fn: HIRFunction): Set<IdentifierId> {
530569
}
531570
return optionals;
532571
}
572+
573+
function doesFunctionHaveVoidReturn(func: HIRFunction): boolean {
574+
for (const [, block] of func.body.blocks) {
575+
if (block.terminal.kind === 'return') {
576+
if (
577+
block.terminal.returnVariant === 'Explicit' ||
578+
block.terminal.returnVariant === 'Implicit'
579+
) {
580+
return false;
581+
}
582+
}
583+
}
584+
return true;
585+
}

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: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,37 @@ function Component() {
2424
## Error
2525

2626
```
27-
Found 1 error:
27+
Found 2 errors:
2828
2929
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.
3030
3131
error.useMemo-no-return-value.ts:3:16
3232
1 | // @enableValidateNoVoidUseMemo
3333
2 | function Component() {
3434
> 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 | }, []);
35+
| ^^^^^^^^^^^^^^^
36+
> 4 | console.log('computing');
37+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
38+
> 5 | }, []);
39+
| ^^^^^^^^^ 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.
3840
6 | const value2 = React.useMemo(() => {
41+
7 | console.log('computing');
42+
8 | }, []);
43+
44+
Error: React Compiler has skipped optimizing this component because React.useMemo doesn't return a value. useMemo should only be used for memoizing values, not running arbitrary side effects.
45+
46+
error.useMemo-no-return-value.ts:6:17
47+
4 | console.log('computing');
48+
5 | }, []);
49+
> 6 | const value2 = React.useMemo(() => {
50+
| ^^^^^^^^^^^^^^^^^^^^^
51+
> 7 | console.log('computing');
52+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
53+
> 8 | }, []);
54+
| ^^^^^^^^^ React Compiler has skipped optimizing this component because React.useMemo doesn't return a value. useMemo should only be used for memoizing values, not running arbitrary side effects.
55+
9 | return (
56+
10 | <div>
57+
11 | {value}
3958
```
4059
4160

0 commit comments

Comments
 (0)