Compare commits

..

1 Commits

Author SHA1 Message Date
Joe Savona
00ae726dd3 [compiler] Add fault tolerance plan document
Add detailed plan for making the React Compiler fault-tolerant by
accumulating errors across all passes instead of stopping at the first
error. This enables reporting multiple compilation errors at once.
2026-02-21 13:01:33 -08:00
55 changed files with 454 additions and 1111 deletions

View File

@@ -31,7 +31,7 @@ Note that some errors may continue to cause an eager bailout:
Add error accumulation to the `Environment` class so that any pass can record errors during compilation without halting.
- [x] **1.1 Add error accumulator to Environment** (`src/HIR/Environment.ts`)
- [ ] **1.1 Add error accumulator to Environment** (`src/HIR/Environment.ts`)
- Add a `#errors: CompilerError` field, initialized in the constructor
- Add a `recordError(error: CompilerDiagnostic | CompilerErrorDetail)` method that:
- If an Invariant-category detail, immediately throw it
@@ -41,7 +41,7 @@ Add error accumulation to the `Environment` class so that any pass can record er
- Add a `aggregateErrors(): CompilerError` method that returns the accumulated error object
- Consider whether `recordError` should accept the same options as `CompilerError.push()` for convenience (reason, description, severity, loc, etc.)
- [x] **1.2 Add a `tryRecord` helper on Environment** (`src/HIR/Environment.ts`)
- [ ] **1.2 Add a `tryRecord` helper on Environment** (`src/HIR/Environment.ts`)
- Add a `tryRecord(fn: () => void): void` method that wraps a callback in try/catch:
- If `fn` throws a `CompilerError` that is NOT an invariant, record it via `recordError`
- If `fn` throws a non-CompilerError or a CompilerError invariant, re-throw
@@ -51,20 +51,20 @@ Add error accumulation to the `Environment` class so that any pass can record er
Change `runWithEnvironment` to run all passes and check for errors at the end instead of letting exceptions propagate.
- [x] **2.1 Change `runWithEnvironment` return type** (`src/Entrypoint/Pipeline.ts`)
- [ ] **2.1 Change `runWithEnvironment` return type** (`src/Entrypoint/Pipeline.ts`)
- Change return type from `CodegenFunction` to `Result<CodegenFunction, CompilerError>`
- At the end of the pipeline, check `env.hasErrors()`:
- If no errors: return `Ok(ast)`
- If errors: return `Err(env.aggregateErrors())`
- [x] **2.2 Update `compileFn` to propagate the Result** (`src/Entrypoint/Pipeline.ts`)
- [ ] **2.2 Update `compileFn` to propagate the Result** (`src/Entrypoint/Pipeline.ts`)
- Change `compileFn` return type from `CodegenFunction` to `Result<CodegenFunction, CompilerError>`
- Propagate the Result from `runWithEnvironment`
- [x] **2.3 Update `run` to propagate the Result** (`src/Entrypoint/Pipeline.ts`)
- [ ] **2.3 Update `run` to propagate the Result** (`src/Entrypoint/Pipeline.ts`)
- Same change for the internal `run` function
- [x] **2.4 Update callers in Program.ts** (`src/Entrypoint/Program.ts`)
- [ ] **2.4 Update callers in Program.ts** (`src/Entrypoint/Program.ts`)
- In `tryCompileFunction`, change from try/catch around `compileFn` to handling the `Result`:
- If `Ok(codegenFn)`: return the compiled function
- If `Err(compilerError)`: return `{kind: 'error', error: compilerError}`
@@ -127,64 +127,64 @@ All validation passes need to record errors on the environment instead of return
These passes already accumulate errors internally and return `Result<void, CompilerError>`. The change is: instead of returning the Result, record errors on `env` and return void. Remove the `.unwrap()` call in Pipeline.ts.
- [x] **4.1 `validateHooksUsage`** (`src/Validation/ValidateHooksUsage.ts`)
- [ ] **4.1 `validateHooksUsage`** (`src/Validation/ValidateHooksUsage.ts`)
- Change signature from `(fn: HIRFunction): Result<void, CompilerError>` to `(fn: HIRFunction): void`
- Record errors on `fn.env` instead of returning `errors.asResult()`
- Update Pipeline.ts call site (line 211): remove `.unwrap()`
- [x] **4.2 `validateNoCapitalizedCalls`** (`src/Validation/ValidateNoCapitalizedCalls.ts`)
- [ ] **4.2 `validateNoCapitalizedCalls`** (`src/Validation/ValidateNoCapitalizedCalls.ts`)
- Change signature to return void
- Fix the hybrid pattern: the direct `CallExpression` path currently throws via `CompilerError.throwInvalidReact()` — change to record on env
- The `MethodCall` path already accumulates — change to record on env
- Update Pipeline.ts call site (line 214): remove `.unwrap()`
- [x] **4.3 `validateUseMemo`** (`src/Validation/ValidateUseMemo.ts`)
- [ ] **4.3 `validateUseMemo`** (`src/Validation/ValidateUseMemo.ts`)
- Change signature to return void
- Record hard errors on env instead of returning `errors.asResult()`
- The soft `voidMemoErrors` path already uses `env.logErrors()` — keep as-is or also record
- Update Pipeline.ts call site (line 170): remove `.unwrap()`
- [x] **4.4 `dropManualMemoization`** (`src/Inference/DropManualMemoization.ts`)
- [ ] **4.4 `dropManualMemoization`** (`src/Inference/DropManualMemoization.ts`)
- Change signature to return void
- Record errors on env instead of returning `errors.asResult()`
- Update Pipeline.ts call site (line 178): remove `.unwrap()`
- [x] **4.5 `validateNoRefAccessInRender`** (`src/Validation/ValidateNoRefAccessInRender.ts`)
- [ ] **4.5 `validateNoRefAccessInRender`** (`src/Validation/ValidateNoRefAccessInRender.ts`)
- Change signature to return void
- Record errors on env instead of returning Result
- Update Pipeline.ts call site (line 275): remove `.unwrap()`
- [x] **4.6 `validateNoSetStateInRender`** (`src/Validation/ValidateNoSetStateInRender.ts`)
- [ ] **4.6 `validateNoSetStateInRender`** (`src/Validation/ValidateNoSetStateInRender.ts`)
- Change signature to return void
- Record errors on env
- Update Pipeline.ts call site (line 279): remove `.unwrap()`
- [x] **4.7 `validateNoImpureFunctionsInRender`** (`src/Validation/ValidateNoImpureFunctionsInRender.ts`)
- [ ] **4.7 `validateNoImpureFunctionsInRender`** (`src/Validation/ValidateNoImpureFunctionsInRender.ts`)
- Change signature to return void
- Record errors on env
- Update Pipeline.ts call site (line 300): remove `.unwrap()`
- [x] **4.8 `validateNoFreezingKnownMutableFunctions`** (`src/Validation/ValidateNoFreezingKnownMutableFunctions.ts`)
- [ ] **4.8 `validateNoFreezingKnownMutableFunctions`** (`src/Validation/ValidateNoFreezingKnownMutableFunctions.ts`)
- Change signature to return void
- Record errors on env
- Update Pipeline.ts call site (line 303): remove `.unwrap()`
- [x] **4.9 `validateExhaustiveDependencies`** (`src/Validation/ValidateExhaustiveDependencies.ts`)
- [ ] **4.9 `validateExhaustiveDependencies`** (`src/Validation/ValidateExhaustiveDependencies.ts`)
- Change signature to return void
- Record errors on env
- Update Pipeline.ts call site (line 315): remove `.unwrap()`
- [x] **4.10 `validateMemoizedEffectDependencies`** (`src/Validation/ValidateMemoizedEffectDependencies.ts`)
- [ ] **4.10 `validateMemoizedEffectDependencies`** (`src/Validation/ValidateMemoizedEffectDependencies.ts`)
- Change signature to return void (note: operates on `ReactiveFunction`)
- Record errors on the function's env
- Update Pipeline.ts call site (line 565): remove `.unwrap()`
- [x] **4.11 `validatePreservedManualMemoization`** (`src/Validation/ValidatePreservedManualMemoization.ts`)
- [ ] **4.11 `validatePreservedManualMemoization`** (`src/Validation/ValidatePreservedManualMemoization.ts`)
- Change signature to return void (note: operates on `ReactiveFunction`)
- Record errors on the function's env
- Update Pipeline.ts call site (line 572): remove `.unwrap()`
- [x] **4.12 `validateSourceLocations`** (`src/Validation/ValidateSourceLocations.ts`)
- [ ] **4.12 `validateSourceLocations`** (`src/Validation/ValidateSourceLocations.ts`)
- Change signature to return void
- Record errors on env
- Update Pipeline.ts call site (line 585): remove `.unwrap()`
@@ -202,16 +202,16 @@ These already use a soft-logging pattern and don't block compilation. They can b
These throw `CompilerError` directly (not via Result). They need the most work.
- [x] **4.17 `validateContextVariableLValues`** (`src/Validation/ValidateContextVariableLValues.ts`)
- [ ] **4.17 `validateContextVariableLValues`** (`src/Validation/ValidateContextVariableLValues.ts`)
- Currently throws via `CompilerError.throwTodo()` and `CompilerError.invariant()`
- Change to record Todo errors on env and continue
- Keep invariant throws (those indicate internal bugs)
- [x] **4.18 `validateLocalsNotReassignedAfterRender`** (`src/Validation/ValidateLocalsNotReassignedAfterRender.ts`)
- [ ] **4.18 `validateLocalsNotReassignedAfterRender`** (`src/Validation/ValidateLocalsNotReassignedAfterRender.ts`)
- Currently constructs a `CompilerError` and `throw`s it directly
- Change to record errors on env
- [x] **4.19 `validateNoDerivedComputationsInEffects`** (`src/Validation/ValidateNoDerivedComputationsInEffects.ts`)
- [ ] **4.19 `validateNoDerivedComputationsInEffects`** (`src/Validation/ValidateNoDerivedComputationsInEffects.ts`)
- Currently throws directly
- Change to record errors on env
@@ -219,14 +219,14 @@ These throw `CompilerError` directly (not via Result). They need the most work.
The inference passes are the most critical to handle correctly because they produce side effects (populating effects on instructions, computing mutable ranges) that downstream passes depend on. They must continue producing valid (even if imprecise) output when errors are encountered.
- [x] **5.1 `inferMutationAliasingEffects`** (`src/Inference/InferMutationAliasingEffects.ts`)
- [ ] **5.1 `inferMutationAliasingEffects`** (`src/Inference/InferMutationAliasingEffects.ts`)
- Currently returns `Result<void, CompilerError>` — errors are about mutation of frozen/global values
- Change to record errors on `fn.env` instead of accumulating internally
- **Key recovery strategy**: When a mutation of a frozen value is detected, record the error but treat the operation as a non-mutating read. This way downstream passes see a consistent (if conservative) view
- When a mutation of a global is detected, record the error but continue with the global unchanged
- Update Pipeline.ts (lines 233-239): remove the conditional `.isErr()` / throw pattern
- [x] **5.2 `inferMutationAliasingRanges`** (`src/Inference/InferMutationAliasingRanges.ts`)
- [ ] **5.2 `inferMutationAliasingRanges`** (`src/Inference/InferMutationAliasingRanges.ts`)
- Currently returns `Result<Array<AliasingEffect>, CompilerError>`
- This pass has a meaningful success value (the function's external aliasing effects)
- Change to: always produce a best-effort effects array, record errors on env
@@ -235,7 +235,7 @@ The inference passes are the most critical to handle correctly because they prod
### Phase 6: Update Codegen
- [x] **6.1 `codegenFunction`** (`src/ReactiveScopes/CodegenReactiveFunction.ts`)
- [ ] **6.1 `codegenFunction`** (`src/ReactiveScopes/CodegenReactiveFunction.ts`)
- Currently returns `Result<CodegenFunction, CompilerError>`
- Change to: always produce a `CodegenFunction`, record errors on env
- If codegen encounters an error (e.g., an instruction it can't generate code for), it should:
@@ -248,58 +248,58 @@ The inference passes are the most critical to handle correctly because they prod
Walk through `runWithEnvironment` and wrap each pass call site. This is the integration work tying Phases 3-6 together.
- [x] **7.1 Wrap `lower()` call** (line 163)
- [ ] **7.1 Wrap `lower()` call** (line 163)
- Change from `lower(func, env).unwrap()` to `lower(func, env)` (direct return after Phase 3.1)
- [x] **7.2 Wrap validation calls that use `.unwrap()`** (lines 169-303)
- [ ] **7.2 Wrap validation calls that use `.unwrap()`** (lines 169-303)
- Remove `.unwrap()` from all validation calls after they're updated in Phase 4
- For validations guarded by `env.enableValidations`, keep the guard but remove the `.unwrap()`
- [x] **7.3 Wrap inference calls** (lines 233-267)
- [ ] **7.3 Wrap inference calls** (lines 233-267)
- After Phase 5, `inferMutationAliasingEffects` and `inferMutationAliasingRanges` record errors directly
- Remove the `mutabilityAliasingErrors` / `mutabilityAliasingRangeErrors` variables and their conditional throw logic
- [x] **7.4 Wrap `env.logErrors()` calls** (lines 286-331)
- [ ] **7.4 Wrap `env.logErrors()` calls** (lines 286-331)
- After Phase 4.13-4.16, these passes record on env directly
- Remove the `env.logErrors()` wrapper calls
- [x] **7.5 Wrap codegen** (lines 575-578)
- [ ] **7.5 Wrap codegen** (lines 575-578)
- After Phase 6.1, `codegenFunction` returns directly
- Remove the `.unwrap()`
- [x] **7.6 Add final error check** (end of `runWithEnvironment`)
- [ ] **7.6 Add final error check** (end of `runWithEnvironment`)
- After all passes complete, check `env.hasErrors()`
- If no errors: return `Ok(ast)`
- If errors: return `Err(env.aggregateErrors())`
- [x] **7.7 Consider wrapping each pass in `env.tryRecord()`** as a safety net
- [ ] **7.7 Consider wrapping each pass in `env.tryRecord()`** as a safety net
- Even after individual passes are updated, wrapping each pass call in `env.tryRecord()` provides defense-in-depth
- If a pass unexpectedly throws a CompilerError (e.g., from a code path we missed), it gets caught and recorded rather than aborting the pipeline
- Non-CompilerError exceptions and invariants still propagate immediately
### Phase 8: Testing
- [x] **8.1 Update existing `error.todo-*` fixture expectations**
- [ ] **8.1 Update existing `error.todo-*` fixture expectations**
- Currently, fixtures with `error.todo-` prefix expect a single error and bailout
- After fault tolerance, some of these may now produce multiple errors
- Update the `.expect.md` files to reflect the new aggregated error output
- [x] **8.2 Add multi-error test fixtures**
- [ ] **8.2 Add multi-error test fixtures**
- Create test fixtures that contain multiple independent errors (e.g., both a `var` declaration and a mutation of a frozen value)
- Verify that all errors are reported, not just the first one
- [x] **8.3 Add test for invariant-still-throws behavior**
- [ ] **8.3 Add test for invariant-still-throws behavior**
- Verify that `CompilerError.invariant()` failures still cause immediate abort
- Verify that non-CompilerError exceptions still cause immediate abort
- [x] **8.4 Add test for partial HIR codegen**
- [ ] **8.4 Add test for partial HIR codegen**
- Verify that when BuildHIR produces partial HIR (with `UnsupportedNode` values), later passes handle it gracefully and codegen produces the original AST for unsupported portions
- [x] **8.5 Verify error severity in aggregated output**
- [ ] **8.5 Verify error severity in aggregated output**
- Test that the aggregated `CompilerError` correctly reports `hasErrors()` vs `hasWarning()` vs `hasHints()` based on the mix of accumulated diagnostics
- Verify that `panicThreshold` behavior in Program.ts is correct for aggregated errors
- [x] **8.6 Run full test suite**
- [ ] **8.6 Run full test suite**
- Run `yarn snap` and `yarn snap -u` to update all fixture expectations
- Ensure no regressions in passing tests
@@ -318,10 +318,3 @@ Walk through `runWithEnvironment` and wrap each pass call site. This is the inte
- The `assertConsistentIdentifiers`, `assertTerminalSuccessorsExist`, `assertTerminalPredsExist`, `assertValidBlockNesting`, `assertValidMutableRanges`, `assertWellFormedBreakTargets`, `assertScopeInstructionsWithinScopes` assertion functions should continue to throw — they are invariant checks on internal data structure consistency
- The `panicThreshold` mechanism in Program.ts should continue to work — it now operates on the aggregated error from the Result rather than a caught exception, but the behavior is the same
## Key Learnings
* **Phase 2+7 (Pipeline tryRecord wrapping) was sufficient for basic fault tolerance.** Wrapping all passes in `env.tryRecord()` immediately enabled the compiler to continue past errors that previously threw. This caused 52 test fixtures to produce additional errors that were previously masked by the first error bailing out. For example, `error.todo-reassign-const` previously reported only "Support destructuring of context variables" but now also reports the immutability violation.
* **Lint-only passes (Pattern B: `env.logErrors()`) should not use `tryRecord()`/`recordError()`** because those errors are intentionally non-blocking. They are reported via the logger only and should not cause the pipeline to return `Err`. The `logErrors` pattern was kept for `validateNoDerivedComputationsInEffects_exp`, `validateNoSetStateInEffects`, `validateNoJSXInTryStatement`, and `validateStaticComponents`.
* **Inference passes that return `Result` with validation errors** (`inferMutationAliasingEffects`, `inferMutationAliasingRanges`) were changed to record errors via `env.recordErrors()` instead of throwing, allowing subsequent passes to proceed.
* **Value-producing passes** (`memoizeFbtAndMacroOperandsInSameScope`, `renameVariables`, `buildReactiveFunction`) need safe default values when wrapped in `tryRecord()` since the callback can't return values. We initialize with empty defaults (e.g., `new Set()`) before the `tryRecord()` call.

View File

@@ -11,6 +11,7 @@ import {
injectReanimatedFlag,
pipelineUsesReanimatedPlugin,
} from '../Entrypoint/Reanimated';
import validateNoUntransformedReferences from '../Entrypoint/ValidateNoUntransformedReferences';
import {CompilerError} from '..';
const ENABLE_REACT_COMPILER_TIMINGS =
@@ -63,12 +64,19 @@ export default function BabelPluginReactCompiler(
},
};
}
compileProgram(prog, {
const result = compileProgram(prog, {
opts,
filename: pass.filename ?? null,
comments: pass.file.ast.comments ?? [],
code: pass.file.code,
});
validateNoUntransformedReferences(
prog,
pass.filename ?? null,
opts.logger,
opts.environment,
result,
);
if (ENABLE_REACT_COMPILER_TIMINGS === true) {
performance.mark(`${filename}:end`, {
detail: 'BabelPlugin:Program:end',

View File

@@ -19,7 +19,7 @@ import {getOrInsertWith} from '../Utils/utils';
import {ExternalFunction, isHookName} from '../HIR/Environment';
import {Err, Ok, Result} from '../Utils/Result';
import {LoggerEvent, ParsedPluginOptions} from './Options';
import {getReactCompilerRuntimeModule} from './Program';
import {BabelFn, getReactCompilerRuntimeModule} from './Program';
import {SuppressionRange} from './Suppression';
export function validateRestrictedImports(
@@ -84,6 +84,11 @@ export class ProgramContext {
// generated imports
imports: Map<string, Map<string, NonLocalImportSpecifier>> = new Map();
/**
* Metadata from compilation
*/
retryErrors: Array<{fn: BabelFn; error: CompilerError}> = [];
constructor({
program,
suppressions,

View File

@@ -228,6 +228,8 @@ const CompilerOutputModeSchema = z.enum([
'ssr',
// Build optimized for the client, with auto memoization
'client',
// Build optimized for the client without auto memo
'client-no-memo',
// Lint mode, the output is unused but validations should run
'lint',
]);

View File

@@ -9,11 +9,8 @@ import {NodePath} from '@babel/traverse';
import * as t from '@babel/types';
import prettyFormat from 'pretty-format';
import {CompilerOutputMode, Logger, ProgramContext} from '.';
import {CompilerError} from '../CompilerError';
import {Err, Ok, Result} from '../Utils/Result';
import {
HIRFunction,
IdentifierId,
ReactiveFunction,
assertConsistentIdentifiers,
assertTerminalPredsExist,
@@ -92,6 +89,7 @@ import {validateNoJSXInTryStatement} from '../Validation/ValidateNoJSXInTryState
import {propagateScopeDependenciesHIR} from '../HIR/PropagateScopeDependenciesHIR';
import {outlineJSX} from '../Optimization/OutlineJsx';
import {optimizePropsMethodCalls} from '../Optimization/OptimizePropsMethodCalls';
import {validateNoImpureFunctionsInRender} from '../Validation/ValidateNoImpureFunctionsInRender';
import {validateStaticComponents} from '../Validation/ValidateStaticComponents';
import {validateNoFreezingKnownMutableFunctions} from '../Validation/ValidateNoFreezingKnownMutableFunctions';
import {inferMutationAliasingEffects} from '../Inference/InferMutationAliasingEffects';
@@ -120,7 +118,7 @@ function run(
logger: Logger | null,
filename: string | null,
code: string | null,
): Result<CodegenFunction, CompilerError> {
): CodegenFunction {
const contextIdentifiers = findContextIdentifiers(func);
const env = new Environment(
func.scope,
@@ -151,7 +149,7 @@ function runWithEnvironment(
t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
>,
env: Environment,
): Result<CodegenFunction, CompilerError> {
): CodegenFunction {
const log = (value: CompilerPipelineValue): void => {
env.logger?.debugLogIRs?.(value);
};
@@ -162,10 +160,10 @@ function runWithEnvironment(
log({kind: 'hir', name: 'PruneMaybeThrows', value: hir});
validateContextVariableLValues(hir);
validateUseMemo(hir);
validateUseMemo(hir).unwrap();
if (env.enableDropManualMemoization) {
dropManualMemoization(hir);
dropManualMemoization(hir).unwrap();
log({kind: 'hir', name: 'DropManualMemoization', value: hir});
}
@@ -198,10 +196,10 @@ function runWithEnvironment(
if (env.enableValidations) {
if (env.config.validateHooksUsage) {
validateHooksUsage(hir);
validateHooksUsage(hir).unwrap();
}
if (env.config.validateNoCapitalizedCalls) {
validateNoCapitalizedCalls(hir);
validateNoCapitalizedCalls(hir).unwrap();
}
}
@@ -211,8 +209,13 @@ function runWithEnvironment(
analyseFunctions(hir);
log({kind: 'hir', name: 'AnalyseFunctions', value: hir});
inferMutationAliasingEffects(hir);
const mutabilityAliasingErrors = inferMutationAliasingEffects(hir);
log({kind: 'hir', name: 'InferMutationAliasingEffects', value: hir});
if (env.enableValidations) {
if (mutabilityAliasingErrors.isErr()) {
throw mutabilityAliasingErrors.unwrapErr();
}
}
if (env.outputMode === 'ssr') {
optimizeForSSR(hir);
@@ -225,11 +228,14 @@ function runWithEnvironment(
pruneMaybeThrows(hir);
log({kind: 'hir', name: 'PruneMaybeThrows', value: hir});
inferMutationAliasingRanges(hir, {
const mutabilityAliasingRangeErrors = inferMutationAliasingRanges(hir, {
isFunctionExpression: false,
});
log({kind: 'hir', name: 'InferMutationAliasingRanges', value: hir});
if (env.enableValidations) {
if (mutabilityAliasingRangeErrors.isErr()) {
throw mutabilityAliasingRangeErrors.unwrapErr();
}
validateLocalsNotReassignedAfterRender(hir);
}
@@ -239,11 +245,11 @@ function runWithEnvironment(
}
if (env.config.validateRefAccessDuringRender) {
validateNoRefAccessInRender(hir);
validateNoRefAccessInRender(hir).unwrap();
}
if (env.config.validateNoSetStateInRender) {
validateNoSetStateInRender(hir);
validateNoSetStateInRender(hir).unwrap();
}
if (
@@ -263,9 +269,11 @@ function runWithEnvironment(
env.logErrors(validateNoJSXInTryStatement(hir));
}
env.tryRecord(() => {
validateNoFreezingKnownMutableFunctions(hir);
});
if (env.config.validateNoImpureFunctionsInRender) {
validateNoImpureFunctionsInRender(hir).unwrap();
}
validateNoFreezingKnownMutableFunctions(hir).unwrap();
}
inferReactivePlaces(hir);
@@ -277,7 +285,7 @@ function runWithEnvironment(
env.config.validateExhaustiveEffectDependencies
) {
// NOTE: this relies on reactivity inference running first
validateExhaustiveDependencies(hir);
validateExhaustiveDependencies(hir).unwrap();
}
}
@@ -306,8 +314,7 @@ function runWithEnvironment(
log({kind: 'hir', name: 'InferReactiveScopeVariables', value: hir});
}
let fbtOperands: Set<IdentifierId> = new Set();
fbtOperands = memoizeFbtAndMacroOperandsInSameScope(hir);
const fbtOperands = memoizeFbtAndMacroOperandsInSameScope(hir);
log({
kind: 'hir',
name: 'MemoizeFbtAndMacroOperandsInSameScope',
@@ -399,8 +406,7 @@ function runWithEnvironment(
value: hir,
});
let reactiveFunction!: ReactiveFunction;
reactiveFunction = buildReactiveFunction(hir);
const reactiveFunction = buildReactiveFunction(hir);
log({
kind: 'reactive',
name: 'BuildReactiveFunction',
@@ -487,8 +493,7 @@ function runWithEnvironment(
value: reactiveFunction,
});
let uniqueIdentifiers: Set<string> = new Set();
uniqueIdentifiers = renameVariables(reactiveFunction);
const uniqueIdentifiers = renameVariables(reactiveFunction);
log({
kind: 'reactive',
name: 'RenameVariables',
@@ -506,20 +511,20 @@ function runWithEnvironment(
env.config.enablePreserveExistingMemoizationGuarantees ||
env.config.validatePreserveExistingMemoizationGuarantees
) {
validatePreservedManualMemoization(reactiveFunction);
validatePreservedManualMemoization(reactiveFunction).unwrap();
}
const ast = codegenFunction(reactiveFunction, {
uniqueIdentifiers,
fbtOperands,
});
}).unwrap();
log({kind: 'ast', name: 'Codegen', value: ast});
for (const outlined of ast.outlined) {
log({kind: 'ast', name: 'Codegen (outlined)', value: outlined.fn});
}
if (env.config.validateSourceLocations) {
validateSourceLocations(func, ast, env);
validateSourceLocations(func, ast).unwrap();
}
/**
@@ -531,10 +536,7 @@ function runWithEnvironment(
throw new Error('unexpected error');
}
if (env.hasErrors()) {
return Err(env.aggregateErrors());
}
return Ok(ast);
return ast;
}
export function compileFn(
@@ -548,7 +550,7 @@ export function compileFn(
logger: Logger | null,
filename: string | null,
code: string | null,
): Result<CodegenFunction, CompilerError> {
): CodegenFunction {
return run(
func,
config,

View File

@@ -350,6 +350,9 @@ function isFilePartOfSources(
return false;
}
export type CompileProgramMetadata = {
retryErrors: Array<{fn: BabelFn; error: CompilerError}>;
};
/**
* Main entrypoint for React Compiler.
*
@@ -360,7 +363,7 @@ function isFilePartOfSources(
export function compileProgram(
program: NodePath<t.Program>,
pass: CompilerPass,
): void {
): CompileProgramMetadata | null {
/**
* This is directly invoked by the react-compiler babel plugin, so exceptions
* thrown by this function will fail the babel build.
@@ -373,7 +376,7 @@ export function compileProgram(
* the outlined functions.
*/
if (shouldSkipCompilation(program, pass)) {
return;
return null;
}
const restrictedImportsErr = validateRestrictedImports(
program,
@@ -381,7 +384,7 @@ export function compileProgram(
);
if (restrictedImportsErr) {
handleError(restrictedImportsErr, pass, null);
return;
return null;
}
/*
* Record lint errors and critical errors as depending on Forget's config,
@@ -475,11 +478,15 @@ export function compileProgram(
);
handleError(error, programContext, null);
}
return;
return null;
}
// Insert React Compiler generated functions into the Babel AST
applyCompiledFunctions(program, compiledFns, pass, programContext);
return {
retryErrors: programContext.retryErrors,
};
}
type CompileSource = {
@@ -697,21 +704,19 @@ function tryCompileFunction(
}
try {
const result = compileFn(
fn,
programContext.opts.environment,
fnType,
outputMode,
programContext,
programContext.opts.logger,
programContext.filename,
programContext.code,
);
if (result.isOk()) {
return {kind: 'compile', compiledFn: result.unwrap()};
} else {
return {kind: 'error', error: result.unwrapErr()};
}
return {
kind: 'compile',
compiledFn: compileFn(
fn,
programContext.opts.environment,
fnType,
outputMode,
programContext,
programContext.opts.logger,
programContext.filename,
programContext.code,
),
};
} catch (err) {
return {kind: 'error', error: err};
}

View File

@@ -0,0 +1,162 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {NodePath} from '@babel/core';
import * as t from '@babel/types';
import {CompilerError, EnvironmentConfig, Logger} from '..';
import {getOrInsertWith} from '../Utils/utils';
import {GeneratedSource} from '../HIR';
import {DEFAULT_EXPORT} from '../HIR/Environment';
import {CompileProgramMetadata} from './Program';
export default function validateNoUntransformedReferences(
path: NodePath<t.Program>,
filename: string | null,
logger: Logger | null,
env: EnvironmentConfig,
compileResult: CompileProgramMetadata | null,
): void {
const moduleLoadChecks = new Map<
string,
Map<string, CheckInvalidReferenceFn>
>();
if (moduleLoadChecks.size > 0) {
transformProgram(path, moduleLoadChecks, filename, logger, compileResult);
}
}
type TraversalState = {
shouldInvalidateScopes: boolean;
program: NodePath<t.Program>;
logger: Logger | null;
filename: string | null;
transformErrors: Array<{fn: NodePath<t.Node>; error: CompilerError}>;
};
type CheckInvalidReferenceFn = (
paths: Array<NodePath<t.Node>>,
context: TraversalState,
) => void;
function validateImportSpecifier(
specifier: NodePath<t.ImportSpecifier>,
importSpecifierChecks: Map<string, CheckInvalidReferenceFn>,
state: TraversalState,
): void {
const imported = specifier.get('imported');
const specifierName: string =
imported.node.type === 'Identifier'
? imported.node.name
: imported.node.value;
const checkFn = importSpecifierChecks.get(specifierName);
if (checkFn == null) {
return;
}
if (state.shouldInvalidateScopes) {
state.shouldInvalidateScopes = false;
state.program.scope.crawl();
}
const local = specifier.get('local');
const binding = local.scope.getBinding(local.node.name);
CompilerError.invariant(binding != null, {
reason: 'Expected binding to be found for import specifier',
loc: local.node.loc ?? GeneratedSource,
});
checkFn(binding.referencePaths, state);
}
function validateNamespacedImport(
specifier: NodePath<t.ImportNamespaceSpecifier | t.ImportDefaultSpecifier>,
importSpecifierChecks: Map<string, CheckInvalidReferenceFn>,
state: TraversalState,
): void {
if (state.shouldInvalidateScopes) {
state.shouldInvalidateScopes = false;
state.program.scope.crawl();
}
const local = specifier.get('local');
const binding = local.scope.getBinding(local.node.name);
const defaultCheckFn = importSpecifierChecks.get(DEFAULT_EXPORT);
CompilerError.invariant(binding != null, {
reason: 'Expected binding to be found for import specifier',
loc: local.node.loc ?? GeneratedSource,
});
const filteredReferences = new Map<
CheckInvalidReferenceFn,
Array<NodePath<t.Node>>
>();
for (const reference of binding.referencePaths) {
if (defaultCheckFn != null) {
getOrInsertWith(filteredReferences, defaultCheckFn, () => []).push(
reference,
);
}
const parent = reference.parentPath;
if (
parent != null &&
parent.isMemberExpression() &&
parent.get('object') === reference
) {
if (parent.node.computed || parent.node.property.type !== 'Identifier') {
continue;
}
const checkFn = importSpecifierChecks.get(parent.node.property.name);
if (checkFn != null) {
getOrInsertWith(filteredReferences, checkFn, () => []).push(parent);
}
}
}
for (const [checkFn, references] of filteredReferences) {
checkFn(references, state);
}
}
function transformProgram(
path: NodePath<t.Program>,
moduleLoadChecks: Map<string, Map<string, CheckInvalidReferenceFn>>,
filename: string | null,
logger: Logger | null,
compileResult: CompileProgramMetadata | null,
): void {
const traversalState: TraversalState = {
shouldInvalidateScopes: true,
program: path,
filename,
logger,
transformErrors: compileResult?.retryErrors ?? [],
};
path.traverse({
ImportDeclaration(path: NodePath<t.ImportDeclaration>) {
const importSpecifierChecks = moduleLoadChecks.get(
path.node.source.value,
);
if (importSpecifierChecks == null) {
return;
}
const specifiers = path.get('specifiers');
for (const specifier of specifiers) {
if (specifier.isImportSpecifier()) {
validateImportSpecifier(
specifier,
importSpecifierChecks,
traversalState,
);
} else {
validateNamespacedImport(
specifier as NodePath<
t.ImportNamespaceSpecifier | t.ImportDefaultSpecifier
>,
importSpecifierChecks,
traversalState,
);
}
}
},
});
}

View File

@@ -8,12 +8,7 @@
import * as t from '@babel/types';
import {ZodError, z} from 'zod/v4';
import {fromZodError} from 'zod-validation-error/v4';
import {
CompilerDiagnostic,
CompilerError,
CompilerErrorDetail,
ErrorCategory,
} from '../CompilerError';
import {CompilerError} from '../CompilerError';
import {CompilerOutputMode, Logger, ProgramContext} from '../Entrypoint';
import {Err, Ok, Result} from '../Utils/Result';
import {
@@ -550,12 +545,6 @@ export class Environment {
#flowTypeEnvironment: FlowTypeEnv | null;
/**
* Accumulated compilation errors. Passes record errors here instead of
* throwing, so the pipeline can continue and report all errors at once.
*/
#errors: CompilerError = new CompilerError();
constructor(
scope: BabelScope,
fnType: ReactFunctionType,
@@ -640,6 +629,9 @@ export class Environment {
case 'ssr': {
return true;
}
case 'client-no-memo': {
return false;
}
default: {
assertExhaustive(
this.outputMode,
@@ -656,7 +648,8 @@ export class Environment {
// linting also enables memoization so that we can check if manual memoization is preserved
return true;
}
case 'ssr': {
case 'ssr':
case 'client-no-memo': {
return false;
}
default: {
@@ -675,6 +668,9 @@ export class Environment {
case 'ssr': {
return true;
}
case 'client-no-memo': {
return false;
}
default: {
assertExhaustive(
this.outputMode,
@@ -713,75 +709,6 @@ export class Environment {
}
}
/**
* Record a single diagnostic or error detail on this environment.
* If the error is an Invariant, it is immediately thrown since invariants
* represent internal bugs that cannot be recovered from.
* Otherwise, the error is accumulated and optionally logged.
*/
recordError(error: CompilerDiagnostic | CompilerErrorDetail): void {
if (error.category === ErrorCategory.Invariant) {
const compilerError = new CompilerError();
if (error instanceof CompilerDiagnostic) {
compilerError.pushDiagnostic(error);
} else {
compilerError.pushErrorDetail(error);
}
throw compilerError;
}
if (error instanceof CompilerDiagnostic) {
this.#errors.pushDiagnostic(error);
} else {
this.#errors.pushErrorDetail(error);
}
}
/**
* Record all diagnostics from a CompilerError onto this environment.
*/
recordErrors(error: CompilerError): void {
for (const detail of error.details) {
this.recordError(detail);
}
}
/**
* Returns true if any errors have been recorded during compilation.
*/
hasErrors(): boolean {
return this.#errors.hasAnyErrors();
}
/**
* Returns the accumulated CompilerError containing all recorded diagnostics.
*/
aggregateErrors(): CompilerError {
return this.#errors;
}
/**
* Wraps a callback in try/catch: if the callback throws a CompilerError
* that is NOT an invariant, the error is recorded and execution continues.
* Non-CompilerError exceptions and invariants are re-thrown.
*/
tryRecord(fn: () => void): void {
try {
fn();
} catch (err) {
if (err instanceof CompilerError) {
// Check if any detail is an invariant — if so, re-throw
for (const detail of err.details) {
if (detail.category === ErrorCategory.Invariant) {
throw err;
}
}
this.recordErrors(err);
} else {
throw err;
}
}
}
isContextIdentifier(node: t.Identifier): boolean {
return this.#contextIdentifiers.has(node);
}

View File

@@ -54,7 +54,7 @@ function lowerWithMutationAliasing(fn: HIRFunction): void {
deadCodeElimination(fn);
const functionEffects = inferMutationAliasingRanges(fn, {
isFunctionExpression: true,
});
}).unwrap();
rewriteInstructionKindsBasedOnReassignment(fn);
inferReactiveScopeVariables(fn);
fn.aliasingEffects = functionEffects;

View File

@@ -31,6 +31,7 @@ import {
makeInstructionId,
} from '../HIR';
import {createTemporaryPlace, markInstructionIds} from '../HIR/HIRBuilder';
import {Result} from '../Utils/Result';
type ManualMemoCallee = {
kind: 'useMemo' | 'useCallback';
@@ -388,7 +389,9 @@ function extractManualMemoizationArgs(
* This pass also validates that useMemo callbacks return a value (not void), ensuring that useMemo
* is only used for memoizing values and not for running arbitrary side effects.
*/
export function dropManualMemoization(func: HIRFunction): void {
export function dropManualMemoization(
func: HIRFunction,
): Result<void, CompilerError> {
const errors = new CompilerError();
const isValidationEnabled =
func.env.config.validatePreserveExistingMemoizationGuarantees ||
@@ -550,9 +553,7 @@ export function dropManualMemoization(func: HIRFunction): void {
}
}
if (errors.hasAnyErrors()) {
func.env.recordErrors(errors);
}
return errors.asResult();
}
function findOptionalPlaces(fn: HIRFunction): Set<IdentifierId> {

View File

@@ -45,7 +45,7 @@ import {
eachTerminalOperand,
eachTerminalSuccessor,
} from '../HIR/visitors';
import {Ok, Result} from '../Utils/Result';
import {
assertExhaustive,
getOrInsertDefault,
@@ -100,7 +100,7 @@ export function inferMutationAliasingEffects(
{isFunctionExpression}: {isFunctionExpression: boolean} = {
isFunctionExpression: false,
},
): void {
): Result<void, CompilerError> {
const initialState = InferenceState.empty(fn.env, isFunctionExpression);
// Map of blocks to the last (merged) incoming state that was processed
@@ -220,7 +220,7 @@ export function inferMutationAliasingEffects(
}
}
}
return;
return Ok(undefined);
}
function findHoistedContextDeclarations(

View File

@@ -26,7 +26,7 @@ import {
eachTerminalOperand,
} from '../HIR/visitors';
import {assertExhaustive, getOrInsertWith} from '../Utils/utils';
import {Err, Ok, Result} from '../Utils/Result';
import {AliasingEffect, MutationReason} from './AliasingEffects';
/**
@@ -74,7 +74,7 @@ import {AliasingEffect, MutationReason} from './AliasingEffects';
export function inferMutationAliasingRanges(
fn: HIRFunction,
{isFunctionExpression}: {isFunctionExpression: boolean},
): Array<AliasingEffect> {
): Result<Array<AliasingEffect>, CompilerError> {
// The set of externally-visible effects
const functionEffects: Array<AliasingEffect> = [];
@@ -547,14 +547,10 @@ export function inferMutationAliasingRanges(
}
}
if (
errors.hasAnyErrors() &&
!isFunctionExpression &&
fn.env.enableValidations
) {
fn.env.recordErrors(errors);
if (errors.hasAnyErrors() && !isFunctionExpression) {
return Err(errors);
}
return functionEffects;
return Ok(functionEffects);
}
function appendFunctionErrors(errors: CompilerError, fn: HIRFunction): void {

View File

@@ -46,7 +46,7 @@ import {
} from '../HIR/HIR';
import {printIdentifier, printInstruction, printPlace} from '../HIR/PrintHIR';
import {eachPatternOperand} from '../HIR/visitors';
import {Err, Ok, Result} from '../Utils/Result';
import {GuardKind} from '../Utils/RuntimeDiagnosticConstants';
import {assertExhaustive} from '../Utils/utils';
import {buildReactiveFunction} from './BuildReactiveFunction';
@@ -111,7 +111,7 @@ export function codegenFunction(
uniqueIdentifiers: Set<string>;
fbtOperands: Set<IdentifierId>;
},
): CodegenFunction {
): Result<CodegenFunction, CompilerError> {
const cx = new Context(
fn.env,
fn.id ?? '[[ anonymous ]]',
@@ -141,7 +141,11 @@ export function codegenFunction(
};
}
const compiled = codegenReactiveFunction(cx, fn);
const compileResult = codegenReactiveFunction(cx, fn);
if (compileResult.isErr()) {
return compileResult;
}
const compiled = compileResult.unwrap();
const hookGuard = fn.env.config.enableEmitHookGuards;
if (hookGuard != null && fn.env.outputMode === 'client') {
@@ -269,7 +273,7 @@ export function codegenFunction(
emitInstrumentForget.globalGating,
);
if (assertResult.isErr()) {
fn.env.recordErrors(assertResult.unwrapErr());
return assertResult;
}
}
@@ -319,17 +323,20 @@ export function codegenFunction(
),
reactiveFunction,
);
outlined.push({fn: codegen, type});
if (codegen.isErr()) {
return codegen;
}
outlined.push({fn: codegen.unwrap(), type});
}
compiled.outlined = outlined;
return compiled;
return compileResult;
}
function codegenReactiveFunction(
cx: Context,
fn: ReactiveFunction,
): CodegenFunction {
): Result<CodegenFunction, CompilerError> {
for (const param of fn.params) {
const place = param.kind === 'Identifier' ? param : param.place;
cx.temp.set(place.identifier.declarationId, null);
@@ -348,13 +355,13 @@ function codegenReactiveFunction(
}
if (cx.errors.hasAnyErrors()) {
fn.env.recordErrors(cx.errors);
return Err(cx.errors);
}
const countMemoBlockVisitor = new CountMemoBlockVisitor(fn.env);
visitReactiveFunction(fn, countMemoBlockVisitor, undefined);
return {
return Ok({
type: 'CodegenFunction',
loc: fn.loc,
id: fn.id !== null ? t.identifier(fn.id) : null,
@@ -369,7 +376,7 @@ function codegenReactiveFunction(
prunedMemoBlocks: countMemoBlockVisitor.prunedMemoBlocks,
prunedMemoValues: countMemoBlockVisitor.prunedMemoValues,
outlined: [],
};
});
}
class CountMemoBlockVisitor extends ReactiveFunctionVisitor<void> {
@@ -1658,7 +1665,7 @@ function codegenInstructionValue(
cx.temp,
),
reactiveFunction,
);
).unwrap();
/*
* ObjectMethod builder must be backwards compatible with older versions of babel.
@@ -1857,7 +1864,7 @@ function codegenInstructionValue(
cx.temp,
),
reactiveFunction,
);
).unwrap();
if (instrValue.type === 'ArrowFunctionExpression') {
let body: t.BlockStatement | t.Expression = fn.body;

View File

@@ -5,9 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerDiagnostic, CompilerError} from '..';
import {ErrorCategory} from '../CompilerError';
import {Environment} from '../HIR/Environment';
import {CompilerError} from '..';
import {HIRFunction, IdentifierId, Place} from '../HIR';
import {printPlace} from '../HIR/PrintHIR';
import {eachInstructionValueLValue, eachPatternOperand} from '../HIR/visitors';
@@ -19,13 +17,12 @@ import {eachInstructionValueLValue, eachPatternOperand} from '../HIR/visitors';
*/
export function validateContextVariableLValues(fn: HIRFunction): void {
const identifierKinds: IdentifierKinds = new Map();
validateContextVariableLValuesImpl(fn, identifierKinds, fn.env);
validateContextVariableLValuesImpl(fn, identifierKinds);
}
function validateContextVariableLValuesImpl(
fn: HIRFunction,
identifierKinds: IdentifierKinds,
env: Environment,
): void {
for (const [, block] of fn.body.blocks) {
for (const instr of block.instructions) {
@@ -33,30 +30,30 @@ function validateContextVariableLValuesImpl(
switch (value.kind) {
case 'DeclareContext':
case 'StoreContext': {
visit(identifierKinds, value.lvalue.place, 'context', env);
visit(identifierKinds, value.lvalue.place, 'context');
break;
}
case 'LoadContext': {
visit(identifierKinds, value.place, 'context', env);
visit(identifierKinds, value.place, 'context');
break;
}
case 'StoreLocal':
case 'DeclareLocal': {
visit(identifierKinds, value.lvalue.place, 'local', env);
visit(identifierKinds, value.lvalue.place, 'local');
break;
}
case 'LoadLocal': {
visit(identifierKinds, value.place, 'local', env);
visit(identifierKinds, value.place, 'local');
break;
}
case 'PostfixUpdate':
case 'PrefixUpdate': {
visit(identifierKinds, value.lvalue, 'local', env);
visit(identifierKinds, value.lvalue, 'local');
break;
}
case 'Destructure': {
for (const lvalue of eachPatternOperand(value.lvalue.pattern)) {
visit(identifierKinds, lvalue, 'destructure', env);
visit(identifierKinds, lvalue, 'destructure');
}
break;
}
@@ -65,24 +62,18 @@ function validateContextVariableLValuesImpl(
validateContextVariableLValuesImpl(
value.loweredFunc.func,
identifierKinds,
env,
);
break;
}
default: {
for (const _ of eachInstructionValueLValue(value)) {
fn.env.recordError(
CompilerDiagnostic.create({
category: ErrorCategory.Todo,
reason:
'ValidateContextVariableLValues: unhandled instruction variant',
description: `Handle '${value.kind} lvalues`,
}).withDetails({
kind: 'error',
loc: value.loc,
message: null,
}),
);
CompilerError.throwTodo({
reason:
'ValidateContextVariableLValues: unhandled instruction variant',
loc: value.loc,
description: `Handle '${value.kind} lvalues`,
suggestions: null,
});
}
}
}
@@ -99,7 +90,6 @@ function visit(
identifiers: IdentifierKinds,
place: Place,
kind: 'local' | 'context' | 'destructure',
env: Environment,
): void {
const prev = identifiers.get(place.identifier.id);
if (prev !== undefined) {
@@ -107,18 +97,12 @@ function visit(
const isContext = kind === 'context';
if (wasContext !== isContext) {
if (prev.kind === 'destructure' || kind === 'destructure') {
env.recordError(
CompilerDiagnostic.create({
category: ErrorCategory.Todo,
reason: `Support destructuring of context variables`,
description: null,
}).withDetails({
kind: 'error',
loc: kind === 'destructure' ? place.loc : prev.place.loc,
message: null,
}),
);
return;
CompilerError.throwTodo({
reason: `Support destructuring of context variables`,
loc: kind === 'destructure' ? place.loc : prev.place.loc,
description: null,
suggestions: null,
});
}
CompilerError.invariant(false, {

View File

@@ -44,6 +44,7 @@ import {
eachInstructionValueOperand,
eachTerminalOperand,
} from '../HIR/visitors';
import {Result} from '../Utils/Result';
import {retainWhere} from '../Utils/utils';
const DEBUG = false;
@@ -87,7 +88,9 @@ const DEBUG = false;
* When we go to compute the dependencies, we then think that the user's manual dep
* logic is part of what the memo computation logic.
*/
export function validateExhaustiveDependencies(fn: HIRFunction): void {
export function validateExhaustiveDependencies(
fn: HIRFunction,
): Result<void, CompilerError> {
const env = fn.env;
const reactive = collectReactiveIdentifiersHIR(fn);
@@ -214,9 +217,7 @@ export function validateExhaustiveDependencies(fn: HIRFunction): void {
},
false, // isFunctionExpression
);
if (error.hasAnyErrors()) {
fn.env.recordErrors(error);
}
return error.asResult();
}
function validateDependencies(

View File

@@ -26,6 +26,7 @@ import {
eachTerminalOperand,
} from '../HIR/visitors';
import {assertExhaustive} from '../Utils/utils';
import {Result} from '../Utils/Result';
/**
* Represents the possible kinds of value which may be stored at a given Place during
@@ -87,7 +88,9 @@ function joinKinds(a: Kind, b: Kind): Kind {
* may not appear as the callee of a conditional call.
* See the note for Kind.PotentialHook for sources of potential hooks
*/
export function validateHooksUsage(fn: HIRFunction): void {
export function validateHooksUsage(
fn: HIRFunction,
): Result<void, CompilerError> {
const unconditionalBlocks = computeUnconditionalBlocks(fn);
const errors = new CompilerError();
@@ -423,9 +426,7 @@ export function validateHooksUsage(fn: HIRFunction): void {
for (const [, error] of errorsByPlace) {
errors.pushErrorDetail(error);
}
if (errors.hasAnyErrors()) {
fn.env.recordErrors(errors);
}
return errors.asResult();
}
function visitFunctionExpression(errors: CompilerError, fn: HIRFunction): void {

View File

@@ -7,7 +7,6 @@
import {CompilerDiagnostic, CompilerError, Effect} from '..';
import {ErrorCategory} from '../CompilerError';
import {Environment} from '../HIR/Environment';
import {HIRFunction, IdentifierId, Place} from '../HIR';
import {
eachInstructionLValue,
@@ -28,15 +27,15 @@ export function validateLocalsNotReassignedAfterRender(fn: HIRFunction): void {
contextVariables,
false,
false,
fn.env,
);
if (reassignment !== null) {
const errors = new CompilerError();
const variable =
reassignment.identifier.name != null &&
reassignment.identifier.name.kind === 'named'
? `\`${reassignment.identifier.name.value}\``
: 'variable';
fn.env.recordError(
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Immutability,
reason: 'Cannot reassign variable after render completes',
@@ -47,6 +46,7 @@ export function validateLocalsNotReassignedAfterRender(fn: HIRFunction): void {
message: `Cannot reassign ${variable} after render completes`,
}),
);
throw errors;
}
}
@@ -55,7 +55,6 @@ function getContextReassignment(
contextVariables: Set<IdentifierId>,
isFunctionExpression: boolean,
isAsync: boolean,
env: Environment,
): Place | null {
const reassigningFunctions = new Map<IdentifierId, Place>();
for (const [, block] of fn.body.blocks) {
@@ -69,7 +68,6 @@ function getContextReassignment(
contextVariables,
true,
isAsync || value.loweredFunc.func.async,
env,
);
if (reassignment === null) {
// If the function itself doesn't reassign, does one of its dependencies?
@@ -86,12 +84,13 @@ function getContextReassignment(
// if the function or its depends reassign, propagate that fact on the lvalue
if (reassignment !== null) {
if (isAsync || value.loweredFunc.func.async) {
const errors = new CompilerError();
const variable =
reassignment.identifier.name !== null &&
reassignment.identifier.name.kind === 'named'
? `\`${reassignment.identifier.name.value}\``
: 'variable';
env.recordError(
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Immutability,
reason: 'Cannot reassign variable in async function',
@@ -103,7 +102,7 @@ function getContextReassignment(
message: `Cannot reassign ${variable}`,
}),
);
return null;
throw errors;
}
reassigningFunctions.set(lvalue.identifier.id, reassignment);
}

View File

@@ -5,12 +5,15 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError, CompilerErrorDetail, EnvironmentConfig} from '..';
import {CompilerError, EnvironmentConfig} from '..';
import {ErrorCategory} from '../CompilerError';
import {HIRFunction, IdentifierId} from '../HIR';
import {DEFAULT_GLOBALS} from '../HIR/Globals';
import {Result} from '../Utils/Result';
export function validateNoCapitalizedCalls(fn: HIRFunction): void {
export function validateNoCapitalizedCalls(
fn: HIRFunction,
): Result<void, CompilerError> {
const envConfig: EnvironmentConfig = fn.env.config;
const ALLOW_LIST = new Set([
...DEFAULT_GLOBALS.keys(),
@@ -45,16 +48,13 @@ export function validateNoCapitalizedCalls(fn: HIRFunction): void {
const calleeIdentifier = value.callee.identifier.id;
const calleeName = capitalLoadGlobals.get(calleeIdentifier);
if (calleeName != null) {
fn.env.recordError(
new CompilerErrorDetail({
category: ErrorCategory.CapitalizedCalls,
reason,
description: `${calleeName} may be a component`,
loc: value.loc,
suggestions: null,
}),
);
continue;
CompilerError.throwInvalidReact({
category: ErrorCategory.CapitalizedCalls,
reason,
description: `${calleeName} may be a component`,
loc: value.loc,
suggestions: null,
});
}
break;
}
@@ -85,7 +85,5 @@ export function validateNoCapitalizedCalls(fn: HIRFunction): void {
}
}
}
if (errors.hasAnyErrors()) {
fn.env.recordErrors(errors);
}
return errors.asResult();
}

View File

@@ -97,8 +97,8 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
}
}
}
for (const detail of errors.details) {
fn.env.recordError(detail);
if (errors.hasAnyErrors()) {
throw errors;
}
}

View File

@@ -18,6 +18,7 @@ import {
eachTerminalOperand,
} from '../HIR/visitors';
import {AliasingEffect} from '../Inference/AliasingEffects';
import {Result} from '../Utils/Result';
/**
* Validates that functions with known mutations (ie due to types) cannot be passed
@@ -42,7 +43,9 @@ import {AliasingEffect} from '../Inference/AliasingEffects';
* This pass detects functions with *known* mutations (Store or Mutate, not ConditionallyMutate)
* that are passed where a frozen value is expected and rejects them.
*/
export function validateNoFreezingKnownMutableFunctions(fn: HIRFunction): void {
export function validateNoFreezingKnownMutableFunctions(
fn: HIRFunction,
): Result<void, CompilerError> {
const errors = new CompilerError();
const contextMutationEffects: Map<
IdentifierId,
@@ -159,7 +162,5 @@ export function validateNoFreezingKnownMutableFunctions(fn: HIRFunction): void {
visitOperand(operand);
}
}
if (errors.hasAnyErrors()) {
fn.env.recordErrors(errors);
}
return errors.asResult();
}

View File

@@ -9,6 +9,7 @@ import {CompilerDiagnostic, CompilerError} from '..';
import {ErrorCategory} from '../CompilerError';
import {HIRFunction} from '../HIR';
import {getFunctionCallSignature} from '../Inference/InferMutationAliasingEffects';
import {Result} from '../Utils/Result';
/**
* Checks that known-impure functions are not called during render. Examples of invalid functions to
@@ -19,7 +20,9 @@ import {getFunctionCallSignature} from '../Inference/InferMutationAliasingEffect
* this in several of our validation passes and should unify those analyses into a reusable helper
* and use it here.
*/
export function validateNoImpureFunctionsInRender(fn: HIRFunction): void {
export function validateNoImpureFunctionsInRender(
fn: HIRFunction,
): Result<void, CompilerError> {
const errors = new CompilerError();
for (const [, block] of fn.body.blocks) {
for (const instr of block.instructions) {
@@ -52,7 +55,5 @@ export function validateNoImpureFunctionsInRender(fn: HIRFunction): void {
}
}
}
if (errors.hasAnyErrors()) {
fn.env.recordErrors(errors);
}
return errors.asResult();
}

View File

@@ -27,6 +27,7 @@ import {
eachPatternOperand,
eachTerminalOperand,
} from '../HIR/visitors';
import {Err, Ok, Result} from '../Utils/Result';
import {retainWhere} from '../Utils/utils';
/**
@@ -119,14 +120,12 @@ class Env {
}
}
export function validateNoRefAccessInRender(fn: HIRFunction): void {
export function validateNoRefAccessInRender(
fn: HIRFunction,
): Result<void, CompilerError> {
const env = new Env();
collectTemporariesSidemap(fn, env);
const errors = new CompilerError();
validateNoRefAccessInRenderImpl(fn, env, errors);
if (errors.hasAnyErrors()) {
fn.env.recordErrors(errors);
}
return validateNoRefAccessInRenderImpl(fn, env).map(_ => undefined);
}
function collectTemporariesSidemap(fn: HIRFunction, env: Env): void {
@@ -306,8 +305,7 @@ function joinRefAccessTypes(...types: Array<RefAccessType>): RefAccessType {
function validateNoRefAccessInRenderImpl(
fn: HIRFunction,
env: Env,
errors: CompilerError,
): RefAccessType {
): Result<RefAccessType, CompilerError> {
let returnValues: Array<undefined | RefAccessType> = [];
let place;
for (const param of fn.params) {
@@ -338,6 +336,7 @@ function validateNoRefAccessInRenderImpl(
env.resetChanged();
returnValues = [];
const safeBlocks: Array<{block: BlockId; ref: RefId}> = [];
const errors = new CompilerError();
for (const [, block] of fn.body.blocks) {
retainWhere(safeBlocks, entry => entry.block !== block.id);
for (const phi of block.phis) {
@@ -433,15 +432,13 @@ function validateNoRefAccessInRenderImpl(
case 'FunctionExpression': {
let returnType: RefAccessType = {kind: 'None'};
let readRefEffect = false;
const innerErrors = new CompilerError();
const result = validateNoRefAccessInRenderImpl(
instr.value.loweredFunc.func,
env,
innerErrors,
);
if (!innerErrors.hasAnyErrors()) {
returnType = result;
} else {
if (result.isOk()) {
returnType = result.unwrap();
} else if (result.isErr()) {
readRefEffect = true;
}
env.set(instr.lvalue.identifier.id, {
@@ -732,7 +729,7 @@ function validateNoRefAccessInRenderImpl(
}
if (errors.hasAnyErrors()) {
return {kind: 'None'};
return Err(errors);
}
}
@@ -741,8 +738,10 @@ function validateNoRefAccessInRenderImpl(
loc: GeneratedSource,
});
return joinRefAccessTypes(
...returnValues.filter((env): env is RefAccessType => env !== undefined),
return Ok(
joinRefAccessTypes(
...returnValues.filter((env): env is RefAccessType => env !== undefined),
),
);
}

View File

@@ -13,6 +13,7 @@ import {
import {HIRFunction, IdentifierId, isSetStateType} from '../HIR';
import {computeUnconditionalBlocks} from '../HIR/ComputeUnconditionalBlocks';
import {eachInstructionValueOperand} from '../HIR/visitors';
import {Result} from '../Utils/Result';
/**
* Validates that the given function does not have an infinite update loop
@@ -42,21 +43,17 @@ import {eachInstructionValueOperand} from '../HIR/visitors';
* y();
* ```
*/
export function validateNoSetStateInRender(fn: HIRFunction): void {
export function validateNoSetStateInRender(
fn: HIRFunction,
): Result<void, CompilerError> {
const unconditionalSetStateFunctions: Set<IdentifierId> = new Set();
const errors = validateNoSetStateInRenderImpl(
fn,
unconditionalSetStateFunctions,
);
if (errors.hasAnyErrors()) {
fn.env.recordErrors(errors);
}
return validateNoSetStateInRenderImpl(fn, unconditionalSetStateFunctions);
}
function validateNoSetStateInRenderImpl(
fn: HIRFunction,
unconditionalSetStateFunctions: Set<IdentifierId>,
): CompilerError {
): Result<void, CompilerError> {
const unconditionalBlocks = computeUnconditionalBlocks(fn);
let activeManualMemoId: number | null = null;
const errors = new CompilerError();
@@ -95,7 +92,7 @@ function validateNoSetStateInRenderImpl(
validateNoSetStateInRenderImpl(
instr.value.loweredFunc.func,
unconditionalSetStateFunctions,
).hasAnyErrors()
).isErr()
) {
// This function expression unconditionally calls a setState
unconditionalSetStateFunctions.add(instr.lvalue.identifier.id);
@@ -186,5 +183,5 @@ function validateNoSetStateInRenderImpl(
}
}
return errors;
return errors.asResult();
}

View File

@@ -37,6 +37,7 @@ import {
ReactiveFunctionVisitor,
visitReactiveFunction,
} from '../ReactiveScopes/visitors';
import {Result} from '../Utils/Result';
import {getOrInsertDefault} from '../Utils/utils';
/**
@@ -46,15 +47,15 @@ import {getOrInsertDefault} from '../Utils/utils';
* This can occur if a value's mutable range somehow extended to include a hook and
* was pruned.
*/
export function validatePreservedManualMemoization(fn: ReactiveFunction): void {
export function validatePreservedManualMemoization(
fn: ReactiveFunction,
): Result<void, CompilerError> {
const state = {
errors: new CompilerError(),
manualMemoState: null,
};
visitReactiveFunction(fn, new Visitor(), state);
for (const detail of state.errors.details) {
fn.env.recordError(detail);
}
return state.errors.asResult();
}
const DEBUG = false;

View File

@@ -9,7 +9,7 @@ import {NodePath} from '@babel/traverse';
import * as t from '@babel/types';
import {CompilerDiagnostic, CompilerError, ErrorCategory} from '..';
import {CodegenFunction} from '../ReactiveScopes';
import {Environment} from '../HIR/Environment';
import {Result} from '../Utils/Result';
/**
* IMPORTANT: This validation is only intended for use in unit tests.
@@ -123,8 +123,7 @@ export function validateSourceLocations(
t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
>,
generatedAst: CodegenFunction,
env: Environment,
): void {
): Result<void, CompilerError> {
const errors = new CompilerError();
/*
@@ -310,7 +309,5 @@ export function validateSourceLocations(
}
}
for (const detail of errors.details) {
env.recordError(detail);
}
return errors.asResult();
}

View File

@@ -20,8 +20,9 @@ import {
eachInstructionValueOperand,
eachTerminalOperand,
} from '../HIR/visitors';
import {Result} from '../Utils/Result';
export function validateUseMemo(fn: HIRFunction): void {
export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
const errors = new CompilerError();
const voidMemoErrors = new CompilerError();
const useMemos = new Set<IdentifierId>();
@@ -176,9 +177,7 @@ export function validateUseMemo(fn: HIRFunction): void {
}
}
fn.env.logErrors(voidMemoErrors.asResult());
if (errors.hasAnyErrors()) {
fn.env.recordErrors(errors);
}
return errors.asResult();
}
function validateNoContextVariableAssignment(

View File

@@ -1,60 +0,0 @@
## Input
```javascript
// @validateRefAccessDuringRender
/**
* This fixture tests fault tolerance: the compiler should report
* multiple independent errors rather than stopping at the first one.
*
* Error 1: Ref access during render (ref.current)
* Error 2: Mutation of frozen value (props)
*/
function Component(props) {
const ref = useRef(null);
// Error: reading ref during render
const value = ref.current;
// Error: mutating frozen value (props, which is frozen after hook call)
props.items = [];
return <div>{value}</div>;
}
```
## Error
```
Found 2 errors:
Error: This value cannot be modified
Modifying component props or hook arguments is not allowed. Consider using a local variable instead.
error.fault-tolerance-reports-multiple-errors.ts:16:2
14 |
15 | // Error: mutating frozen value (props, which is frozen after hook call)
> 16 | props.items = [];
| ^^^^^ value cannot be modified
17 |
18 | return <div>{value}</div>;
19 | }
Error: Cannot access refs during render
React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).
error.fault-tolerance-reports-multiple-errors.ts:13:16
11 |
12 | // Error: reading ref during render
> 13 | const value = ref.current;
| ^^^^^^^^^^^ Cannot access ref value during render
14 |
15 | // Error: mutating frozen value (props, which is frozen after hook call)
16 | props.items = [];
```

View File

@@ -1,19 +0,0 @@
// @validateRefAccessDuringRender
/**
* This fixture tests fault tolerance: the compiler should report
* multiple independent errors rather than stopping at the first one.
*
* Error 1: Ref access during render (ref.current)
* Error 2: Mutation of frozen value (props)
*/
function Component(props) {
const ref = useRef(null);
// Error: reading ref during render
const value = ref.current;
// Error: mutating frozen value (props, which is frozen after hook call)
props.items = [];
return <div>{value}</div>;
}

View File

@@ -29,7 +29,7 @@ export const FIXTURE_ENTRYPOINT = {
## Error
```
Found 2 errors:
Found 1 error:
Error: This value cannot be modified
@@ -43,32 +43,6 @@ error.hook-call-freezes-captured-memberexpr.ts:13:2
14 | return <Stringify x={x} cb={cb} />;
15 | }
16 |
Error: Cannot modify local variables after render completes
This argument is a function which may reassign or mutate `x` after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead.
error.hook-call-freezes-captured-memberexpr.ts:9:25
7 | * After this custom hook call, it's no longer valid to mutate x.
8 | */
> 9 | const cb = useIdentity(() => {
| ^^^^^^^
> 10 | x.value++;
| ^^^^^^^^^^^^^^
> 11 | });
| ^^^^ This function may (indirectly) reassign or modify `x` after render
12 |
13 | x.value += count;
14 | return <Stringify x={x} cb={cb} />;
error.hook-call-freezes-captured-memberexpr.ts:10:4
8 | */
9 | const cb = useIdentity(() => {
> 10 | x.value++;
| ^ This modifies `x`
11 | });
12 |
13 | x.value += count;
```

View File

@@ -15,7 +15,7 @@ function component(a, b) {
## Error
```
Found 3 errors:
Found 1 error:
Error: useMemo() callbacks may not be async or generator functions
@@ -32,37 +32,6 @@ error.invalid-ReactUseMemo-async-callback.ts:2:24
5 | return x;
6 | }
7 |
Error: Found missing memoization dependencies
Missing dependencies can cause a value to update less often than it should, resulting in stale UI.
error.invalid-ReactUseMemo-async-callback.ts:3:10
1 | function component(a, b) {
2 | let x = React.useMemo(async () => {
> 3 | await a;
| ^ Missing dependency `a`
4 | }, []);
5 | return x;
6 | }
Inferred dependencies: `[a]`
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `a`, but the source dependencies were []. Inferred dependency not present in source.
error.invalid-ReactUseMemo-async-callback.ts:2:24
1 | function component(a, b) {
> 2 | let x = React.useMemo(async () => {
| ^^^^^^^^^^^^^
> 3 | await a;
| ^^^^^^^^^^^^
> 4 | }, []);
| ^^^^ Could not preserve existing manual memoization
5 | return x;
6 | }
7 |
```

View File

@@ -22,7 +22,7 @@ function Component({item, cond}) {
## Error
```
Found 3 errors:
Found 2 errors:
Error: Calling setState from useMemo may trigger an infinite loop
@@ -49,39 +49,6 @@ error.invalid-conditional-setState-in-useMemo.ts:8:6
9 | }
10 | }, [cond, key, init]);
11 |
Error: Found missing/extra memoization dependencies
Missing dependencies can cause a value to update less often than it should, resulting in stale UI. Extra dependencies can cause a value to update more often than it should, resulting in performance problems such as excessive renders or effects firing too often.
error.invalid-conditional-setState-in-useMemo.ts:7:18
5 | useMemo(() => {
6 | if (cond) {
> 7 | setPrevItem(item);
| ^^^^ Missing dependency `item`
8 | setState(0);
9 | }
10 | }, [cond, key, init]);
error.invalid-conditional-setState-in-useMemo.ts:10:12
8 | setState(0);
9 | }
> 10 | }, [cond, key, init]);
| ^^^ Unnecessary dependency `key`. Values declared outside of a component/hook should not be listed as dependencies as the component will not re-render if they change
11 |
12 | return state;
13 | }
error.invalid-conditional-setState-in-useMemo.ts:10:17
8 | setState(0);
9 | }
> 10 | }, [cond, key, init]);
| ^^^^ Unnecessary dependency `init`. Values declared outside of a component/hook should not be listed as dependencies as the component will not re-render if they change
11 |
12 | return state;
13 | }
Inferred dependencies: `[cond, item]`
```

View File

@@ -16,7 +16,7 @@ function useInvalidMutation(options) {
## Error
```
Found 2 errors:
Found 1 error:
Error: This value cannot be modified
@@ -30,27 +30,6 @@ error.invalid-mutation-in-closure.ts:4:4
5 | }
6 | return test;
7 | }
Error: Cannot modify local variables after render completes
This argument is a function which may reassign or mutate `options` after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead.
error.invalid-mutation-in-closure.ts:6:9
4 | options.foo = 'bar';
5 | }
> 6 | return test;
| ^^^^ This function may (indirectly) reassign or modify `options` after render
7 | }
8 |
error.invalid-mutation-in-closure.ts:4:4
2 | function test() {
3 | foo(options.foo); // error should not point on this line
> 4 | options.foo = 'bar';
| ^^^^^^^ This modifies `options`
5 | }
6 | return test;
7 | }
```

View File

@@ -15,7 +15,7 @@ function useFoo() {
## Error
```
Found 2 errors:
Found 1 error:
Error: Cannot reassign variable after render completes
@@ -29,31 +29,6 @@ error.invalid-reassign-local-in-hook-return-value.ts:4:4
5 | };
6 | }
7 |
Error: Cannot modify local variables after render completes
This argument is a function which may reassign or mutate `x` after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead.
error.invalid-reassign-local-in-hook-return-value.ts:3:9
1 | function useFoo() {
2 | let x = 0;
> 3 | return value => {
| ^^^^^^^^^^
> 4 | x = value;
| ^^^^^^^^^^^^^^
> 5 | };
| ^^^^ This function may (indirectly) reassign or modify `x` after render
6 | }
7 |
error.invalid-reassign-local-in-hook-return-value.ts:4:4
2 | let x = 0;
3 | return value => {
> 4 | x = value;
| ^ This modifies `x`
5 | };
6 | }
7 |
```

View File

@@ -47,7 +47,7 @@ function Component() {
## Error
```
Found 2 errors:
Found 1 error:
Error: Cannot reassign variable after render completes
@@ -61,32 +61,6 @@ error.invalid-reassign-local-variable-in-effect.ts:7:4
8 | };
9 |
10 | const onMount = newValue => {
Error: Cannot modify local variables after render completes
This argument is a function which may reassign or mutate `local` after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead.
error.invalid-reassign-local-variable-in-effect.ts:33:12
31 | };
32 |
> 33 | useEffect(() => {
| ^^^^^^^
> 34 | onMount();
| ^^^^^^^^^^^^^^
> 35 | }, [onMount]);
| ^^^^ This function may (indirectly) reassign or modify `local` after render
36 |
37 | return 'ok';
38 | }
error.invalid-reassign-local-variable-in-effect.ts:7:4
5 |
6 | const reassignLocal = newValue => {
> 7 | local = newValue;
| ^^^^^ This modifies `local`
8 | };
9 |
10 | const onMount = newValue => {
```

View File

@@ -48,7 +48,7 @@ function Component() {
## Error
```
Found 2 errors:
Found 1 error:
Error: Cannot reassign variable after render completes
@@ -62,32 +62,6 @@ error.invalid-reassign-local-variable-in-hook-argument.ts:8:4
9 | };
10 |
11 | const callback = newValue => {
Error: Cannot modify local variables after render completes
This argument is a function which may reassign or mutate `local` after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead.
error.invalid-reassign-local-variable-in-hook-argument.ts:34:14
32 | };
33 |
> 34 | useIdentity(() => {
| ^^^^^^^
> 35 | callback();
| ^^^^^^^^^^^^^^^
> 36 | });
| ^^^^ This function may (indirectly) reassign or modify `local` after render
37 |
38 | return 'ok';
39 | }
error.invalid-reassign-local-variable-in-hook-argument.ts:8:4
6 |
7 | const reassignLocal = newValue => {
> 8 | local = newValue;
| ^^^^^ This modifies `local`
9 | };
10 |
11 | const callback = newValue => {
```

View File

@@ -41,7 +41,7 @@ function Component() {
## Error
```
Found 2 errors:
Found 1 error:
Error: Cannot reassign variable after render completes
@@ -55,27 +55,6 @@ error.invalid-reassign-local-variable-in-jsx-callback.ts:5:4
6 | };
7 |
8 | const onClick = newValue => {
Error: Cannot modify local variables after render completes
This argument is a function which may reassign or mutate `local` after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead.
error.invalid-reassign-local-variable-in-jsx-callback.ts:31:26
29 | };
30 |
> 31 | return <button onClick={onClick}>Submit</button>;
| ^^^^^^^ This function may (indirectly) reassign or modify `local` after render
32 | }
33 |
error.invalid-reassign-local-variable-in-jsx-callback.ts:5:4
3 |
4 | const reassignLocal = newValue => {
> 5 | local = newValue;
| ^^^^^ This modifies `local`
6 | };
7 |
8 | const onClick = newValue => {
```

View File

@@ -26,7 +26,7 @@ function useKeyedState({key, init}) {
## Error
```
Found 3 errors:
Found 1 error:
Error: Calling setState from useMemo may trigger an infinite loop
@@ -40,61 +40,6 @@ error.invalid-setState-in-useMemo-indirect-useCallback.ts:13:4
14 | }, [key, init]);
15 |
16 | return state;
Error: Found missing memoization dependencies
Missing dependencies can cause a value to update less often than it should, resulting in stale UI.
error.invalid-setState-in-useMemo-indirect-useCallback.ts:9:13
7 | const fn = useCallback(() => {
8 | setPrevKey(key);
> 9 | setState(init);
| ^^^^ Missing dependency `init`
10 | });
11 |
12 | useMemo(() => {
error.invalid-setState-in-useMemo-indirect-useCallback.ts:8:15
6 |
7 | const fn = useCallback(() => {
> 8 | setPrevKey(key);
| ^^^ Missing dependency `key`
9 | setState(init);
10 | });
11 |
Error: Found missing/extra memoization dependencies
Missing dependencies can cause a value to update less often than it should, resulting in stale UI. Extra dependencies can cause a value to update more often than it should, resulting in performance problems such as excessive renders or effects firing too often.
error.invalid-setState-in-useMemo-indirect-useCallback.ts:13:4
11 |
12 | useMemo(() => {
> 13 | fn();
| ^^ Missing dependency `fn`
14 | }, [key, init]);
15 |
16 | return state;
error.invalid-setState-in-useMemo-indirect-useCallback.ts:14:6
12 | useMemo(() => {
13 | fn();
> 14 | }, [key, init]);
| ^^^ Unnecessary dependency `key`
15 |
16 | return state;
17 | }
error.invalid-setState-in-useMemo-indirect-useCallback.ts:14:11
12 | useMemo(() => {
13 | fn();
> 14 | }, [key, init]);
| ^^^^ Unnecessary dependency `init`
15 |
16 | return state;
17 | }
Inferred dependencies: `[fn]`
```

View File

@@ -15,7 +15,7 @@ function component(a, b) {
## Error
```
Found 3 errors:
Found 1 error:
Error: useMemo() callbacks may not be async or generator functions
@@ -32,37 +32,6 @@ error.invalid-useMemo-async-callback.ts:2:18
5 | return x;
6 | }
7 |
Error: Found missing memoization dependencies
Missing dependencies can cause a value to update less often than it should, resulting in stale UI.
error.invalid-useMemo-async-callback.ts:3:10
1 | function component(a, b) {
2 | let x = useMemo(async () => {
> 3 | await a;
| ^ Missing dependency `a`
4 | }, []);
5 | return x;
6 | }
Inferred dependencies: `[a]`
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `a`, but the source dependencies were []. Inferred dependency not present in source.
error.invalid-useMemo-async-callback.ts:2:18
1 | function component(a, b) {
> 2 | let x = useMemo(async () => {
| ^^^^^^^^^^^^^
> 3 | await a;
| ^^^^^^^^^^^^
> 4 | }, []);
| ^^^^ Could not preserve existing manual memoization
5 | return x;
6 | }
7 |
```

View File

@@ -13,7 +13,7 @@ function component(a, b) {
## Error
```
Found 3 errors:
Found 1 error:
Error: useMemo() callbacks may not accept parameters
@@ -26,32 +26,6 @@ error.invalid-useMemo-callback-args.ts:2:18
3 | return x;
4 | }
5 |
Error: Found missing memoization dependencies
Missing dependencies can cause a value to update less often than it should, resulting in stale UI.
error.invalid-useMemo-callback-args.ts:2:23
1 | function component(a, b) {
> 2 | let x = useMemo(c => a, []);
| ^ Missing dependency `a`
3 | return x;
4 | }
5 |
Inferred dependencies: `[a]`
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `a`, but the source dependencies were []. Inferred dependency not present in source.
error.invalid-useMemo-callback-args.ts:2:18
1 | function component(a, b) {
> 2 | let x = useMemo(c => a, []);
| ^^^^^^ Could not preserve existing manual memoization
3 | return x;
4 | }
5 |
```

View File

@@ -32,7 +32,7 @@ export const FIXTURE_ENTRYPOINT = {
## Error
```
Found 2 errors:
Found 1 error:
Error: Cannot reassign variable after render completes
@@ -46,28 +46,6 @@ error.mutable-range-shared-inner-outer-function.ts:8:6
9 | b = [];
10 | } else {
11 | a = {};
Error: Cannot modify local variables after render completes
This argument is a function which may reassign or mutate `a` after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead.
error.mutable-range-shared-inner-outer-function.ts:17:23
15 | b.push(false);
16 | };
> 17 | return <div onClick={f} />;
| ^ This function may (indirectly) reassign or modify `a` after render
18 | }
19 |
20 | export const FIXTURE_ENTRYPOINT = {
error.mutable-range-shared-inner-outer-function.ts:8:6
6 | const f = () => {
7 | if (cond) {
> 8 | a = {};
| ^ This modifies `a`
9 | b = [];
10 | } else {
11 | a = {};
```

View File

@@ -29,7 +29,7 @@ function useHook(parentRef) {
## Error
```
Found 2 errors:
Found 1 error:
Error: This value cannot be modified
@@ -43,27 +43,6 @@ error.todo-allow-assigning-to-inferred-ref-prop-in-callback.ts:15:8
16 | }
17 | }
18 | };
Error: Cannot modify local variables after render completes
This argument is a function which may reassign or mutate `parentRef` after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead.
error.todo-allow-assigning-to-inferred-ref-prop-in-callback.ts:19:9
17 | }
18 | };
> 19 | return handler;
| ^^^^^^^ This function may (indirectly) reassign or modify `parentRef` after render
20 | }
21 |
error.todo-allow-assigning-to-inferred-ref-prop-in-callback.ts:15:8
13 | } else {
14 | // So this assignment fails since we don't know its a ref
> 15 | parentRef.current = instance;
| ^^^^^^^^^ This modifies `parentRef`
16 | }
17 | }
18 | };
```

View File

@@ -17,7 +17,7 @@ function Component() {
## Error
```
Found 2 errors:
Found 1 error:
Error: Cannot reassign variable after render completes
@@ -31,27 +31,6 @@ error.todo-function-expression-references-later-variable-declaration.ts:3:4
4 | };
5 | let onClick;
6 |
Error: Cannot modify local variables after render completes
This argument is a function which may reassign or mutate `onClick` after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead.
error.todo-function-expression-references-later-variable-declaration.ts:7:23
5 | let onClick;
6 |
> 7 | return <div onClick={callback} />;
| ^^^^^^^^ This function may (indirectly) reassign or modify `onClick` after render
8 | }
9 |
error.todo-function-expression-references-later-variable-declaration.ts:3:4
1 | function Component() {
2 | let callback = () => {
> 3 | onClick = () => {};
| ^^^^^^^ This modifies `onClick`
4 | };
5 | let onClick;
6 |
```

View File

@@ -21,7 +21,7 @@ function Component({foo}) {
## Error
```
Found 3 errors:
Found 1 error:
Todo: Support destructuring of context variables
@@ -29,34 +29,10 @@ error.todo-reassign-const.ts:3:20
1 | import {Stringify} from 'shared-runtime';
2 |
> 3 | function Component({foo}) {
| ^^^
| ^^^ Support destructuring of context variables
4 | let bar = foo.bar;
5 | return (
6 | <Stringify
Todo: Support destructuring of context variables
error.todo-reassign-const.ts:3:20
1 | import {Stringify} from 'shared-runtime';
2 |
> 3 | function Component({foo}) {
| ^^^
4 | let bar = foo.bar;
5 | return (
6 | <Stringify
Error: This value cannot be modified
Modifying component props or hook arguments is not allowed. Consider using a local variable instead.
error.todo-reassign-const.ts:8:8
6 | <Stringify
7 | handler={() => {
> 8 | foo = true;
| ^^^ `foo` cannot be modified
9 | }}
10 | />
11 | );
```

View File

@@ -51,7 +51,7 @@ function Component({x, y, z}) {
## Error
```
Found 6 errors:
Found 4 errors:
Error: Found missing/extra memoization dependencies
@@ -157,48 +157,6 @@ error.invalid-exhaustive-deps.ts:37:13
40 | }, []);
Inferred dependencies: `[ref]`
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `x.y.z.a.b`, but the source dependencies were [x?.y.z.a?.b.z]. Inferred different dependency than source.
error.invalid-exhaustive-deps.ts:14:20
12 | // ok, not our job to type check nullability
13 | }, [x.y.z.a]);
> 14 | const c = useMemo(() => {
| ^^^^^^^
> 15 | return x?.y.z.a?.b;
| ^^^^^^^^^^^^^^^^^^^^^^^
> 16 | // error: too precise
| ^^^^^^^^^^^^^^^^^^^^^^^
> 17 | }, [x?.y.z.a?.b.z]);
| ^^^^ Could not preserve existing manual memoization
18 | const d = useMemo(() => {
19 | return x?.y?.[(console.log(y), z?.b)];
20 | // ok
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `ref`, but the source dependencies were []. Inferred dependency not present in source.
error.invalid-exhaustive-deps.ts:35:21
33 | const ref2 = useRef(null);
34 | const ref = z ? ref1 : ref2;
> 35 | const cb = useMemo(() => {
| ^^^^^^^
> 36 | return () => {
| ^^^^^^^^^^^^^^^^^^
> 37 | return ref.current;
| ^^^^^^^^^^^^^^^^^^
> 38 | };
| ^^^^^^^^^^^^^^^^^^
> 39 | // error: ref is a stable type but reactive
| ^^^^^^^^^^^^^^^^^^
> 40 | }, []);
| ^^^^ Could not preserve existing manual memoization
41 | return <Stringify results={[a, b, c, d, e, f, cb]} />;
42 | }
43 |
```

View File

@@ -22,7 +22,7 @@ function useHook() {
## Error
```
Found 2 errors:
Found 1 error:
Error: Found missing memoization dependencies
@@ -38,19 +38,6 @@ error.invalid-missing-nonreactive-dep-unmemoized.ts:11:31
14 |
Inferred dependencies: `[object]`
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `object`, but the source dependencies were []. Inferred dependency not present in source.
error.invalid-missing-nonreactive-dep-unmemoized.ts:11:24
9 | useIdentity();
10 | object.x = 0;
> 11 | const array = useMemo(() => [object], []);
| ^^^^^^^^^^^^^^ Could not preserve existing manual memoization
12 | return array;
13 | }
14 |
```

View File

@@ -42,7 +42,7 @@ function Component() {
## Error
```
Found 2 errors:
Found 1 error:
Error: Cannot reassign variable after render completes
@@ -56,27 +56,6 @@ error.invalid-reassign-local-variable-in-jsx-callback.ts:6:4
7 | };
8 |
9 | const onClick = newValue => {
Error: Cannot modify local variables after render completes
This argument is a function which may reassign or mutate `local` after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead.
error.invalid-reassign-local-variable-in-jsx-callback.ts:32:26
30 | };
31 |
> 32 | return <button onClick={onClick}>Submit</button>;
| ^^^^^^^ This function may (indirectly) reassign or modify `local` after render
33 | }
34 |
error.invalid-reassign-local-variable-in-jsx-callback.ts:6:4
4 |
5 | const reassignLocal = newValue => {
> 6 | local = newValue;
| ^^^^^ This modifies `local`
7 | };
8 |
9 | const onClick = newValue => {
```

View File

@@ -31,7 +31,7 @@ function Component({content, refetch}) {
## Error
```
Found 2 errors:
Found 1 error:
Error: Cannot access variable before it is declared
@@ -52,18 +52,6 @@ Error: Cannot access variable before it is declared
20 |
21 | return <Foo data={data} onSubmit={onSubmit} />;
22 | }
Error: Found missing memoization dependencies
Missing dependencies can cause a value to update less often than it should, resulting in stale UI.
9 | // TDZ violation!
10 | const onRefetch = useCallback(() => {
> 11 | refetch(data);
| ^^^^ Missing dependency `data`
12 | }, [refetch]);
13 |
14 | // The context variable gets frozen here since it's passed to a hook
```

View File

@@ -30,7 +30,7 @@ function useFoo(input1) {
## Error
```
Found 2 errors:
Found 1 error:
Error: Found missing memoization dependencies
@@ -46,23 +46,6 @@ error.useMemo-unrelated-mutation-in-depslist.ts:18:14
21 | }
Inferred dependencies: `[x, y]`
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `input1`, but the source dependencies were [y]. Inferred different dependency than source.
error.useMemo-unrelated-mutation-in-depslist.ts:16:27
14 | const x = {};
15 | const y = [input1];
> 16 | const memoized = useMemo(() => {
| ^^^^^^^
> 17 | return [y];
| ^^^^^^^^^^^^^^^
> 18 | }, [(mutate(x), y)]);
| ^^^^ Could not preserve existing manual memoization
19 |
20 | return [x, memoized];
21 | }
```

View File

@@ -16,24 +16,29 @@ function Component(props) {
## Error
```
Found 1 error:
Found 2 errors:
Invariant: Unexpected empty block with `goto` terminal
Error: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)
Block bb5 is empty.
error.invalid-hook-for.ts:4:9
2 | let i = 0;
3 | for (let x = 0; useHook(x) < 10; useHook(i), x++) {
> 4 | i += useHook(x);
| ^^^^^^^ Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)
5 | }
6 | return i;
7 | }
error.invalid-hook-for.ts:3:2
Error: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)
error.invalid-hook-for.ts:3:35
1 | function Component(props) {
2 | let i = 0;
> 3 | for (let x = 0; useHook(x) < 10; useHook(i), x++) {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 4 | i += useHook(x);
| ^^^^^^^^^^^^^^^^^^^^
> 5 | }
| ^^^^ Unexpected empty block with `goto` terminal
| ^^^^^^^ Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)
4 | i += useHook(x);
5 | }
6 | return i;
7 | }
8 |
```

View File

@@ -25,7 +25,6 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { useRef } from "react";
const useControllableState = (options) => {};

View File

@@ -64,9 +64,6 @@ testRule(
makeTestCaseError(
'Capitalized functions are reserved for components',
),
makeTestCaseError(
'Capitalized functions are reserved for components',
),
],
},
],

View File

@@ -57,6 +57,7 @@ testRule('plugin-recommended', TestRecommendedRules, {
],
invalid: [
{
// TODO: actually return multiple diagnostics in this case
name: 'Multiple diagnostic kinds from the same function are surfaced',
code: normalizeIndent`
import Child from './Child';
@@ -69,7 +70,6 @@ testRule('plugin-recommended', TestRecommendedRules, {
`,
errors: [
makeTestCaseError('Hooks must always be called in a consistent order'),
makeTestCaseError('Capitalized functions are reserved for components'),
],
},
{
@@ -128,7 +128,6 @@ testRule('plugin-recommended', TestRecommendedRules, {
makeTestCaseError(
'Calling setState from useMemo may trigger an infinite loop',
),
makeTestCaseError('Found extra memoization dependencies'),
],
},
],

View File

@@ -3991,23 +3991,9 @@ function attemptEarlyBailoutIfNoScheduledUpdate(
// whether to retry the primary children, or to skip over it and
// go straight to the fallback. Check the priority of the primary
// child fragment.
//
// Propagate context changes first. If a parent context changed
// and the primary children's consumer fibers were discarded
// during initial mount suspension, normal propagation can't find
// them. In that case we conservatively retry the boundary — the
// re-mounted children will read the updated context value.
const contextChanged = lazilyPropagateParentContextChanges(
current,
workInProgress,
renderLanes,
);
const primaryChildFragment: Fiber = (workInProgress.child: any);
const primaryChildLanes = primaryChildFragment.childLanes;
if (
contextChanged ||
includesSomeLane(renderLanes, primaryChildLanes)
) {
if (includesSomeLane(renderLanes, primaryChildLanes)) {
// The primary children have pending work. Use the normal path
// to attempt to render the primary children again.
return updateSuspenseComponent(current, workInProgress, renderLanes);

View File

@@ -20,11 +20,7 @@ import type {Hook} from './ReactFiberHooks';
import {isPrimaryRenderer, HostTransitionContext} from './ReactFiberConfig';
import {createCursor, push, pop} from './ReactFiberStack';
import {
ContextProvider,
DehydratedFragment,
SuspenseComponent,
} from './ReactWorkTags';
import {ContextProvider, DehydratedFragment} from './ReactWorkTags';
import {NoLanes, isSubsetOfLanes, mergeLanes} from './ReactFiberLane';
import {
NoFlags,
@@ -299,37 +295,6 @@ function propagateContextChanges<T>(
workInProgress,
);
nextFiber = null;
} else if (
fiber.tag === SuspenseComponent &&
fiber.memoizedState !== null &&
fiber.memoizedState.dehydrated === null
) {
// This is a client-rendered Suspense boundary that is currently
// showing its fallback. The primary children may include context
// consumers, but their fibers may not exist in the tree — during
// initial mount, if the primary children suspended, their fibers
// were discarded since there was no current tree to preserve them.
// We can't walk into the primary tree to find consumers, so
// conservatively mark the Suspense boundary itself for retry.
// When it re-renders, it will re-mount the primary children,
// which will read the updated context value.
fiber.lanes = mergeLanes(fiber.lanes, renderLanes);
const alternate = fiber.alternate;
if (alternate !== null) {
alternate.lanes = mergeLanes(alternate.lanes, renderLanes);
}
scheduleContextWorkOnParentPath(
fiber.return,
renderLanes,
workInProgress,
);
if (!forcePropagateEntireTree) {
// During lazy propagation, we can defer propagating changes to
// the children, same as the consumer match above.
nextFiber = null;
} else {
nextFiber = fiber.child;
}
} else {
// Traverse down.
nextFiber = fiber.child;
@@ -366,9 +331,9 @@ export function lazilyPropagateParentContextChanges(
current: Fiber,
workInProgress: Fiber,
renderLanes: Lanes,
): boolean {
) {
const forcePropagateEntireTree = false;
return propagateParentContextChanges(
propagateParentContextChanges(
current,
workInProgress,
renderLanes,
@@ -399,7 +364,7 @@ function propagateParentContextChanges(
workInProgress: Fiber,
renderLanes: Lanes,
forcePropagateEntireTree: boolean,
): boolean {
) {
// Collect all the parent providers that changed. Since this is usually small
// number, we use an Array instead of Set.
let contexts = null;
@@ -495,7 +460,6 @@ function propagateParentContextChanges(
// then we could remove both `DidPropagateContext` and `NeedsPropagation`.
// Consider this as part of the next refactor to the fiber tree structure.
workInProgress.flags |= DidPropagateContext;
return contexts !== null;
}
export function checkIfContextChanged(

View File

@@ -2,7 +2,6 @@ let React;
let ReactNoop;
let Scheduler;
let act;
let use;
let useState;
let useContext;
let Suspense;
@@ -20,7 +19,6 @@ describe('ReactLazyContextPropagation', () => {
ReactNoop = require('react-noop-renderer');
Scheduler = require('scheduler');
act = require('internal-test-utils').act;
use = React.use;
useState = React.useState;
useContext = React.useContext;
Suspense = React.Suspense;
@@ -939,102 +937,4 @@ describe('ReactLazyContextPropagation', () => {
assertLog(['B', 'B']);
expect(root).toMatchRenderedOutput('BB');
});
it('regression: context change triggers retry of suspended Suspense boundary on initial mount', async () => {
// Regression test for a bug where a context change above a suspended
// Suspense boundary would fail to trigger a retry. When a Suspense
// boundary suspends during initial mount, the primary children's fibers
// are discarded because there is no current tree to preserve them. If
// the suspended promise never resolves, the only way to retry is
// something external — like a context change. Context propagation must
// mark suspended Suspense boundaries for retry even though the consumer
// fibers no longer exist in the tree.
//
// The Provider component owns the state update. The children are
// passed in from above, so they are not re-created when the Provider
// re-renders — this means the Suspense boundary bails out, exercising
// the lazy context propagation path where the bug manifests.
const Context = React.createContext(null);
const neverResolvingPromise = new Promise(() => {});
const resolvedThenable = {status: 'fulfilled', value: 'Result', then() {}};
function Consumer() {
return <Text text={use(use(Context))} />;
}
let setPromise;
function Provider({children}) {
const [promise, _setPromise] = useState(neverResolvingPromise);
setPromise = _setPromise;
return <Context.Provider value={promise}>{children}</Context.Provider>;
}
const root = ReactNoop.createRoot();
await act(() => {
root.render(
<Provider>
<Suspense fallback={<Text text="Loading" />}>
<Consumer />
</Suspense>
</Provider>,
);
});
assertLog(['Loading']);
expect(root).toMatchRenderedOutput('Loading');
await act(() => {
setPromise(resolvedThenable);
});
assertLog(['Result']);
expect(root).toMatchRenderedOutput('Result');
});
it('regression: context change triggers retry of suspended Suspense boundary on initial mount (nested)', async () => {
// Same as above, but with an additional indirection component between
// the provider and the Suspense boundary. This exercises the
// propagateContextChanges walker path rather than the
// propagateParentContextChanges path.
const Context = React.createContext(null);
const neverResolvingPromise = new Promise(() => {});
const resolvedThenable = {status: 'fulfilled', value: 'Result', then() {}};
function Consumer() {
return <Text text={use(use(Context))} />;
}
function Indirection({children}) {
Scheduler.log('Indirection');
return children;
}
let setPromise;
function Provider({children}) {
const [promise, _setPromise] = useState(neverResolvingPromise);
setPromise = _setPromise;
return <Context.Provider value={promise}>{children}</Context.Provider>;
}
const root = ReactNoop.createRoot();
await act(() => {
root.render(
<Provider>
<Indirection>
<Suspense fallback={<Text text="Loading" />}>
<Consumer />
</Suspense>
</Indirection>
</Provider>,
);
});
assertLog(['Indirection', 'Loading']);
expect(root).toMatchRenderedOutput('Loading');
// Indirection should not re-render — only the Suspense boundary
// should be retried.
await act(() => {
setPromise(resolvedThenable);
});
assertLog(['Result']);
expect(root).toMatchRenderedOutput('Result');
});
});