Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2824f88468 | ||
|
|
cebe42e245 | ||
|
|
d6558f36e2 | ||
|
|
59d7c27087 | ||
|
|
9b2d8013ee | ||
|
|
e3e5d95cc4 | ||
|
|
426a394845 | ||
|
|
eca778cf8b | ||
|
|
0dbb43bc57 | ||
|
|
8b6b11f703 | ||
|
|
ab18f33d46 |
@@ -327,4 +327,7 @@ Walk through `runWithEnvironment` and wrap each pass call site. This is the inte
|
||||
* **Phase 3 (BuildHIR) revealed that most error sites already used `builder.errors.push()` for accumulation.** The existing lowering code was designed to accumulate errors rather than throw. The main changes were: (1) changing `lower()` return type from `Result` to `HIRFunction`, (2) recording builder errors on env, (3) adding a try/catch around body lowering to catch thrown CompilerErrors from sub-calls like `resolveBinding()`, (4) treating `var` as `let` instead of skipping declarations, and (5) fixing ForStatement init/test handling to produce valid CFG structure.
|
||||
* **Partial HIR can trigger downstream invariants.** When lowering skips or partially handles constructs (e.g., unreachable hoisted functions, `var` declarations before the fix), downstream passes like `InferMutationAliasingEffects` may encounter uninitialized identifiers and throw invariants. This is acceptable since the function still correctly bails out of compilation, but error messages may be less specific. The fix for `var` (treating as `let`) demonstrates how to avoid this: continue lowering with a best-effort representation rather than skipping entirely.
|
||||
* **Errors accumulated on `env` are lost when an invariant propagates out of the pipeline.** Since invariant CompilerErrors always re-throw through `tryRecord()`, they exit the pipeline as exceptions. The caller only sees the invariant error, not any errors previously recorded on `env`. This is a design limitation that could be addressed by aggregating env errors with caught exceptions in `tryCompileFunction()`.
|
||||
* **Dedicated fault tolerance test fixtures** were added in `__tests__/fixtures/compiler/fault-tolerance/`. Each fixture combines two or more errors from different passes to verify the compiler reports all of them rather than short-circuiting on the first. Coverage includes: `var`+props mutation (BuildHIR→InferMutationAliasingEffects), `var`+ref access (BuildHIR→ValidateNoRefAccessInRender), `try/finally`+props mutation (BuildHIR→InferMutationAliasingEffects), `try/finally`+ref access (BuildHIR→ValidateNoRefAccessInRender), and a 3-error test combining try/finally+ref access+props mutation.
|
||||
* **Cleanup: consistent `tryRecord()` wrapping in Pipeline.ts.** All validation passes and inference passes are now wrapped in `env.tryRecord()` for defense-in-depth, consistent with the approach used for transform passes. Previously only transform passes were wrapped. Merged duplicate `env.enableValidations` guard blocks. Pattern B lint-only passes (`env.logErrors()`) were intentionally not wrapped since they use a different error recording strategy.
|
||||
* **Cleanup: normalized validation error recording pattern.** Four validation passes (`ValidateNoDerivedComputationsInEffects`, `ValidateMemoizedEffectDependencies`, `ValidatePreservedManualMemoization`, `ValidateSourceLocations`) were using `for (const detail of errors.details) { env.recordError(detail); }` instead of the simpler `env.recordErrors(errors)`. Normalized to use the batch method.
|
||||
|
||||
|
||||
@@ -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,116 +158,108 @@ function runWithEnvironment(
|
||||
const hir = lower(func, env);
|
||||
log({kind: 'hir', name: 'HIR', value: hir});
|
||||
|
||||
env.tryRecord(() => {
|
||||
pruneMaybeThrows(hir);
|
||||
});
|
||||
pruneMaybeThrows(hir);
|
||||
log({kind: 'hir', name: 'PruneMaybeThrows', value: hir});
|
||||
|
||||
validateContextVariableLValues(hir);
|
||||
validateUseMemo(hir);
|
||||
env.tryRecord(() => {
|
||||
validateContextVariableLValues(hir);
|
||||
});
|
||||
env.tryRecord(() => {
|
||||
validateUseMemo(hir);
|
||||
});
|
||||
|
||||
if (env.enableDropManualMemoization) {
|
||||
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) {
|
||||
validateHooksUsage(hir);
|
||||
env.tryRecord(() => {
|
||||
validateHooksUsage(hir);
|
||||
});
|
||||
}
|
||||
if (env.config.validateNoCapitalizedCalls) {
|
||||
validateNoCapitalizedCalls(hir);
|
||||
env.tryRecord(() => {
|
||||
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});
|
||||
|
||||
inferMutationAliasingEffects(hir);
|
||||
env.tryRecord(() => {
|
||||
inferMutationAliasingEffects(hir);
|
||||
});
|
||||
log({kind: 'hir', name: 'InferMutationAliasingEffects', value: hir});
|
||||
|
||||
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});
|
||||
|
||||
inferMutationAliasingRanges(hir, {
|
||||
isFunctionExpression: false,
|
||||
env.tryRecord(() => {
|
||||
inferMutationAliasingRanges(hir, {
|
||||
isFunctionExpression: false,
|
||||
});
|
||||
});
|
||||
log({kind: 'hir', name: 'InferMutationAliasingRanges', value: hir});
|
||||
if (env.enableValidations) {
|
||||
validateLocalsNotReassignedAfterRender(hir);
|
||||
}
|
||||
env.tryRecord(() => {
|
||||
validateLocalsNotReassignedAfterRender(hir);
|
||||
});
|
||||
|
||||
if (env.enableValidations) {
|
||||
if (env.config.assertValidMutableRanges) {
|
||||
assertValidMutableRanges(hir);
|
||||
}
|
||||
|
||||
if (env.config.validateRefAccessDuringRender) {
|
||||
validateNoRefAccessInRender(hir);
|
||||
env.tryRecord(() => {
|
||||
validateNoRefAccessInRender(hir);
|
||||
});
|
||||
}
|
||||
|
||||
if (env.config.validateNoSetStateInRender) {
|
||||
validateNoSetStateInRender(hir);
|
||||
env.tryRecord(() => {
|
||||
validateNoSetStateInRender(hir);
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -277,7 +268,9 @@ function runWithEnvironment(
|
||||
) {
|
||||
env.logErrors(validateNoDerivedComputationsInEffects_exp(hir));
|
||||
} else if (env.config.validateNoDerivedComputationsInEffects) {
|
||||
validateNoDerivedComputationsInEffects(hir);
|
||||
env.tryRecord(() => {
|
||||
validateNoDerivedComputationsInEffects(hir);
|
||||
});
|
||||
}
|
||||
|
||||
if (env.config.validateNoSetStateInEffects && env.outputMode === 'lint') {
|
||||
@@ -288,16 +281,12 @@ function runWithEnvironment(
|
||||
env.logErrors(validateNoJSXInTryStatement(hir));
|
||||
}
|
||||
|
||||
if (env.config.validateNoImpureFunctionsInRender) {
|
||||
validateNoImpureFunctionsInRender(hir);
|
||||
}
|
||||
|
||||
validateNoFreezingKnownMutableFunctions(hir);
|
||||
env.tryRecord(() => {
|
||||
validateNoFreezingKnownMutableFunctions(hir);
|
||||
});
|
||||
}
|
||||
|
||||
env.tryRecord(() => {
|
||||
inferReactivePlaces(hir);
|
||||
});
|
||||
inferReactivePlaces(hir);
|
||||
log({kind: 'hir', name: 'InferReactivePlaces', value: hir});
|
||||
|
||||
if (env.enableValidations) {
|
||||
@@ -306,13 +295,13 @@ function runWithEnvironment(
|
||||
env.config.validateExhaustiveEffectDependencies
|
||||
) {
|
||||
// NOTE: this relies on reactivity inference running first
|
||||
validateExhaustiveDependencies(hir);
|
||||
env.tryRecord(() => {
|
||||
validateExhaustiveDependencies(hir);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
env.tryRecord(() => {
|
||||
rewriteInstructionKindsBasedOnReassignment(hir);
|
||||
});
|
||||
rewriteInstructionKindsBasedOnReassignment(hir);
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'RewriteInstructionKindsBasedOnReassignment',
|
||||
@@ -333,16 +322,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',
|
||||
@@ -350,15 +335,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',
|
||||
@@ -367,51 +348,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',
|
||||
@@ -419,9 +388,7 @@ function runWithEnvironment(
|
||||
});
|
||||
assertValidBlockNesting(hir);
|
||||
|
||||
env.tryRecord(() => {
|
||||
buildReactiveScopeTerminalsHIR(hir);
|
||||
});
|
||||
buildReactiveScopeTerminalsHIR(hir);
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'BuildReactiveScopeTerminalsHIR',
|
||||
@@ -430,18 +397,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',
|
||||
@@ -449,9 +412,7 @@ function runWithEnvironment(
|
||||
});
|
||||
assertTerminalSuccessorsExist(hir);
|
||||
assertTerminalPredsExist(hir);
|
||||
env.tryRecord(() => {
|
||||
propagateScopeDependenciesHIR(hir);
|
||||
});
|
||||
propagateScopeDependenciesHIR(hir);
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'PropagateScopeDependenciesHIR',
|
||||
@@ -459,9 +420,7 @@ function runWithEnvironment(
|
||||
});
|
||||
|
||||
let reactiveFunction!: ReactiveFunction;
|
||||
env.tryRecord(() => {
|
||||
reactiveFunction = buildReactiveFunction(hir);
|
||||
});
|
||||
reactiveFunction = buildReactiveFunction(hir);
|
||||
log({
|
||||
kind: 'reactive',
|
||||
name: 'BuildReactiveFunction',
|
||||
@@ -470,9 +429,7 @@ function runWithEnvironment(
|
||||
|
||||
assertWellFormedBreakTargets(reactiveFunction);
|
||||
|
||||
env.tryRecord(() => {
|
||||
pruneUnusedLabels(reactiveFunction);
|
||||
});
|
||||
pruneUnusedLabels(reactiveFunction);
|
||||
log({
|
||||
kind: 'reactive',
|
||||
name: 'PruneUnusedLabels',
|
||||
@@ -480,90 +437,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',
|
||||
@@ -571,30 +508,27 @@ 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
|
||||
) {
|
||||
validatePreservedManualMemoization(reactiveFunction);
|
||||
env.tryRecord(() => {
|
||||
validatePreservedManualMemoization(reactiveFunction);
|
||||
});
|
||||
}
|
||||
|
||||
const ast = codegenFunction(reactiveFunction, {
|
||||
@@ -607,7 +541,9 @@ function runWithEnvironment(
|
||||
}
|
||||
|
||||
if (env.config.validateSourceLocations) {
|
||||
validateSourceLocations(func, ast, env);
|
||||
env.tryRecord(() => {
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -217,11 +217,7 @@ export function lower(
|
||||
if (err instanceof CompilerError) {
|
||||
// Re-throw invariant errors immediately
|
||||
for (const detail of err.details) {
|
||||
if (
|
||||
(detail instanceof CompilerDiagnostic
|
||||
? detail.category
|
||||
: detail.category) === ErrorCategory.Invariant
|
||||
) {
|
||||
if (detail.category === ErrorCategory.Invariant) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
@@ -576,8 +572,10 @@ function lowerStatement(
|
||||
const initBlock = builder.enter('loop', _blockId => {
|
||||
const init = stmt.get('init');
|
||||
if (init.node == null) {
|
||||
// No init expression (e.g., `for (; ...)`), add a placeholder to avoid
|
||||
// invariant about empty blocks
|
||||
/*
|
||||
* No init expression (e.g., `for (; ...)`), add a placeholder to avoid
|
||||
* invariant about empty blocks
|
||||
*/
|
||||
lowerValueToTemporary(builder, {
|
||||
kind: 'Primitive',
|
||||
value: undefined,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -97,9 +97,7 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const detail of errors.details) {
|
||||
fn.env.recordError(detail);
|
||||
}
|
||||
fn.env.recordErrors(errors);
|
||||
}
|
||||
|
||||
function validateEffect(
|
||||
|
||||
@@ -52,9 +52,7 @@ export function validatePreservedManualMemoization(fn: ReactiveFunction): void {
|
||||
manualMemoState: null,
|
||||
};
|
||||
visitReactiveFunction(fn, new Visitor(), state);
|
||||
for (const detail of state.errors.details) {
|
||||
fn.env.recordError(detail);
|
||||
}
|
||||
fn.env.recordErrors(state.errors);
|
||||
}
|
||||
|
||||
const DEBUG = false;
|
||||
|
||||
@@ -310,7 +310,5 @@ export function validateSourceLocations(
|
||||
}
|
||||
}
|
||||
|
||||
for (const detail of errors.details) {
|
||||
env.recordError(detail);
|
||||
}
|
||||
env.recordErrors(errors);
|
||||
}
|
||||
|
||||
@@ -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 |
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Fault tolerance test: two independent errors should both be reported.
|
||||
*
|
||||
* Error 1 (BuildHIR): `try/finally` is not supported
|
||||
* Error 2 (InferMutationAliasingEffects): Mutation of frozen props
|
||||
*/
|
||||
function Component(props) {
|
||||
// Error: try/finally (Todo from BuildHIR)
|
||||
try {
|
||||
doWork();
|
||||
} finally {
|
||||
doCleanup();
|
||||
}
|
||||
|
||||
// Error: mutating frozen props
|
||||
props.value = 1;
|
||||
|
||||
return <div>{props.value}</div>;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 2 errors:
|
||||
|
||||
Todo: (BuildHIR::lowerStatement) Handle TryStatement without a catch clause
|
||||
|
||||
error.try-finally-and-mutation-of-props.ts:9:2
|
||||
7 | function Component(props) {
|
||||
8 | // Error: try/finally (Todo from BuildHIR)
|
||||
> 9 | try {
|
||||
| ^^^^^
|
||||
> 10 | doWork();
|
||||
| ^^^^^^^^^^^^^
|
||||
> 11 | } finally {
|
||||
| ^^^^^^^^^^^^^
|
||||
> 12 | doCleanup();
|
||||
| ^^^^^^^^^^^^^
|
||||
> 13 | }
|
||||
| ^^^^ (BuildHIR::lowerStatement) Handle TryStatement without a catch clause
|
||||
14 |
|
||||
15 | // Error: mutating frozen props
|
||||
16 | props.value = 1;
|
||||
|
||||
Error: This value cannot be modified
|
||||
|
||||
Modifying component props or hook arguments is not allowed. Consider using a local variable instead.
|
||||
|
||||
error.try-finally-and-mutation-of-props.ts:16:2
|
||||
14 |
|
||||
15 | // Error: mutating frozen props
|
||||
> 16 | props.value = 1;
|
||||
| ^^^^^ value cannot be modified
|
||||
17 |
|
||||
18 | return <div>{props.value}</div>;
|
||||
19 | }
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Fault tolerance test: two independent errors should both be reported.
|
||||
*
|
||||
* Error 1 (BuildHIR): `try/finally` is not supported
|
||||
* Error 2 (InferMutationAliasingEffects): Mutation of frozen props
|
||||
*/
|
||||
function Component(props) {
|
||||
// Error: try/finally (Todo from BuildHIR)
|
||||
try {
|
||||
doWork();
|
||||
} finally {
|
||||
doCleanup();
|
||||
}
|
||||
|
||||
// Error: mutating frozen props
|
||||
props.value = 1;
|
||||
|
||||
return <div>{props.value}</div>;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateRefAccessDuringRender
|
||||
/**
|
||||
* Fault tolerance test: two independent errors should both be reported.
|
||||
*
|
||||
* Error 1 (BuildHIR): `try/finally` is not supported
|
||||
* Error 2 (ValidateNoRefAccessInRender): reading ref.current during render
|
||||
*/
|
||||
function Component() {
|
||||
const ref = useRef(null);
|
||||
|
||||
// Error: try/finally (Todo from BuildHIR)
|
||||
try {
|
||||
doSomething();
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
|
||||
// Error: reading ref during render
|
||||
const value = ref.current;
|
||||
|
||||
return <div>{value}</div>;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 2 errors:
|
||||
|
||||
Todo: (BuildHIR::lowerStatement) Handle TryStatement without a catch clause
|
||||
|
||||
error.try-finally-and-ref-access.ts:12:2
|
||||
10 |
|
||||
11 | // Error: try/finally (Todo from BuildHIR)
|
||||
> 12 | try {
|
||||
| ^^^^^
|
||||
> 13 | doSomething();
|
||||
| ^^^^^^^^^^^^^^^^^^
|
||||
> 14 | } finally {
|
||||
| ^^^^^^^^^^^^^^^^^^
|
||||
> 15 | cleanup();
|
||||
| ^^^^^^^^^^^^^^^^^^
|
||||
> 16 | }
|
||||
| ^^^^ (BuildHIR::lowerStatement) Handle TryStatement without a catch clause
|
||||
17 |
|
||||
18 | // Error: reading ref during render
|
||||
19 | const value = ref.current;
|
||||
|
||||
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.try-finally-and-ref-access.ts:19:16
|
||||
17 |
|
||||
18 | // Error: reading ref during render
|
||||
> 19 | const value = ref.current;
|
||||
| ^^^^^^^^^^^ Cannot access ref value during render
|
||||
20 |
|
||||
21 | return <div>{value}</div>;
|
||||
22 | }
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
// @validateRefAccessDuringRender
|
||||
/**
|
||||
* Fault tolerance test: two independent errors should both be reported.
|
||||
*
|
||||
* Error 1 (BuildHIR): `try/finally` is not supported
|
||||
* Error 2 (ValidateNoRefAccessInRender): reading ref.current during render
|
||||
*/
|
||||
function Component() {
|
||||
const ref = useRef(null);
|
||||
|
||||
// Error: try/finally (Todo from BuildHIR)
|
||||
try {
|
||||
doSomething();
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
|
||||
// Error: reading ref during render
|
||||
const value = ref.current;
|
||||
|
||||
return <div>{value}</div>;
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateRefAccessDuringRender
|
||||
/**
|
||||
* Fault tolerance test: three independent errors should all be reported.
|
||||
*
|
||||
* Error 1 (BuildHIR): `try/finally` is not supported
|
||||
* Error 2 (ValidateNoRefAccessInRender): reading ref.current during render
|
||||
* Error 3 (InferMutationAliasingEffects): Mutation of frozen props
|
||||
*/
|
||||
function Component(props) {
|
||||
const ref = useRef(null);
|
||||
|
||||
// Error: try/finally (Todo from BuildHIR)
|
||||
try {
|
||||
doWork();
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
|
||||
// Error: reading ref during render
|
||||
const value = ref.current;
|
||||
|
||||
// Error: mutating frozen props
|
||||
props.items = [];
|
||||
|
||||
return <div>{value}</div>;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 3 errors:
|
||||
|
||||
Todo: (BuildHIR::lowerStatement) Handle TryStatement without a catch clause
|
||||
|
||||
error.try-finally-ref-access-and-mutation.ts:13:2
|
||||
11 |
|
||||
12 | // Error: try/finally (Todo from BuildHIR)
|
||||
> 13 | try {
|
||||
| ^^^^^
|
||||
> 14 | doWork();
|
||||
| ^^^^^^^^^^^^^
|
||||
> 15 | } finally {
|
||||
| ^^^^^^^^^^^^^
|
||||
> 16 | cleanup();
|
||||
| ^^^^^^^^^^^^^
|
||||
> 17 | }
|
||||
| ^^^^ (BuildHIR::lowerStatement) Handle TryStatement without a catch clause
|
||||
18 |
|
||||
19 | // Error: reading ref during render
|
||||
20 | const value = ref.current;
|
||||
|
||||
Error: This value cannot be modified
|
||||
|
||||
Modifying component props or hook arguments is not allowed. Consider using a local variable instead.
|
||||
|
||||
error.try-finally-ref-access-and-mutation.ts:23:2
|
||||
21 |
|
||||
22 | // Error: mutating frozen props
|
||||
> 23 | props.items = [];
|
||||
| ^^^^^ value cannot be modified
|
||||
24 |
|
||||
25 | return <div>{value}</div>;
|
||||
26 | }
|
||||
|
||||
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.try-finally-ref-access-and-mutation.ts:20:16
|
||||
18 |
|
||||
19 | // Error: reading ref during render
|
||||
> 20 | const value = ref.current;
|
||||
| ^^^^^^^^^^^ Cannot access ref value during render
|
||||
21 |
|
||||
22 | // Error: mutating frozen props
|
||||
23 | props.items = [];
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
// @validateRefAccessDuringRender
|
||||
/**
|
||||
* Fault tolerance test: three independent errors should all be reported.
|
||||
*
|
||||
* Error 1 (BuildHIR): `try/finally` is not supported
|
||||
* Error 2 (ValidateNoRefAccessInRender): reading ref.current during render
|
||||
* Error 3 (InferMutationAliasingEffects): Mutation of frozen props
|
||||
*/
|
||||
function Component(props) {
|
||||
const ref = useRef(null);
|
||||
|
||||
// Error: try/finally (Todo from BuildHIR)
|
||||
try {
|
||||
doWork();
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
|
||||
// Error: reading ref during render
|
||||
const value = ref.current;
|
||||
|
||||
// Error: mutating frozen props
|
||||
props.items = [];
|
||||
|
||||
return <div>{value}</div>;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Fault tolerance test: two independent errors should both be reported.
|
||||
*
|
||||
* Error 1 (BuildHIR): `var` declarations are not supported (treated as `let`)
|
||||
* Error 2 (InferMutationAliasingEffects): Mutation of frozen props
|
||||
*/
|
||||
function Component(props) {
|
||||
// Error: var declaration (Todo from BuildHIR)
|
||||
var items = props.items;
|
||||
|
||||
// Error: mutating frozen props (detected during inference)
|
||||
props.items = [];
|
||||
|
||||
return <div>{items.length}</div>;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 2 errors:
|
||||
|
||||
Todo: (BuildHIR::lowerStatement) Handle var kinds in VariableDeclaration
|
||||
|
||||
error.var-declaration-and-mutation-of-props.ts:9:2
|
||||
7 | function Component(props) {
|
||||
8 | // Error: var declaration (Todo from BuildHIR)
|
||||
> 9 | var items = props.items;
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ (BuildHIR::lowerStatement) Handle var kinds in VariableDeclaration
|
||||
10 |
|
||||
11 | // Error: mutating frozen props (detected during inference)
|
||||
12 | props.items = [];
|
||||
|
||||
Error: This value cannot be modified
|
||||
|
||||
Modifying component props or hook arguments is not allowed. Consider using a local variable instead.
|
||||
|
||||
error.var-declaration-and-mutation-of-props.ts:12:2
|
||||
10 |
|
||||
11 | // Error: mutating frozen props (detected during inference)
|
||||
> 12 | props.items = [];
|
||||
| ^^^^^ value cannot be modified
|
||||
13 |
|
||||
14 | return <div>{items.length}</div>;
|
||||
15 | }
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Fault tolerance test: two independent errors should both be reported.
|
||||
*
|
||||
* Error 1 (BuildHIR): `var` declarations are not supported (treated as `let`)
|
||||
* Error 2 (InferMutationAliasingEffects): Mutation of frozen props
|
||||
*/
|
||||
function Component(props) {
|
||||
// Error: var declaration (Todo from BuildHIR)
|
||||
var items = props.items;
|
||||
|
||||
// Error: mutating frozen props (detected during inference)
|
||||
props.items = [];
|
||||
|
||||
return <div>{items.length}</div>;
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateRefAccessDuringRender
|
||||
/**
|
||||
* Fault tolerance test: two independent errors should both be reported.
|
||||
*
|
||||
* Error 1 (BuildHIR): `var` declarations are not supported (treated as `let`)
|
||||
* Error 2 (ValidateNoRefAccessInRender): reading ref.current during render
|
||||
*/
|
||||
function Component() {
|
||||
const ref = useRef(null);
|
||||
|
||||
// Error: var declaration (Todo from BuildHIR)
|
||||
var items = [1, 2, 3];
|
||||
|
||||
// Error: reading ref during render
|
||||
const value = ref.current;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{value}
|
||||
{items.length}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 2 errors:
|
||||
|
||||
Todo: (BuildHIR::lowerStatement) Handle var kinds in VariableDeclaration
|
||||
|
||||
error.var-declaration-and-ref-access.ts:12:2
|
||||
10 |
|
||||
11 | // Error: var declaration (Todo from BuildHIR)
|
||||
> 12 | var items = [1, 2, 3];
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^ (BuildHIR::lowerStatement) Handle var kinds in VariableDeclaration
|
||||
13 |
|
||||
14 | // Error: reading ref during render
|
||||
15 | const value = ref.current;
|
||||
|
||||
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.var-declaration-and-ref-access.ts:15:16
|
||||
13 |
|
||||
14 | // Error: reading ref during render
|
||||
> 15 | const value = ref.current;
|
||||
| ^^^^^^^^^^^ Cannot access ref value during render
|
||||
16 |
|
||||
17 | return (
|
||||
18 | <div>
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
// @validateRefAccessDuringRender
|
||||
/**
|
||||
* Fault tolerance test: two independent errors should both be reported.
|
||||
*
|
||||
* Error 1 (BuildHIR): `var` declarations are not supported (treated as `let`)
|
||||
* Error 2 (ValidateNoRefAccessInRender): reading ref.current during render
|
||||
*/
|
||||
function Component() {
|
||||
const ref = useRef(null);
|
||||
|
||||
// Error: var declaration (Todo from BuildHIR)
|
||||
var items = [1, 2, 3];
|
||||
|
||||
// Error: reading ref during render
|
||||
const value = ref.current;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{value}
|
||||
{items.length}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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