Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90e0b7b0ce | ||
|
|
e3e5d95cc4 | ||
|
|
426a394845 | ||
|
|
eca778cf8b | ||
|
|
0dbb43bc57 | ||
|
|
8b6b11f703 | ||
|
|
ab18f33d46 |
@@ -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.
|
||||
|
||||
- [ ] **4.1 `validateHooksUsage`** (`src/Validation/ValidateHooksUsage.ts`)
|
||||
- [x] **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()`
|
||||
|
||||
- [ ] **4.2 `validateNoCapitalizedCalls`** (`src/Validation/ValidateNoCapitalizedCalls.ts`)
|
||||
- [x] **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()`
|
||||
|
||||
- [ ] **4.3 `validateUseMemo`** (`src/Validation/ValidateUseMemo.ts`)
|
||||
- [x] **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()`
|
||||
|
||||
- [ ] **4.4 `dropManualMemoization`** (`src/Inference/DropManualMemoization.ts`)
|
||||
- [x] **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()`
|
||||
|
||||
- [ ] **4.5 `validateNoRefAccessInRender`** (`src/Validation/ValidateNoRefAccessInRender.ts`)
|
||||
- [x] **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()`
|
||||
|
||||
- [ ] **4.6 `validateNoSetStateInRender`** (`src/Validation/ValidateNoSetStateInRender.ts`)
|
||||
- [x] **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()`
|
||||
|
||||
- [ ] **4.7 `validateNoImpureFunctionsInRender`** (`src/Validation/ValidateNoImpureFunctionsInRender.ts`)
|
||||
- [x] **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()`
|
||||
|
||||
- [ ] **4.8 `validateNoFreezingKnownMutableFunctions`** (`src/Validation/ValidateNoFreezingKnownMutableFunctions.ts`)
|
||||
- [x] **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()`
|
||||
|
||||
- [ ] **4.9 `validateExhaustiveDependencies`** (`src/Validation/ValidateExhaustiveDependencies.ts`)
|
||||
- [x] **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()`
|
||||
|
||||
- [ ] **4.10 `validateMemoizedEffectDependencies`** (`src/Validation/ValidateMemoizedEffectDependencies.ts`)
|
||||
- [x] **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()`
|
||||
|
||||
- [ ] **4.11 `validatePreservedManualMemoization`** (`src/Validation/ValidatePreservedManualMemoization.ts`)
|
||||
- [x] **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()`
|
||||
|
||||
- [ ] **4.12 `validateSourceLocations`** (`src/Validation/ValidateSourceLocations.ts`)
|
||||
- [x] **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.
|
||||
|
||||
- [ ] **4.17 `validateContextVariableLValues`** (`src/Validation/ValidateContextVariableLValues.ts`)
|
||||
- [x] **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)
|
||||
|
||||
- [ ] **4.18 `validateLocalsNotReassignedAfterRender`** (`src/Validation/ValidateLocalsNotReassignedAfterRender.ts`)
|
||||
- [x] **4.18 `validateLocalsNotReassignedAfterRender`** (`src/Validation/ValidateLocalsNotReassignedAfterRender.ts`)
|
||||
- Currently constructs a `CompilerError` and `throw`s it directly
|
||||
- Change to record errors on env
|
||||
|
||||
- [ ] **4.19 `validateNoDerivedComputationsInEffects`** (`src/Validation/ValidateNoDerivedComputationsInEffects.ts`)
|
||||
- [x] **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.
|
||||
|
||||
- [ ] **5.1 `inferMutationAliasingEffects`** (`src/Inference/InferMutationAliasingEffects.ts`)
|
||||
- [x] **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
|
||||
|
||||
- [ ] **5.2 `inferMutationAliasingRanges`** (`src/Inference/InferMutationAliasingRanges.ts`)
|
||||
- [x] **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
|
||||
|
||||
- [ ] **6.1 `codegenFunction`** (`src/ReactiveScopes/CodegenReactiveFunction.ts`)
|
||||
- [x] **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:
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
injectReanimatedFlag,
|
||||
pipelineUsesReanimatedPlugin,
|
||||
} from '../Entrypoint/Reanimated';
|
||||
import validateNoUntransformedReferences from '../Entrypoint/ValidateNoUntransformedReferences';
|
||||
import {CompilerError} from '..';
|
||||
|
||||
const ENABLE_REACT_COMPILER_TIMINGS =
|
||||
@@ -64,19 +63,12 @@ export default function BabelPluginReactCompiler(
|
||||
},
|
||||
};
|
||||
}
|
||||
const result = compileProgram(prog, {
|
||||
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',
|
||||
|
||||
@@ -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 {BabelFn, getReactCompilerRuntimeModule} from './Program';
|
||||
import {getReactCompilerRuntimeModule} from './Program';
|
||||
import {SuppressionRange} from './Suppression';
|
||||
|
||||
export function validateRestrictedImports(
|
||||
@@ -84,11 +84,6 @@ 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,
|
||||
|
||||
@@ -228,8 +228,6 @@ 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',
|
||||
]);
|
||||
|
||||
@@ -92,7 +92,6 @@ 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';
|
||||
@@ -159,123 +158,79 @@ function runWithEnvironment(
|
||||
const hir = lower(func, env).unwrap();
|
||||
log({kind: 'hir', name: 'HIR', value: hir});
|
||||
|
||||
env.tryRecord(() => {
|
||||
pruneMaybeThrows(hir);
|
||||
});
|
||||
pruneMaybeThrows(hir);
|
||||
log({kind: 'hir', name: 'PruneMaybeThrows', value: hir});
|
||||
|
||||
env.tryRecord(() => {
|
||||
validateContextVariableLValues(hir);
|
||||
});
|
||||
env.tryRecord(() => {
|
||||
validateUseMemo(hir).unwrap();
|
||||
});
|
||||
validateContextVariableLValues(hir);
|
||||
validateUseMemo(hir);
|
||||
|
||||
if (env.enableDropManualMemoization) {
|
||||
env.tryRecord(() => {
|
||||
dropManualMemoization(hir).unwrap();
|
||||
});
|
||||
dropManualMemoization(hir);
|
||||
log({kind: 'hir', name: 'DropManualMemoization', value: hir});
|
||||
}
|
||||
|
||||
env.tryRecord(() => {
|
||||
inlineImmediatelyInvokedFunctionExpressions(hir);
|
||||
});
|
||||
inlineImmediatelyInvokedFunctionExpressions(hir);
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'InlineImmediatelyInvokedFunctionExpressions',
|
||||
value: hir,
|
||||
});
|
||||
|
||||
env.tryRecord(() => {
|
||||
mergeConsecutiveBlocks(hir);
|
||||
});
|
||||
mergeConsecutiveBlocks(hir);
|
||||
log({kind: 'hir', name: 'MergeConsecutiveBlocks', value: hir});
|
||||
|
||||
assertConsistentIdentifiers(hir);
|
||||
assertTerminalSuccessorsExist(hir);
|
||||
|
||||
env.tryRecord(() => {
|
||||
enterSSA(hir);
|
||||
});
|
||||
enterSSA(hir);
|
||||
log({kind: 'hir', name: 'SSA', value: hir});
|
||||
|
||||
env.tryRecord(() => {
|
||||
eliminateRedundantPhi(hir);
|
||||
});
|
||||
eliminateRedundantPhi(hir);
|
||||
log({kind: 'hir', name: 'EliminateRedundantPhi', value: hir});
|
||||
|
||||
assertConsistentIdentifiers(hir);
|
||||
|
||||
env.tryRecord(() => {
|
||||
constantPropagation(hir);
|
||||
});
|
||||
constantPropagation(hir);
|
||||
log({kind: 'hir', name: 'ConstantPropagation', value: hir});
|
||||
|
||||
env.tryRecord(() => {
|
||||
inferTypes(hir);
|
||||
});
|
||||
inferTypes(hir);
|
||||
log({kind: 'hir', name: 'InferTypes', value: hir});
|
||||
|
||||
if (env.enableValidations) {
|
||||
if (env.config.validateHooksUsage) {
|
||||
env.tryRecord(() => {
|
||||
validateHooksUsage(hir).unwrap();
|
||||
});
|
||||
validateHooksUsage(hir);
|
||||
}
|
||||
if (env.config.validateNoCapitalizedCalls) {
|
||||
env.tryRecord(() => {
|
||||
validateNoCapitalizedCalls(hir).unwrap();
|
||||
});
|
||||
validateNoCapitalizedCalls(hir);
|
||||
}
|
||||
}
|
||||
|
||||
env.tryRecord(() => {
|
||||
optimizePropsMethodCalls(hir);
|
||||
});
|
||||
optimizePropsMethodCalls(hir);
|
||||
log({kind: 'hir', name: 'OptimizePropsMethodCalls', value: hir});
|
||||
|
||||
env.tryRecord(() => {
|
||||
analyseFunctions(hir);
|
||||
});
|
||||
analyseFunctions(hir);
|
||||
log({kind: 'hir', name: 'AnalyseFunctions', value: hir});
|
||||
|
||||
const mutabilityAliasingErrors = inferMutationAliasingEffects(hir);
|
||||
inferMutationAliasingEffects(hir);
|
||||
log({kind: 'hir', name: 'InferMutationAliasingEffects', value: hir});
|
||||
if (env.enableValidations) {
|
||||
if (mutabilityAliasingErrors.isErr()) {
|
||||
env.recordErrors(mutabilityAliasingErrors.unwrapErr());
|
||||
}
|
||||
}
|
||||
|
||||
if (env.outputMode === 'ssr') {
|
||||
env.tryRecord(() => {
|
||||
optimizeForSSR(hir);
|
||||
});
|
||||
optimizeForSSR(hir);
|
||||
log({kind: 'hir', name: 'OptimizeForSSR', value: hir});
|
||||
}
|
||||
|
||||
// Note: Has to come after infer reference effects because "dead" code may still affect inference
|
||||
env.tryRecord(() => {
|
||||
deadCodeElimination(hir);
|
||||
});
|
||||
deadCodeElimination(hir);
|
||||
log({kind: 'hir', name: 'DeadCodeElimination', value: hir});
|
||||
env.tryRecord(() => {
|
||||
pruneMaybeThrows(hir);
|
||||
});
|
||||
pruneMaybeThrows(hir);
|
||||
log({kind: 'hir', name: 'PruneMaybeThrows', value: hir});
|
||||
|
||||
const mutabilityAliasingRangeErrors = inferMutationAliasingRanges(hir, {
|
||||
inferMutationAliasingRanges(hir, {
|
||||
isFunctionExpression: false,
|
||||
});
|
||||
log({kind: 'hir', name: 'InferMutationAliasingRanges', value: hir});
|
||||
if (env.enableValidations) {
|
||||
if (mutabilityAliasingRangeErrors.isErr()) {
|
||||
env.recordErrors(mutabilityAliasingRangeErrors.unwrapErr());
|
||||
}
|
||||
env.tryRecord(() => {
|
||||
validateLocalsNotReassignedAfterRender(hir);
|
||||
});
|
||||
validateLocalsNotReassignedAfterRender(hir);
|
||||
}
|
||||
|
||||
if (env.enableValidations) {
|
||||
@@ -284,15 +239,11 @@ function runWithEnvironment(
|
||||
}
|
||||
|
||||
if (env.config.validateRefAccessDuringRender) {
|
||||
env.tryRecord(() => {
|
||||
validateNoRefAccessInRender(hir).unwrap();
|
||||
});
|
||||
validateNoRefAccessInRender(hir);
|
||||
}
|
||||
|
||||
if (env.config.validateNoSetStateInRender) {
|
||||
env.tryRecord(() => {
|
||||
validateNoSetStateInRender(hir).unwrap();
|
||||
});
|
||||
validateNoSetStateInRender(hir);
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -301,9 +252,7 @@ function runWithEnvironment(
|
||||
) {
|
||||
env.logErrors(validateNoDerivedComputationsInEffects_exp(hir));
|
||||
} else if (env.config.validateNoDerivedComputationsInEffects) {
|
||||
env.tryRecord(() => {
|
||||
validateNoDerivedComputationsInEffects(hir);
|
||||
});
|
||||
validateNoDerivedComputationsInEffects(hir);
|
||||
}
|
||||
|
||||
if (env.config.validateNoSetStateInEffects && env.outputMode === 'lint') {
|
||||
@@ -314,20 +263,12 @@ function runWithEnvironment(
|
||||
env.logErrors(validateNoJSXInTryStatement(hir));
|
||||
}
|
||||
|
||||
if (env.config.validateNoImpureFunctionsInRender) {
|
||||
env.tryRecord(() => {
|
||||
validateNoImpureFunctionsInRender(hir).unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
env.tryRecord(() => {
|
||||
validateNoFreezingKnownMutableFunctions(hir).unwrap();
|
||||
validateNoFreezingKnownMutableFunctions(hir);
|
||||
});
|
||||
}
|
||||
|
||||
env.tryRecord(() => {
|
||||
inferReactivePlaces(hir);
|
||||
});
|
||||
inferReactivePlaces(hir);
|
||||
log({kind: 'hir', name: 'InferReactivePlaces', value: hir});
|
||||
|
||||
if (env.enableValidations) {
|
||||
@@ -336,15 +277,11 @@ function runWithEnvironment(
|
||||
env.config.validateExhaustiveEffectDependencies
|
||||
) {
|
||||
// NOTE: this relies on reactivity inference running first
|
||||
env.tryRecord(() => {
|
||||
validateExhaustiveDependencies(hir).unwrap();
|
||||
});
|
||||
validateExhaustiveDependencies(hir);
|
||||
}
|
||||
}
|
||||
|
||||
env.tryRecord(() => {
|
||||
rewriteInstructionKindsBasedOnReassignment(hir);
|
||||
});
|
||||
rewriteInstructionKindsBasedOnReassignment(hir);
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'RewriteInstructionKindsBasedOnReassignment',
|
||||
@@ -365,16 +302,12 @@ function runWithEnvironment(
|
||||
* if inferred memoization is enabled. This makes all later passes which
|
||||
* transform reactive-scope labeled instructions no-ops.
|
||||
*/
|
||||
env.tryRecord(() => {
|
||||
inferReactiveScopeVariables(hir);
|
||||
});
|
||||
inferReactiveScopeVariables(hir);
|
||||
log({kind: 'hir', name: 'InferReactiveScopeVariables', value: hir});
|
||||
}
|
||||
|
||||
let fbtOperands: Set<IdentifierId> = new Set();
|
||||
env.tryRecord(() => {
|
||||
fbtOperands = memoizeFbtAndMacroOperandsInSameScope(hir);
|
||||
});
|
||||
fbtOperands = memoizeFbtAndMacroOperandsInSameScope(hir);
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'MemoizeFbtAndMacroOperandsInSameScope',
|
||||
@@ -382,15 +315,11 @@ function runWithEnvironment(
|
||||
});
|
||||
|
||||
if (env.config.enableJsxOutlining) {
|
||||
env.tryRecord(() => {
|
||||
outlineJSX(hir);
|
||||
});
|
||||
outlineJSX(hir);
|
||||
}
|
||||
|
||||
if (env.config.enableNameAnonymousFunctions) {
|
||||
env.tryRecord(() => {
|
||||
nameAnonymousFunctions(hir);
|
||||
});
|
||||
nameAnonymousFunctions(hir);
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'NameAnonymousFunctions',
|
||||
@@ -399,51 +328,39 @@ function runWithEnvironment(
|
||||
}
|
||||
|
||||
if (env.config.enableFunctionOutlining) {
|
||||
env.tryRecord(() => {
|
||||
outlineFunctions(hir, fbtOperands);
|
||||
});
|
||||
outlineFunctions(hir, fbtOperands);
|
||||
log({kind: 'hir', name: 'OutlineFunctions', value: hir});
|
||||
}
|
||||
|
||||
env.tryRecord(() => {
|
||||
alignMethodCallScopes(hir);
|
||||
});
|
||||
alignMethodCallScopes(hir);
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'AlignMethodCallScopes',
|
||||
value: hir,
|
||||
});
|
||||
|
||||
env.tryRecord(() => {
|
||||
alignObjectMethodScopes(hir);
|
||||
});
|
||||
alignObjectMethodScopes(hir);
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'AlignObjectMethodScopes',
|
||||
value: hir,
|
||||
});
|
||||
|
||||
env.tryRecord(() => {
|
||||
pruneUnusedLabelsHIR(hir);
|
||||
});
|
||||
pruneUnusedLabelsHIR(hir);
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'PruneUnusedLabelsHIR',
|
||||
value: hir,
|
||||
});
|
||||
|
||||
env.tryRecord(() => {
|
||||
alignReactiveScopesToBlockScopesHIR(hir);
|
||||
});
|
||||
alignReactiveScopesToBlockScopesHIR(hir);
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'AlignReactiveScopesToBlockScopesHIR',
|
||||
value: hir,
|
||||
});
|
||||
|
||||
env.tryRecord(() => {
|
||||
mergeOverlappingReactiveScopesHIR(hir);
|
||||
});
|
||||
mergeOverlappingReactiveScopesHIR(hir);
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'MergeOverlappingReactiveScopesHIR',
|
||||
@@ -451,9 +368,7 @@ function runWithEnvironment(
|
||||
});
|
||||
assertValidBlockNesting(hir);
|
||||
|
||||
env.tryRecord(() => {
|
||||
buildReactiveScopeTerminalsHIR(hir);
|
||||
});
|
||||
buildReactiveScopeTerminalsHIR(hir);
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'BuildReactiveScopeTerminalsHIR',
|
||||
@@ -462,18 +377,14 @@ function runWithEnvironment(
|
||||
|
||||
assertValidBlockNesting(hir);
|
||||
|
||||
env.tryRecord(() => {
|
||||
flattenReactiveLoopsHIR(hir);
|
||||
});
|
||||
flattenReactiveLoopsHIR(hir);
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'FlattenReactiveLoopsHIR',
|
||||
value: hir,
|
||||
});
|
||||
|
||||
env.tryRecord(() => {
|
||||
flattenScopesWithHooksOrUseHIR(hir);
|
||||
});
|
||||
flattenScopesWithHooksOrUseHIR(hir);
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'FlattenScopesWithHooksOrUseHIR',
|
||||
@@ -481,9 +392,7 @@ function runWithEnvironment(
|
||||
});
|
||||
assertTerminalSuccessorsExist(hir);
|
||||
assertTerminalPredsExist(hir);
|
||||
env.tryRecord(() => {
|
||||
propagateScopeDependenciesHIR(hir);
|
||||
});
|
||||
propagateScopeDependenciesHIR(hir);
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'PropagateScopeDependenciesHIR',
|
||||
@@ -491,9 +400,7 @@ function runWithEnvironment(
|
||||
});
|
||||
|
||||
let reactiveFunction!: ReactiveFunction;
|
||||
env.tryRecord(() => {
|
||||
reactiveFunction = buildReactiveFunction(hir);
|
||||
});
|
||||
reactiveFunction = buildReactiveFunction(hir);
|
||||
log({
|
||||
kind: 'reactive',
|
||||
name: 'BuildReactiveFunction',
|
||||
@@ -502,9 +409,7 @@ function runWithEnvironment(
|
||||
|
||||
assertWellFormedBreakTargets(reactiveFunction);
|
||||
|
||||
env.tryRecord(() => {
|
||||
pruneUnusedLabels(reactiveFunction);
|
||||
});
|
||||
pruneUnusedLabels(reactiveFunction);
|
||||
log({
|
||||
kind: 'reactive',
|
||||
name: 'PruneUnusedLabels',
|
||||
@@ -512,90 +417,70 @@ function runWithEnvironment(
|
||||
});
|
||||
assertScopeInstructionsWithinScopes(reactiveFunction);
|
||||
|
||||
env.tryRecord(() => {
|
||||
pruneNonEscapingScopes(reactiveFunction);
|
||||
});
|
||||
pruneNonEscapingScopes(reactiveFunction);
|
||||
log({
|
||||
kind: 'reactive',
|
||||
name: 'PruneNonEscapingScopes',
|
||||
value: reactiveFunction,
|
||||
});
|
||||
|
||||
env.tryRecord(() => {
|
||||
pruneNonReactiveDependencies(reactiveFunction);
|
||||
});
|
||||
pruneNonReactiveDependencies(reactiveFunction);
|
||||
log({
|
||||
kind: 'reactive',
|
||||
name: 'PruneNonReactiveDependencies',
|
||||
value: reactiveFunction,
|
||||
});
|
||||
|
||||
env.tryRecord(() => {
|
||||
pruneUnusedScopes(reactiveFunction);
|
||||
});
|
||||
pruneUnusedScopes(reactiveFunction);
|
||||
log({
|
||||
kind: 'reactive',
|
||||
name: 'PruneUnusedScopes',
|
||||
value: reactiveFunction,
|
||||
});
|
||||
|
||||
env.tryRecord(() => {
|
||||
mergeReactiveScopesThatInvalidateTogether(reactiveFunction);
|
||||
});
|
||||
mergeReactiveScopesThatInvalidateTogether(reactiveFunction);
|
||||
log({
|
||||
kind: 'reactive',
|
||||
name: 'MergeReactiveScopesThatInvalidateTogether',
|
||||
value: reactiveFunction,
|
||||
});
|
||||
|
||||
env.tryRecord(() => {
|
||||
pruneAlwaysInvalidatingScopes(reactiveFunction);
|
||||
});
|
||||
pruneAlwaysInvalidatingScopes(reactiveFunction);
|
||||
log({
|
||||
kind: 'reactive',
|
||||
name: 'PruneAlwaysInvalidatingScopes',
|
||||
value: reactiveFunction,
|
||||
});
|
||||
|
||||
env.tryRecord(() => {
|
||||
propagateEarlyReturns(reactiveFunction);
|
||||
});
|
||||
propagateEarlyReturns(reactiveFunction);
|
||||
log({
|
||||
kind: 'reactive',
|
||||
name: 'PropagateEarlyReturns',
|
||||
value: reactiveFunction,
|
||||
});
|
||||
|
||||
env.tryRecord(() => {
|
||||
pruneUnusedLValues(reactiveFunction);
|
||||
});
|
||||
pruneUnusedLValues(reactiveFunction);
|
||||
log({
|
||||
kind: 'reactive',
|
||||
name: 'PruneUnusedLValues',
|
||||
value: reactiveFunction,
|
||||
});
|
||||
|
||||
env.tryRecord(() => {
|
||||
promoteUsedTemporaries(reactiveFunction);
|
||||
});
|
||||
promoteUsedTemporaries(reactiveFunction);
|
||||
log({
|
||||
kind: 'reactive',
|
||||
name: 'PromoteUsedTemporaries',
|
||||
value: reactiveFunction,
|
||||
});
|
||||
|
||||
env.tryRecord(() => {
|
||||
extractScopeDeclarationsFromDestructuring(reactiveFunction);
|
||||
});
|
||||
extractScopeDeclarationsFromDestructuring(reactiveFunction);
|
||||
log({
|
||||
kind: 'reactive',
|
||||
name: 'ExtractScopeDeclarationsFromDestructuring',
|
||||
value: reactiveFunction,
|
||||
});
|
||||
|
||||
env.tryRecord(() => {
|
||||
stabilizeBlockIds(reactiveFunction);
|
||||
});
|
||||
stabilizeBlockIds(reactiveFunction);
|
||||
log({
|
||||
kind: 'reactive',
|
||||
name: 'StabilizeBlockIds',
|
||||
@@ -603,52 +488,38 @@ function runWithEnvironment(
|
||||
});
|
||||
|
||||
let uniqueIdentifiers: Set<string> = new Set();
|
||||
env.tryRecord(() => {
|
||||
uniqueIdentifiers = renameVariables(reactiveFunction);
|
||||
});
|
||||
uniqueIdentifiers = renameVariables(reactiveFunction);
|
||||
log({
|
||||
kind: 'reactive',
|
||||
name: 'RenameVariables',
|
||||
value: reactiveFunction,
|
||||
});
|
||||
|
||||
env.tryRecord(() => {
|
||||
pruneHoistedContexts(reactiveFunction);
|
||||
});
|
||||
pruneHoistedContexts(reactiveFunction);
|
||||
log({
|
||||
kind: 'reactive',
|
||||
name: 'PruneHoistedContexts',
|
||||
value: reactiveFunction,
|
||||
});
|
||||
|
||||
|
||||
if (
|
||||
env.config.enablePreserveExistingMemoizationGuarantees ||
|
||||
env.config.validatePreserveExistingMemoizationGuarantees
|
||||
) {
|
||||
env.tryRecord(() => {
|
||||
validatePreservedManualMemoization(reactiveFunction).unwrap();
|
||||
});
|
||||
validatePreservedManualMemoization(reactiveFunction);
|
||||
}
|
||||
|
||||
const codegenResult = codegenFunction(reactiveFunction, {
|
||||
const ast = codegenFunction(reactiveFunction, {
|
||||
uniqueIdentifiers,
|
||||
fbtOperands,
|
||||
});
|
||||
if (codegenResult.isErr()) {
|
||||
env.recordErrors(codegenResult.unwrapErr());
|
||||
return Err(env.aggregateErrors());
|
||||
}
|
||||
const ast = codegenResult.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) {
|
||||
env.tryRecord(() => {
|
||||
validateSourceLocations(func, ast).unwrap();
|
||||
});
|
||||
validateSourceLocations(func, ast, env);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -350,9 +350,6 @@ function isFilePartOfSources(
|
||||
return false;
|
||||
}
|
||||
|
||||
export type CompileProgramMetadata = {
|
||||
retryErrors: Array<{fn: BabelFn; error: CompilerError}>;
|
||||
};
|
||||
/**
|
||||
* Main entrypoint for React Compiler.
|
||||
*
|
||||
@@ -363,7 +360,7 @@ export type CompileProgramMetadata = {
|
||||
export function compileProgram(
|
||||
program: NodePath<t.Program>,
|
||||
pass: CompilerPass,
|
||||
): CompileProgramMetadata | null {
|
||||
): void {
|
||||
/**
|
||||
* This is directly invoked by the react-compiler babel plugin, so exceptions
|
||||
* thrown by this function will fail the babel build.
|
||||
@@ -376,7 +373,7 @@ export function compileProgram(
|
||||
* the outlined functions.
|
||||
*/
|
||||
if (shouldSkipCompilation(program, pass)) {
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
const restrictedImportsErr = validateRestrictedImports(
|
||||
program,
|
||||
@@ -384,7 +381,7 @@ export function compileProgram(
|
||||
);
|
||||
if (restrictedImportsErr) {
|
||||
handleError(restrictedImportsErr, pass, null);
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
/*
|
||||
* Record lint errors and critical errors as depending on Forget's config,
|
||||
@@ -478,15 +475,11 @@ export function compileProgram(
|
||||
);
|
||||
handleError(error, programContext, null);
|
||||
}
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
|
||||
// Insert React Compiler generated functions into the Babel AST
|
||||
applyCompiledFunctions(program, compiledFns, pass, programContext);
|
||||
|
||||
return {
|
||||
retryErrors: programContext.retryErrors,
|
||||
};
|
||||
}
|
||||
|
||||
type CompileSource = {
|
||||
|
||||
@@ -1,162 +0,0 @@
|
||||
/**
|
||||
* 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -640,9 +640,6 @@ export class Environment {
|
||||
case 'ssr': {
|
||||
return true;
|
||||
}
|
||||
case 'client-no-memo': {
|
||||
return false;
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(
|
||||
this.outputMode,
|
||||
@@ -659,8 +656,7 @@ export class Environment {
|
||||
// linting also enables memoization so that we can check if manual memoization is preserved
|
||||
return true;
|
||||
}
|
||||
case 'ssr':
|
||||
case 'client-no-memo': {
|
||||
case 'ssr': {
|
||||
return false;
|
||||
}
|
||||
default: {
|
||||
@@ -679,9 +675,6 @@ export class Environment {
|
||||
case 'ssr': {
|
||||
return true;
|
||||
}
|
||||
case 'client-no-memo': {
|
||||
return false;
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(
|
||||
this.outputMode,
|
||||
@@ -741,13 +734,6 @@ export class Environment {
|
||||
} else {
|
||||
this.#errors.pushErrorDetail(error);
|
||||
}
|
||||
if (this.logger != null) {
|
||||
this.logger.logEvent(this.filename, {
|
||||
kind: 'CompileError',
|
||||
detail: error,
|
||||
fnLoc: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -31,7 +31,6 @@ import {
|
||||
makeInstructionId,
|
||||
} from '../HIR';
|
||||
import {createTemporaryPlace, markInstructionIds} from '../HIR/HIRBuilder';
|
||||
import {Result} from '../Utils/Result';
|
||||
|
||||
type ManualMemoCallee = {
|
||||
kind: 'useMemo' | 'useCallback';
|
||||
@@ -389,9 +388,7 @@ 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,
|
||||
): Result<void, CompilerError> {
|
||||
export function dropManualMemoization(func: HIRFunction): void {
|
||||
const errors = new CompilerError();
|
||||
const isValidationEnabled =
|
||||
func.env.config.validatePreserveExistingMemoizationGuarantees ||
|
||||
@@ -553,7 +550,9 @@ export function dropManualMemoization(
|
||||
}
|
||||
}
|
||||
|
||||
return errors.asResult();
|
||||
if (errors.hasAnyErrors()) {
|
||||
func.env.recordErrors(errors);
|
||||
}
|
||||
}
|
||||
|
||||
function findOptionalPlaces(fn: HIRFunction): Set<IdentifierId> {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
): Result<void, CompilerError> {
|
||||
): void {
|
||||
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 Ok(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
function findHoistedContextDeclarations(
|
||||
|
||||
@@ -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},
|
||||
): Result<Array<AliasingEffect>, CompilerError> {
|
||||
): Array<AliasingEffect> {
|
||||
// The set of externally-visible effects
|
||||
const functionEffects: Array<AliasingEffect> = [];
|
||||
|
||||
@@ -547,10 +547,14 @@ export function inferMutationAliasingRanges(
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.hasAnyErrors() && !isFunctionExpression) {
|
||||
return Err(errors);
|
||||
if (
|
||||
errors.hasAnyErrors() &&
|
||||
!isFunctionExpression &&
|
||||
fn.env.enableValidations
|
||||
) {
|
||||
fn.env.recordErrors(errors);
|
||||
}
|
||||
return Ok(functionEffects);
|
||||
return functionEffects;
|
||||
}
|
||||
|
||||
function appendFunctionErrors(errors: CompilerError, fn: HIRFunction): void {
|
||||
|
||||
@@ -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>;
|
||||
},
|
||||
): Result<CodegenFunction, CompilerError> {
|
||||
): CodegenFunction {
|
||||
const cx = new Context(
|
||||
fn.env,
|
||||
fn.id ?? '[[ anonymous ]]',
|
||||
@@ -141,11 +141,7 @@ export function codegenFunction(
|
||||
};
|
||||
}
|
||||
|
||||
const compileResult = codegenReactiveFunction(cx, fn);
|
||||
if (compileResult.isErr()) {
|
||||
return compileResult;
|
||||
}
|
||||
const compiled = compileResult.unwrap();
|
||||
const compiled = codegenReactiveFunction(cx, fn);
|
||||
|
||||
const hookGuard = fn.env.config.enableEmitHookGuards;
|
||||
if (hookGuard != null && fn.env.outputMode === 'client') {
|
||||
@@ -273,7 +269,7 @@ export function codegenFunction(
|
||||
emitInstrumentForget.globalGating,
|
||||
);
|
||||
if (assertResult.isErr()) {
|
||||
return assertResult;
|
||||
fn.env.recordErrors(assertResult.unwrapErr());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -323,20 +319,17 @@ export function codegenFunction(
|
||||
),
|
||||
reactiveFunction,
|
||||
);
|
||||
if (codegen.isErr()) {
|
||||
return codegen;
|
||||
}
|
||||
outlined.push({fn: codegen.unwrap(), type});
|
||||
outlined.push({fn: codegen, type});
|
||||
}
|
||||
compiled.outlined = outlined;
|
||||
|
||||
return compileResult;
|
||||
return compiled;
|
||||
}
|
||||
|
||||
function codegenReactiveFunction(
|
||||
cx: Context,
|
||||
fn: ReactiveFunction,
|
||||
): Result<CodegenFunction, CompilerError> {
|
||||
): CodegenFunction {
|
||||
for (const param of fn.params) {
|
||||
const place = param.kind === 'Identifier' ? param : param.place;
|
||||
cx.temp.set(place.identifier.declarationId, null);
|
||||
@@ -355,13 +348,13 @@ function codegenReactiveFunction(
|
||||
}
|
||||
|
||||
if (cx.errors.hasAnyErrors()) {
|
||||
return Err(cx.errors);
|
||||
fn.env.recordErrors(cx.errors);
|
||||
}
|
||||
|
||||
const countMemoBlockVisitor = new CountMemoBlockVisitor(fn.env);
|
||||
visitReactiveFunction(fn, countMemoBlockVisitor, undefined);
|
||||
|
||||
return Ok({
|
||||
return {
|
||||
type: 'CodegenFunction',
|
||||
loc: fn.loc,
|
||||
id: fn.id !== null ? t.identifier(fn.id) : null,
|
||||
@@ -376,7 +369,7 @@ function codegenReactiveFunction(
|
||||
prunedMemoBlocks: countMemoBlockVisitor.prunedMemoBlocks,
|
||||
prunedMemoValues: countMemoBlockVisitor.prunedMemoValues,
|
||||
outlined: [],
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
class CountMemoBlockVisitor extends ReactiveFunctionVisitor<void> {
|
||||
@@ -1665,7 +1658,7 @@ function codegenInstructionValue(
|
||||
cx.temp,
|
||||
),
|
||||
reactiveFunction,
|
||||
).unwrap();
|
||||
);
|
||||
|
||||
/*
|
||||
* ObjectMethod builder must be backwards compatible with older versions of babel.
|
||||
@@ -1864,7 +1857,7 @@ function codegenInstructionValue(
|
||||
cx.temp,
|
||||
),
|
||||
reactiveFunction,
|
||||
).unwrap();
|
||||
);
|
||||
|
||||
if (instrValue.type === 'ArrowFunctionExpression') {
|
||||
let body: t.BlockStatement | t.Expression = fn.body;
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerError} from '..';
|
||||
import {CompilerDiagnostic, CompilerError} from '..';
|
||||
import {ErrorCategory} from '../CompilerError';
|
||||
import {Environment} from '../HIR/Environment';
|
||||
import {HIRFunction, IdentifierId, Place} from '../HIR';
|
||||
import {printPlace} from '../HIR/PrintHIR';
|
||||
import {eachInstructionValueLValue, eachPatternOperand} from '../HIR/visitors';
|
||||
@@ -17,12 +19,13 @@ import {eachInstructionValueLValue, eachPatternOperand} from '../HIR/visitors';
|
||||
*/
|
||||
export function validateContextVariableLValues(fn: HIRFunction): void {
|
||||
const identifierKinds: IdentifierKinds = new Map();
|
||||
validateContextVariableLValuesImpl(fn, identifierKinds);
|
||||
validateContextVariableLValuesImpl(fn, identifierKinds, fn.env);
|
||||
}
|
||||
|
||||
function validateContextVariableLValuesImpl(
|
||||
fn: HIRFunction,
|
||||
identifierKinds: IdentifierKinds,
|
||||
env: Environment,
|
||||
): void {
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
for (const instr of block.instructions) {
|
||||
@@ -30,30 +33,30 @@ function validateContextVariableLValuesImpl(
|
||||
switch (value.kind) {
|
||||
case 'DeclareContext':
|
||||
case 'StoreContext': {
|
||||
visit(identifierKinds, value.lvalue.place, 'context');
|
||||
visit(identifierKinds, value.lvalue.place, 'context', env);
|
||||
break;
|
||||
}
|
||||
case 'LoadContext': {
|
||||
visit(identifierKinds, value.place, 'context');
|
||||
visit(identifierKinds, value.place, 'context', env);
|
||||
break;
|
||||
}
|
||||
case 'StoreLocal':
|
||||
case 'DeclareLocal': {
|
||||
visit(identifierKinds, value.lvalue.place, 'local');
|
||||
visit(identifierKinds, value.lvalue.place, 'local', env);
|
||||
break;
|
||||
}
|
||||
case 'LoadLocal': {
|
||||
visit(identifierKinds, value.place, 'local');
|
||||
visit(identifierKinds, value.place, 'local', env);
|
||||
break;
|
||||
}
|
||||
case 'PostfixUpdate':
|
||||
case 'PrefixUpdate': {
|
||||
visit(identifierKinds, value.lvalue, 'local');
|
||||
visit(identifierKinds, value.lvalue, 'local', env);
|
||||
break;
|
||||
}
|
||||
case 'Destructure': {
|
||||
for (const lvalue of eachPatternOperand(value.lvalue.pattern)) {
|
||||
visit(identifierKinds, lvalue, 'destructure');
|
||||
visit(identifierKinds, lvalue, 'destructure', env);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -62,18 +65,24 @@ function validateContextVariableLValuesImpl(
|
||||
validateContextVariableLValuesImpl(
|
||||
value.loweredFunc.func,
|
||||
identifierKinds,
|
||||
env,
|
||||
);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
for (const _ of eachInstructionValueLValue(value)) {
|
||||
CompilerError.throwTodo({
|
||||
reason:
|
||||
'ValidateContextVariableLValues: unhandled instruction variant',
|
||||
loc: value.loc,
|
||||
description: `Handle '${value.kind} lvalues`,
|
||||
suggestions: null,
|
||||
});
|
||||
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,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -90,6 +99,7 @@ function visit(
|
||||
identifiers: IdentifierKinds,
|
||||
place: Place,
|
||||
kind: 'local' | 'context' | 'destructure',
|
||||
env: Environment,
|
||||
): void {
|
||||
const prev = identifiers.get(place.identifier.id);
|
||||
if (prev !== undefined) {
|
||||
@@ -97,12 +107,18 @@ function visit(
|
||||
const isContext = kind === 'context';
|
||||
if (wasContext !== isContext) {
|
||||
if (prev.kind === 'destructure' || kind === 'destructure') {
|
||||
CompilerError.throwTodo({
|
||||
reason: `Support destructuring of context variables`,
|
||||
loc: kind === 'destructure' ? place.loc : prev.place.loc,
|
||||
description: null,
|
||||
suggestions: null,
|
||||
});
|
||||
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.invariant(false, {
|
||||
|
||||
@@ -44,7 +44,6 @@ import {
|
||||
eachInstructionValueOperand,
|
||||
eachTerminalOperand,
|
||||
} from '../HIR/visitors';
|
||||
import {Result} from '../Utils/Result';
|
||||
import {retainWhere} from '../Utils/utils';
|
||||
|
||||
const DEBUG = false;
|
||||
@@ -88,9 +87,7 @@ 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,
|
||||
): Result<void, CompilerError> {
|
||||
export function validateExhaustiveDependencies(fn: HIRFunction): void {
|
||||
const env = fn.env;
|
||||
const reactive = collectReactiveIdentifiersHIR(fn);
|
||||
|
||||
@@ -217,7 +214,9 @@ export function validateExhaustiveDependencies(
|
||||
},
|
||||
false, // isFunctionExpression
|
||||
);
|
||||
return error.asResult();
|
||||
if (error.hasAnyErrors()) {
|
||||
fn.env.recordErrors(error);
|
||||
}
|
||||
}
|
||||
|
||||
function validateDependencies(
|
||||
|
||||
@@ -26,7 +26,6 @@ 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
|
||||
@@ -88,9 +87,7 @@ 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,
|
||||
): Result<void, CompilerError> {
|
||||
export function validateHooksUsage(fn: HIRFunction): void {
|
||||
const unconditionalBlocks = computeUnconditionalBlocks(fn);
|
||||
|
||||
const errors = new CompilerError();
|
||||
@@ -426,7 +423,9 @@ export function validateHooksUsage(
|
||||
for (const [, error] of errorsByPlace) {
|
||||
errors.pushErrorDetail(error);
|
||||
}
|
||||
return errors.asResult();
|
||||
if (errors.hasAnyErrors()) {
|
||||
fn.env.recordErrors(errors);
|
||||
}
|
||||
}
|
||||
|
||||
function visitFunctionExpression(errors: CompilerError, fn: HIRFunction): void {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
import {CompilerDiagnostic, CompilerError, Effect} from '..';
|
||||
import {ErrorCategory} from '../CompilerError';
|
||||
import {Environment} from '../HIR/Environment';
|
||||
import {HIRFunction, IdentifierId, Place} from '../HIR';
|
||||
import {
|
||||
eachInstructionLValue,
|
||||
@@ -27,15 +28,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';
|
||||
errors.pushDiagnostic(
|
||||
fn.env.recordError(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Immutability,
|
||||
reason: 'Cannot reassign variable after render completes',
|
||||
@@ -46,7 +47,6 @@ export function validateLocalsNotReassignedAfterRender(fn: HIRFunction): void {
|
||||
message: `Cannot reassign ${variable} after render completes`,
|
||||
}),
|
||||
);
|
||||
throw errors;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +55,7 @@ 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) {
|
||||
@@ -68,6 +69,7 @@ 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?
|
||||
@@ -84,13 +86,12 @@ 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';
|
||||
errors.pushDiagnostic(
|
||||
env.recordError(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Immutability,
|
||||
reason: 'Cannot reassign variable in async function',
|
||||
@@ -102,7 +103,7 @@ function getContextReassignment(
|
||||
message: `Cannot reassign ${variable}`,
|
||||
}),
|
||||
);
|
||||
throw errors;
|
||||
return null;
|
||||
}
|
||||
reassigningFunctions.set(lvalue.identifier.id, reassignment);
|
||||
}
|
||||
|
||||
@@ -5,15 +5,12 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerError, EnvironmentConfig} from '..';
|
||||
import {CompilerError, CompilerErrorDetail, 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,
|
||||
): Result<void, CompilerError> {
|
||||
export function validateNoCapitalizedCalls(fn: HIRFunction): void {
|
||||
const envConfig: EnvironmentConfig = fn.env.config;
|
||||
const ALLOW_LIST = new Set([
|
||||
...DEFAULT_GLOBALS.keys(),
|
||||
@@ -48,13 +45,16 @@ export function validateNoCapitalizedCalls(
|
||||
const calleeIdentifier = value.callee.identifier.id;
|
||||
const calleeName = capitalLoadGlobals.get(calleeIdentifier);
|
||||
if (calleeName != null) {
|
||||
CompilerError.throwInvalidReact({
|
||||
category: ErrorCategory.CapitalizedCalls,
|
||||
reason,
|
||||
description: `${calleeName} may be a component`,
|
||||
loc: value.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
fn.env.recordError(
|
||||
new CompilerErrorDetail({
|
||||
category: ErrorCategory.CapitalizedCalls,
|
||||
reason,
|
||||
description: `${calleeName} may be a component`,
|
||||
loc: value.loc,
|
||||
suggestions: null,
|
||||
}),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -85,5 +85,7 @@ export function validateNoCapitalizedCalls(
|
||||
}
|
||||
}
|
||||
}
|
||||
return errors.asResult();
|
||||
if (errors.hasAnyErrors()) {
|
||||
fn.env.recordErrors(errors);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,8 +97,8 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (errors.hasAnyErrors()) {
|
||||
throw errors;
|
||||
for (const detail of errors.details) {
|
||||
fn.env.recordError(detail);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ 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
|
||||
@@ -43,9 +42,7 @@ import {Result} from '../Utils/Result';
|
||||
* 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,
|
||||
): Result<void, CompilerError> {
|
||||
export function validateNoFreezingKnownMutableFunctions(fn: HIRFunction): void {
|
||||
const errors = new CompilerError();
|
||||
const contextMutationEffects: Map<
|
||||
IdentifierId,
|
||||
@@ -162,5 +159,7 @@ export function validateNoFreezingKnownMutableFunctions(
|
||||
visitOperand(operand);
|
||||
}
|
||||
}
|
||||
return errors.asResult();
|
||||
if (errors.hasAnyErrors()) {
|
||||
fn.env.recordErrors(errors);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ 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
|
||||
@@ -20,9 +19,7 @@ import {Result} from '../Utils/Result';
|
||||
* 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,
|
||||
): Result<void, CompilerError> {
|
||||
export function validateNoImpureFunctionsInRender(fn: HIRFunction): void {
|
||||
const errors = new CompilerError();
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
for (const instr of block.instructions) {
|
||||
@@ -55,5 +52,7 @@ export function validateNoImpureFunctionsInRender(
|
||||
}
|
||||
}
|
||||
}
|
||||
return errors.asResult();
|
||||
if (errors.hasAnyErrors()) {
|
||||
fn.env.recordErrors(errors);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@ import {
|
||||
eachPatternOperand,
|
||||
eachTerminalOperand,
|
||||
} from '../HIR/visitors';
|
||||
import {Err, Ok, Result} from '../Utils/Result';
|
||||
import {retainWhere} from '../Utils/utils';
|
||||
|
||||
/**
|
||||
@@ -120,12 +119,14 @@ class Env {
|
||||
}
|
||||
}
|
||||
|
||||
export function validateNoRefAccessInRender(
|
||||
fn: HIRFunction,
|
||||
): Result<void, CompilerError> {
|
||||
export function validateNoRefAccessInRender(fn: HIRFunction): void {
|
||||
const env = new Env();
|
||||
collectTemporariesSidemap(fn, env);
|
||||
return validateNoRefAccessInRenderImpl(fn, env).map(_ => undefined);
|
||||
const errors = new CompilerError();
|
||||
validateNoRefAccessInRenderImpl(fn, env, errors);
|
||||
if (errors.hasAnyErrors()) {
|
||||
fn.env.recordErrors(errors);
|
||||
}
|
||||
}
|
||||
|
||||
function collectTemporariesSidemap(fn: HIRFunction, env: Env): void {
|
||||
@@ -305,7 +306,8 @@ function joinRefAccessTypes(...types: Array<RefAccessType>): RefAccessType {
|
||||
function validateNoRefAccessInRenderImpl(
|
||||
fn: HIRFunction,
|
||||
env: Env,
|
||||
): Result<RefAccessType, CompilerError> {
|
||||
errors: CompilerError,
|
||||
): RefAccessType {
|
||||
let returnValues: Array<undefined | RefAccessType> = [];
|
||||
let place;
|
||||
for (const param of fn.params) {
|
||||
@@ -336,7 +338,6 @@ 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) {
|
||||
@@ -432,13 +433,15 @@ 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 (result.isOk()) {
|
||||
returnType = result.unwrap();
|
||||
} else if (result.isErr()) {
|
||||
if (!innerErrors.hasAnyErrors()) {
|
||||
returnType = result;
|
||||
} else {
|
||||
readRefEffect = true;
|
||||
}
|
||||
env.set(instr.lvalue.identifier.id, {
|
||||
@@ -729,7 +732,7 @@ function validateNoRefAccessInRenderImpl(
|
||||
}
|
||||
|
||||
if (errors.hasAnyErrors()) {
|
||||
return Err(errors);
|
||||
return {kind: 'None'};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -738,10 +741,8 @@ function validateNoRefAccessInRenderImpl(
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
|
||||
return Ok(
|
||||
joinRefAccessTypes(
|
||||
...returnValues.filter((env): env is RefAccessType => env !== undefined),
|
||||
),
|
||||
return joinRefAccessTypes(
|
||||
...returnValues.filter((env): env is RefAccessType => env !== undefined),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ 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
|
||||
@@ -43,17 +42,21 @@ import {Result} from '../Utils/Result';
|
||||
* y();
|
||||
* ```
|
||||
*/
|
||||
export function validateNoSetStateInRender(
|
||||
fn: HIRFunction,
|
||||
): Result<void, CompilerError> {
|
||||
export function validateNoSetStateInRender(fn: HIRFunction): void {
|
||||
const unconditionalSetStateFunctions: Set<IdentifierId> = new Set();
|
||||
return validateNoSetStateInRenderImpl(fn, unconditionalSetStateFunctions);
|
||||
const errors = validateNoSetStateInRenderImpl(
|
||||
fn,
|
||||
unconditionalSetStateFunctions,
|
||||
);
|
||||
if (errors.hasAnyErrors()) {
|
||||
fn.env.recordErrors(errors);
|
||||
}
|
||||
}
|
||||
|
||||
function validateNoSetStateInRenderImpl(
|
||||
fn: HIRFunction,
|
||||
unconditionalSetStateFunctions: Set<IdentifierId>,
|
||||
): Result<void, CompilerError> {
|
||||
): CompilerError {
|
||||
const unconditionalBlocks = computeUnconditionalBlocks(fn);
|
||||
let activeManualMemoId: number | null = null;
|
||||
const errors = new CompilerError();
|
||||
@@ -92,7 +95,7 @@ function validateNoSetStateInRenderImpl(
|
||||
validateNoSetStateInRenderImpl(
|
||||
instr.value.loweredFunc.func,
|
||||
unconditionalSetStateFunctions,
|
||||
).isErr()
|
||||
).hasAnyErrors()
|
||||
) {
|
||||
// This function expression unconditionally calls a setState
|
||||
unconditionalSetStateFunctions.add(instr.lvalue.identifier.id);
|
||||
@@ -183,5 +186,5 @@ function validateNoSetStateInRenderImpl(
|
||||
}
|
||||
}
|
||||
|
||||
return errors.asResult();
|
||||
return errors;
|
||||
}
|
||||
|
||||
@@ -37,7 +37,6 @@ import {
|
||||
ReactiveFunctionVisitor,
|
||||
visitReactiveFunction,
|
||||
} from '../ReactiveScopes/visitors';
|
||||
import {Result} from '../Utils/Result';
|
||||
import {getOrInsertDefault} from '../Utils/utils';
|
||||
|
||||
/**
|
||||
@@ -47,15 +46,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,
|
||||
): Result<void, CompilerError> {
|
||||
export function validatePreservedManualMemoization(fn: ReactiveFunction): void {
|
||||
const state = {
|
||||
errors: new CompilerError(),
|
||||
manualMemoState: null,
|
||||
};
|
||||
visitReactiveFunction(fn, new Visitor(), state);
|
||||
return state.errors.asResult();
|
||||
for (const detail of state.errors.details) {
|
||||
fn.env.recordError(detail);
|
||||
}
|
||||
}
|
||||
|
||||
const DEBUG = false;
|
||||
|
||||
@@ -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 {Result} from '../Utils/Result';
|
||||
import {Environment} from '../HIR/Environment';
|
||||
|
||||
/**
|
||||
* IMPORTANT: This validation is only intended for use in unit tests.
|
||||
@@ -123,7 +123,8 @@ export function validateSourceLocations(
|
||||
t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
|
||||
>,
|
||||
generatedAst: CodegenFunction,
|
||||
): Result<void, CompilerError> {
|
||||
env: Environment,
|
||||
): void {
|
||||
const errors = new CompilerError();
|
||||
|
||||
/*
|
||||
@@ -309,5 +310,7 @@ export function validateSourceLocations(
|
||||
}
|
||||
}
|
||||
|
||||
return errors.asResult();
|
||||
for (const detail of errors.details) {
|
||||
env.recordError(detail);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,9 +20,8 @@ import {
|
||||
eachInstructionValueOperand,
|
||||
eachTerminalOperand,
|
||||
} from '../HIR/visitors';
|
||||
import {Result} from '../Utils/Result';
|
||||
|
||||
export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
|
||||
export function validateUseMemo(fn: HIRFunction): void {
|
||||
const errors = new CompilerError();
|
||||
const voidMemoErrors = new CompilerError();
|
||||
const useMemos = new Set<IdentifierId>();
|
||||
@@ -177,7 +176,9 @@ export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
|
||||
}
|
||||
}
|
||||
fn.env.logErrors(voidMemoErrors.asResult());
|
||||
return errors.asResult();
|
||||
if (errors.hasAnyErrors()) {
|
||||
fn.env.recordErrors(errors);
|
||||
}
|
||||
}
|
||||
|
||||
function validateNoContextVariableAssignment(
|
||||
|
||||
@@ -21,15 +21,15 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Invariant: [InferMutationAliasingEffects] Expected value kind to be initialized
|
||||
Todo: [hoisting] EnterSSA: Expected identifier to be defined before being used
|
||||
|
||||
<unknown> x$1.
|
||||
Identifier x$1 is undefined.
|
||||
|
||||
error.dont-hoist-inline-reference.ts:3:21
|
||||
error.dont-hoist-inline-reference.ts:3:2
|
||||
1 | import {identity} from 'shared-runtime';
|
||||
2 | function useInvalid() {
|
||||
> 3 | const x = identity(x);
|
||||
| ^ this is uninitialized
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^ [hoisting] EnterSSA: Expected identifier to be defined before being used
|
||||
4 | return x;
|
||||
5 | }
|
||||
6 |
|
||||
|
||||
@@ -17,7 +17,7 @@ function Component() {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 6 errors:
|
||||
Found 3 errors:
|
||||
|
||||
Error: Cannot call impure function during render
|
||||
|
||||
@@ -57,45 +57,6 @@ error.invalid-impure-functions-in-render.ts:6:15
|
||||
7 | return <Foo date={date} now={now} rand={rand} />;
|
||||
8 | }
|
||||
9 |
|
||||
|
||||
Error: Cannot call impure function during render
|
||||
|
||||
`Date.now` is an impure function. Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent).
|
||||
|
||||
error.invalid-impure-functions-in-render.ts:4:15
|
||||
2 |
|
||||
3 | function Component() {
|
||||
> 4 | const date = Date.now();
|
||||
| ^^^^^^^^ Cannot call impure function
|
||||
5 | const now = performance.now();
|
||||
6 | const rand = Math.random();
|
||||
7 | return <Foo date={date} now={now} rand={rand} />;
|
||||
|
||||
Error: Cannot call impure function during render
|
||||
|
||||
`performance.now` is an impure function. Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent).
|
||||
|
||||
error.invalid-impure-functions-in-render.ts:5:14
|
||||
3 | function Component() {
|
||||
4 | const date = Date.now();
|
||||
> 5 | const now = performance.now();
|
||||
| ^^^^^^^^^^^^^^^ Cannot call impure function
|
||||
6 | const rand = Math.random();
|
||||
7 | return <Foo date={date} now={now} rand={rand} />;
|
||||
8 | }
|
||||
|
||||
Error: Cannot call impure function during render
|
||||
|
||||
`Math.random` is an impure function. Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent).
|
||||
|
||||
error.invalid-impure-functions-in-render.ts:6:15
|
||||
4 | const date = Date.now();
|
||||
5 | const now = performance.now();
|
||||
> 6 | const rand = Math.random();
|
||||
| ^^^^^^^^^^^ Cannot call impure function
|
||||
7 | return <Foo date={date} now={now} rand={rand} />;
|
||||
8 | }
|
||||
9 |
|
||||
```
|
||||
|
||||
|
||||
@@ -14,19 +14,7 @@ function Component() {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 2 errors:
|
||||
|
||||
Error: Invalid type configuration for module
|
||||
|
||||
Expected type for object property 'useHookNotTypedAsHook' from module 'ReactCompilerTest' to be a hook based on the property name.
|
||||
|
||||
error.invalid-type-provider-hook-name-not-typed-as-hook-namespace.ts:4:9
|
||||
2 |
|
||||
3 | function Component() {
|
||||
> 4 | return ReactCompilerTest.useHookNotTypedAsHook();
|
||||
| ^^^^^^^^^^^^^^^^^ Invalid type configuration for module
|
||||
5 | }
|
||||
6 |
|
||||
Found 1 error:
|
||||
|
||||
Error: Invalid type configuration for module
|
||||
|
||||
|
||||
@@ -14,19 +14,7 @@ function Component() {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 2 errors:
|
||||
|
||||
Error: Invalid type configuration for module
|
||||
|
||||
Expected type for object property 'useHookNotTypedAsHook' from module 'ReactCompilerTest' to be a hook based on the property name.
|
||||
|
||||
error.invalid-type-provider-hook-name-not-typed-as-hook.ts:4:9
|
||||
2 |
|
||||
3 | function Component() {
|
||||
> 4 | return useHookNotTypedAsHook();
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ Invalid type configuration for module
|
||||
5 | }
|
||||
6 |
|
||||
Found 1 error:
|
||||
|
||||
Error: Invalid type configuration for module
|
||||
|
||||
|
||||
@@ -14,19 +14,7 @@ function Component() {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 2 errors:
|
||||
|
||||
Error: Invalid type configuration for module
|
||||
|
||||
Expected type for `import ... from 'useDefaultExportNotTypedAsHook'` to be a hook based on the module name.
|
||||
|
||||
error.invalid-type-provider-hooklike-module-default-not-hook.ts:4:15
|
||||
2 |
|
||||
3 | function Component() {
|
||||
> 4 | return <div>{foo()}</div>;
|
||||
| ^^^ Invalid type configuration for module
|
||||
5 | }
|
||||
6 |
|
||||
Found 1 error:
|
||||
|
||||
Error: Invalid type configuration for module
|
||||
|
||||
|
||||
@@ -14,19 +14,7 @@ function Component() {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 2 errors:
|
||||
|
||||
Error: Invalid type configuration for module
|
||||
|
||||
Expected type for object property 'useHookNotTypedAsHook' from module 'ReactCompilerTest' to be a hook based on the property name.
|
||||
|
||||
error.invalid-type-provider-nonhook-name-typed-as-hook.ts:4:15
|
||||
2 |
|
||||
3 | function Component() {
|
||||
> 4 | return <div>{notAhookTypedAsHook()}</div>;
|
||||
| ^^^^^^^^^^^^^^^^^^^ Invalid type configuration for module
|
||||
5 | }
|
||||
6 |
|
||||
Found 1 error:
|
||||
|
||||
Error: Invalid type configuration for module
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ function Component({foo}) {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 2 errors:
|
||||
Found 3 errors:
|
||||
|
||||
Todo: Support destructuring of context variables
|
||||
|
||||
@@ -29,7 +29,18 @@ 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
|
||||
|
||||
@@ -58,7 +58,6 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileError","detail":{"options":{"category":"PreserveManualMemo","reason":"Existing memoization could not be preserved","description":"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 `value`, but the source dependencies were []. Inferred dependency not present in source","suggestions":null,"details":[{"kind":"error","loc":{"start":{"line":9,"column":31,"index":337},"end":{"line":9,"column":52,"index":358},"filename":"dynamic-gating-bailout-nopanic.ts"},"message":"Could not preserve existing manual memoization"}]}},"fnLoc":null}
|
||||
{"kind":"CompileError","fnLoc":{"start":{"line":6,"column":0,"index":255},"end":{"line":16,"column":1,"index":482},"filename":"dynamic-gating-bailout-nopanic.ts"},"detail":{"options":{"category":"PreserveManualMemo","reason":"Existing memoization could not be preserved","description":"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 `value`, but the source dependencies were []. Inferred dependency not present in source","suggestions":null,"details":[{"kind":"error","loc":{"start":{"line":9,"column":31,"index":337},"end":{"line":9,"column":52,"index":358},"filename":"dynamic-gating-bailout-nopanic.ts"},"message":"Could not preserve existing manual memoization"}]}}}
|
||||
```
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ function Component() {
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 6 errors:
|
||||
Found 3 errors:
|
||||
|
||||
Error: Cannot call impure function during render
|
||||
|
||||
@@ -57,45 +57,6 @@ error.invalid-impure-functions-in-render.ts:6:15
|
||||
7 | return <Foo date={date} now={now} rand={rand} />;
|
||||
8 | }
|
||||
9 |
|
||||
|
||||
Error: Cannot call impure function during render
|
||||
|
||||
`Date.now` is an impure function. Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent).
|
||||
|
||||
error.invalid-impure-functions-in-render.ts:4:15
|
||||
2 |
|
||||
3 | function Component() {
|
||||
> 4 | const date = Date.now();
|
||||
| ^^^^^^^^ Cannot call impure function
|
||||
5 | const now = performance.now();
|
||||
6 | const rand = Math.random();
|
||||
7 | return <Foo date={date} now={now} rand={rand} />;
|
||||
|
||||
Error: Cannot call impure function during render
|
||||
|
||||
`performance.now` is an impure function. Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent).
|
||||
|
||||
error.invalid-impure-functions-in-render.ts:5:14
|
||||
3 | function Component() {
|
||||
4 | const date = Date.now();
|
||||
> 5 | const now = performance.now();
|
||||
| ^^^^^^^^^^^^^^^ Cannot call impure function
|
||||
6 | const rand = Math.random();
|
||||
7 | return <Foo date={date} now={now} rand={rand} />;
|
||||
8 | }
|
||||
|
||||
Error: Cannot call impure function during render
|
||||
|
||||
`Math.random` is an impure function. Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent).
|
||||
|
||||
error.invalid-impure-functions-in-render.ts:6:15
|
||||
4 | const date = Date.now();
|
||||
5 | const now = performance.now();
|
||||
> 6 | const rand = Math.random();
|
||||
| ^^^^^^^^^^^ Cannot call impure function
|
||||
7 | return <Foo date={date} now={now} rand={rand} />;
|
||||
8 | }
|
||||
9 |
|
||||
```
|
||||
|
||||
|
||||
@@ -64,6 +64,9 @@ testRule(
|
||||
makeTestCaseError(
|
||||
'Capitalized functions are reserved for components',
|
||||
),
|
||||
makeTestCaseError(
|
||||
'Capitalized functions are reserved for components',
|
||||
),
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -57,7 +57,6 @@ 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';
|
||||
@@ -70,6 +69,7 @@ testRule('plugin-recommended', TestRecommendedRules, {
|
||||
`,
|
||||
errors: [
|
||||
makeTestCaseError('Hooks must always be called in a consistent order'),
|
||||
makeTestCaseError('Capitalized functions are reserved for components'),
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -128,6 +128,7 @@ testRule('plugin-recommended', TestRecommendedRules, {
|
||||
makeTestCaseError(
|
||||
'Calling setState from useMemo may trigger an infinite loop',
|
||||
),
|
||||
makeTestCaseError('Found extra memoization dependencies'),
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -3991,9 +3991,23 @@ 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 (includesSomeLane(renderLanes, primaryChildLanes)) {
|
||||
if (
|
||||
contextChanged ||
|
||||
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);
|
||||
|
||||
@@ -20,7 +20,11 @@ import type {Hook} from './ReactFiberHooks';
|
||||
|
||||
import {isPrimaryRenderer, HostTransitionContext} from './ReactFiberConfig';
|
||||
import {createCursor, push, pop} from './ReactFiberStack';
|
||||
import {ContextProvider, DehydratedFragment} from './ReactWorkTags';
|
||||
import {
|
||||
ContextProvider,
|
||||
DehydratedFragment,
|
||||
SuspenseComponent,
|
||||
} from './ReactWorkTags';
|
||||
import {NoLanes, isSubsetOfLanes, mergeLanes} from './ReactFiberLane';
|
||||
import {
|
||||
NoFlags,
|
||||
@@ -295,6 +299,37 @@ 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;
|
||||
@@ -331,9 +366,9 @@ export function lazilyPropagateParentContextChanges(
|
||||
current: Fiber,
|
||||
workInProgress: Fiber,
|
||||
renderLanes: Lanes,
|
||||
) {
|
||||
): boolean {
|
||||
const forcePropagateEntireTree = false;
|
||||
propagateParentContextChanges(
|
||||
return propagateParentContextChanges(
|
||||
current,
|
||||
workInProgress,
|
||||
renderLanes,
|
||||
@@ -364,7 +399,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;
|
||||
@@ -460,6 +495,7 @@ 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(
|
||||
|
||||
@@ -2,6 +2,7 @@ let React;
|
||||
let ReactNoop;
|
||||
let Scheduler;
|
||||
let act;
|
||||
let use;
|
||||
let useState;
|
||||
let useContext;
|
||||
let Suspense;
|
||||
@@ -19,6 +20,7 @@ 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;
|
||||
@@ -937,4 +939,102 @@ 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');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user