Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8efe0fc7ab | ||
|
|
2e0927dc70 | ||
|
|
9075330979 | ||
|
|
8a33fb3a1c | ||
|
|
cebe42e245 | ||
|
|
d6558f36e2 | ||
|
|
59d7c27087 | ||
|
|
9b2d8013ee | ||
|
|
e3e5d95cc4 | ||
|
|
426a394845 | ||
|
|
eca778cf8b | ||
|
|
0dbb43bc57 | ||
|
|
8b6b11f703 | ||
|
|
ab18f33d46 |
@@ -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',
|
||||
]);
|
||||
|
||||
@@ -91,7 +91,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';
|
||||
@@ -261,10 +260,6 @@ function runWithEnvironment(
|
||||
env.logErrors(validateNoJSXInTryStatement(hir));
|
||||
}
|
||||
|
||||
if (env.config.validateNoImpureFunctionsInRender) {
|
||||
validateNoImpureFunctionsInRender(hir);
|
||||
}
|
||||
|
||||
validateNoFreezingKnownMutableFunctions(hir);
|
||||
}
|
||||
|
||||
@@ -500,7 +495,6 @@ function runWithEnvironment(
|
||||
value: reactiveFunction,
|
||||
});
|
||||
|
||||
|
||||
if (
|
||||
env.config.enablePreserveExistingMemoizationGuarantees ||
|
||||
env.config.validatePreserveExistingMemoizationGuarantees
|
||||
|
||||
@@ -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 = {
|
||||
@@ -738,7 +731,6 @@ function tryCompileFunction(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Applies React Compiler generated functions to the babel AST by replacing
|
||||
* existing functions in place or inserting new declarations.
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,7 +7,12 @@
|
||||
|
||||
import {Binding, NodePath} from '@babel/traverse';
|
||||
import * as t from '@babel/types';
|
||||
import {CompilerError, ErrorCategory} from '../CompilerError';
|
||||
import {
|
||||
CompilerError,
|
||||
CompilerDiagnostic,
|
||||
CompilerErrorDetail,
|
||||
ErrorCategory,
|
||||
} from '../CompilerError';
|
||||
import {Environment} from './Environment';
|
||||
import {
|
||||
BasicBlock,
|
||||
@@ -110,7 +115,6 @@ export default class HIRBuilder {
|
||||
#bindings: Bindings;
|
||||
#env: Environment;
|
||||
#exceptionHandlerStack: Array<BlockId> = [];
|
||||
errors: CompilerError = new CompilerError();
|
||||
/**
|
||||
* Traversal context: counts the number of `fbt` tag parents
|
||||
* of the current babel node.
|
||||
@@ -148,6 +152,10 @@ export default class HIRBuilder {
|
||||
this.#current = newBlock(this.#entry, options?.entryBlockKind ?? 'block');
|
||||
}
|
||||
|
||||
recordError(error: CompilerDiagnostic | CompilerErrorDetail): void {
|
||||
this.#env.recordError(error);
|
||||
}
|
||||
|
||||
currentBlockKind(): BlockKind {
|
||||
return this.#current.kind;
|
||||
}
|
||||
@@ -308,24 +316,28 @@ export default class HIRBuilder {
|
||||
|
||||
resolveBinding(node: t.Identifier): Identifier {
|
||||
if (node.name === 'fbt') {
|
||||
this.errors.push({
|
||||
category: ErrorCategory.Todo,
|
||||
reason: 'Support local variables named `fbt`',
|
||||
description:
|
||||
'Local variables named `fbt` may conflict with the fbt plugin and are not yet supported',
|
||||
loc: node.loc ?? GeneratedSource,
|
||||
suggestions: null,
|
||||
});
|
||||
this.recordError(
|
||||
new CompilerErrorDetail({
|
||||
category: ErrorCategory.Todo,
|
||||
reason: 'Support local variables named `fbt`',
|
||||
description:
|
||||
'Local variables named `fbt` may conflict with the fbt plugin and are not yet supported',
|
||||
loc: node.loc ?? GeneratedSource,
|
||||
suggestions: null,
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (node.name === 'this') {
|
||||
this.errors.push({
|
||||
category: ErrorCategory.UnsupportedSyntax,
|
||||
reason: '`this` is not supported syntax',
|
||||
description:
|
||||
'React Compiler does not support compiling functions that use `this`',
|
||||
loc: node.loc ?? GeneratedSource,
|
||||
suggestions: null,
|
||||
});
|
||||
this.recordError(
|
||||
new CompilerErrorDetail({
|
||||
category: ErrorCategory.UnsupportedSyntax,
|
||||
reason: '`this` is not supported syntax',
|
||||
description:
|
||||
'React Compiler does not support compiling functions that use `this`',
|
||||
loc: node.loc ?? GeneratedSource,
|
||||
suggestions: null,
|
||||
}),
|
||||
);
|
||||
}
|
||||
const originalName = node.name;
|
||||
let name = originalName;
|
||||
@@ -371,13 +383,15 @@ export default class HIRBuilder {
|
||||
instr => instr.value.kind === 'FunctionExpression',
|
||||
)
|
||||
) {
|
||||
this.errors.push({
|
||||
reason: `Support functions with unreachable code that may contain hoisted declarations`,
|
||||
loc: block.instructions[0]?.loc ?? block.terminal.loc,
|
||||
description: null,
|
||||
suggestions: null,
|
||||
category: ErrorCategory.Todo,
|
||||
});
|
||||
this.recordError(
|
||||
new CompilerErrorDetail({
|
||||
reason: `Support functions with unreachable code that may contain hoisted declarations`,
|
||||
loc: block.instructions[0]?.loc ?? block.terminal.loc,
|
||||
description: null,
|
||||
suggestions: null,
|
||||
category: ErrorCategory.Todo,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
ir.blocks = rpoBlocks;
|
||||
|
||||
@@ -293,7 +293,7 @@ function extractManualMemoizationArgs(
|
||||
instr: TInstruction<CallExpression> | TInstruction<MethodCall>,
|
||||
kind: 'useCallback' | 'useMemo',
|
||||
sidemap: IdentifierSidemap,
|
||||
errors: CompilerError,
|
||||
env: Environment,
|
||||
): {
|
||||
fnPlace: Place;
|
||||
depsList: Array<ManualMemoDependency> | null;
|
||||
@@ -303,7 +303,7 @@ function extractManualMemoizationArgs(
|
||||
Place | SpreadPattern | undefined
|
||||
>;
|
||||
if (fnPlace == null || fnPlace.kind !== 'Identifier') {
|
||||
errors.pushDiagnostic(
|
||||
env.recordError(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.UseMemo,
|
||||
reason: `Expected a callback function to be passed to ${kind}`,
|
||||
@@ -335,7 +335,7 @@ function extractManualMemoizationArgs(
|
||||
? sidemap.maybeDepsLists.get(depsListPlace.identifier.id)
|
||||
: null;
|
||||
if (maybeDepsList == null) {
|
||||
errors.pushDiagnostic(
|
||||
env.recordError(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.UseMemo,
|
||||
reason: `Expected the dependency list for ${kind} to be an array literal`,
|
||||
@@ -354,7 +354,7 @@ function extractManualMemoizationArgs(
|
||||
for (const dep of maybeDepsList.deps) {
|
||||
const maybeDep = sidemap.maybeDeps.get(dep.identifier.id);
|
||||
if (maybeDep == null) {
|
||||
errors.pushDiagnostic(
|
||||
env.recordError(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.UseMemo,
|
||||
reason: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
|
||||
@@ -389,7 +389,6 @@ function extractManualMemoizationArgs(
|
||||
* is only used for memoizing values and not for running arbitrary side effects.
|
||||
*/
|
||||
export function dropManualMemoization(func: HIRFunction): void {
|
||||
const errors = new CompilerError();
|
||||
const isValidationEnabled =
|
||||
func.env.config.validatePreserveExistingMemoizationGuarantees ||
|
||||
func.env.config.validateNoSetStateInRender ||
|
||||
@@ -436,7 +435,7 @@ export function dropManualMemoization(func: HIRFunction): void {
|
||||
instr as TInstruction<CallExpression> | TInstruction<MethodCall>,
|
||||
manualMemo.kind,
|
||||
sidemap,
|
||||
errors,
|
||||
func.env,
|
||||
);
|
||||
|
||||
if (memoDetails == null) {
|
||||
@@ -464,7 +463,7 @@ export function dropManualMemoization(func: HIRFunction): void {
|
||||
* is rare and likely sketchy.
|
||||
*/
|
||||
if (!sidemap.functions.has(fnPlace.identifier.id)) {
|
||||
errors.pushDiagnostic(
|
||||
func.env.recordError(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.UseMemo,
|
||||
reason: `Expected the first argument to be an inline function expression`,
|
||||
@@ -549,10 +548,6 @@ export function dropManualMemoization(func: HIRFunction): void {
|
||||
markInstructionIds(func.body);
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.hasAnyErrors()) {
|
||||
func.env.recordErrors(errors);
|
||||
}
|
||||
}
|
||||
|
||||
function findOptionalPlaces(fn: HIRFunction): Set<IdentifierId> {
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
Place,
|
||||
isPrimitiveType,
|
||||
} from '../HIR/HIR';
|
||||
import {Environment} from '../HIR/Environment';
|
||||
import {
|
||||
eachInstructionLValue,
|
||||
eachInstructionValueOperand,
|
||||
@@ -107,7 +108,7 @@ export function inferMutationAliasingRanges(
|
||||
|
||||
let index = 0;
|
||||
|
||||
const errors = new CompilerError();
|
||||
const shouldRecordErrors = !isFunctionExpression && fn.env.enableValidations;
|
||||
|
||||
for (const param of [...fn.params, ...fn.context, fn.returns]) {
|
||||
const place = param.kind === 'Identifier' ? param : param.place;
|
||||
@@ -200,7 +201,9 @@ export function inferMutationAliasingRanges(
|
||||
effect.kind === 'MutateGlobal' ||
|
||||
effect.kind === 'Impure'
|
||||
) {
|
||||
errors.pushDiagnostic(effect.error);
|
||||
if (shouldRecordErrors) {
|
||||
fn.env.recordError(effect.error);
|
||||
}
|
||||
functionEffects.push(effect);
|
||||
} else if (effect.kind === 'Render') {
|
||||
renders.push({index: index++, place: effect.place});
|
||||
@@ -245,11 +248,15 @@ export function inferMutationAliasingRanges(
|
||||
mutation.kind,
|
||||
mutation.place.loc,
|
||||
mutation.reason,
|
||||
errors,
|
||||
shouldRecordErrors ? fn.env : null,
|
||||
);
|
||||
}
|
||||
for (const render of renders) {
|
||||
state.render(render.index, render.place.identifier, errors);
|
||||
state.render(
|
||||
render.index,
|
||||
render.place.identifier,
|
||||
shouldRecordErrors ? fn.env : null,
|
||||
);
|
||||
}
|
||||
for (const param of [...fn.context, ...fn.params]) {
|
||||
const place = param.kind === 'Identifier' ? param : param.place;
|
||||
@@ -498,7 +505,6 @@ export function inferMutationAliasingRanges(
|
||||
* would be transitively mutated needs a capture relationship.
|
||||
*/
|
||||
const tracked: Array<Place> = [];
|
||||
const ignoredErrors = new CompilerError();
|
||||
for (const param of [...fn.params, ...fn.context, fn.returns]) {
|
||||
const place = param.kind === 'Identifier' ? param : param.place;
|
||||
tracked.push(place);
|
||||
@@ -513,7 +519,7 @@ export function inferMutationAliasingRanges(
|
||||
MutationKind.Conditional,
|
||||
into.loc,
|
||||
null,
|
||||
ignoredErrors,
|
||||
null,
|
||||
);
|
||||
for (const from of tracked) {
|
||||
if (
|
||||
@@ -547,23 +553,17 @@ export function inferMutationAliasingRanges(
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
errors.hasAnyErrors() &&
|
||||
!isFunctionExpression &&
|
||||
fn.env.enableValidations
|
||||
) {
|
||||
fn.env.recordErrors(errors);
|
||||
}
|
||||
return functionEffects;
|
||||
}
|
||||
|
||||
function appendFunctionErrors(errors: CompilerError, fn: HIRFunction): void {
|
||||
function appendFunctionErrors(env: Environment | null, fn: HIRFunction): void {
|
||||
if (env == null) return;
|
||||
for (const effect of fn.aliasingEffects ?? []) {
|
||||
switch (effect.kind) {
|
||||
case 'Impure':
|
||||
case 'MutateFrozen':
|
||||
case 'MutateGlobal': {
|
||||
errors.pushDiagnostic(effect.error);
|
||||
env.recordError(effect.error);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -664,7 +664,7 @@ class AliasingState {
|
||||
}
|
||||
}
|
||||
|
||||
render(index: number, start: Identifier, errors: CompilerError): void {
|
||||
render(index: number, start: Identifier, env: Environment | null): void {
|
||||
const seen = new Set<Identifier>();
|
||||
const queue: Array<Identifier> = [start];
|
||||
while (queue.length !== 0) {
|
||||
@@ -678,7 +678,7 @@ class AliasingState {
|
||||
continue;
|
||||
}
|
||||
if (node.value.kind === 'Function') {
|
||||
appendFunctionErrors(errors, node.value.function);
|
||||
appendFunctionErrors(env, node.value.function);
|
||||
}
|
||||
for (const [alias, when] of node.createdFrom) {
|
||||
if (when >= index) {
|
||||
@@ -710,7 +710,7 @@ class AliasingState {
|
||||
startKind: MutationKind,
|
||||
loc: SourceLocation,
|
||||
reason: MutationReason | null,
|
||||
errors: CompilerError,
|
||||
env: Environment | null,
|
||||
): void {
|
||||
const seen = new Map<Identifier, MutationKind>();
|
||||
const queue: Array<{
|
||||
@@ -742,7 +742,7 @@ class AliasingState {
|
||||
node.transitive == null &&
|
||||
node.local == null
|
||||
) {
|
||||
appendFunctionErrors(errors, node.value.function);
|
||||
appendFunctionErrors(env, node.value.function);
|
||||
}
|
||||
if (transitive) {
|
||||
if (node.transitive == null || node.transitive.kind < kind) {
|
||||
|
||||
@@ -13,7 +13,11 @@ import {
|
||||
pruneUnusedLabels,
|
||||
renameVariables,
|
||||
} from '.';
|
||||
import {CompilerError, ErrorCategory} from '../CompilerError';
|
||||
import {
|
||||
CompilerError,
|
||||
CompilerErrorDetail,
|
||||
ErrorCategory,
|
||||
} from '../CompilerError';
|
||||
import {Environment, ExternalFunction} from '../HIR';
|
||||
import {
|
||||
ArrayPattern,
|
||||
@@ -347,10 +351,6 @@ function codegenReactiveFunction(
|
||||
}
|
||||
}
|
||||
|
||||
if (cx.errors.hasAnyErrors()) {
|
||||
fn.env.recordErrors(cx.errors);
|
||||
}
|
||||
|
||||
const countMemoBlockVisitor = new CountMemoBlockVisitor(fn.env);
|
||||
visitReactiveFunction(fn, countMemoBlockVisitor, undefined);
|
||||
|
||||
@@ -420,7 +420,6 @@ class Context {
|
||||
*/
|
||||
#declarations: Set<DeclarationId> = new Set();
|
||||
temp: Temporaries;
|
||||
errors: CompilerError = new CompilerError();
|
||||
objectMethods: Map<IdentifierId, ObjectMethod> = new Map();
|
||||
uniqueIdentifiers: Set<string>;
|
||||
fbtOperands: Set<IdentifierId>;
|
||||
@@ -439,6 +438,10 @@ class Context {
|
||||
this.fbtOperands = fbtOperands;
|
||||
this.temp = temporaries !== null ? new Map(temporaries) : new Map();
|
||||
}
|
||||
|
||||
recordError(error: CompilerErrorDetail): void {
|
||||
this.env.recordError(error);
|
||||
}
|
||||
get nextCacheIndex(): number {
|
||||
return this.#nextCacheIndex++;
|
||||
}
|
||||
@@ -775,12 +778,14 @@ function codegenTerminal(
|
||||
loc: terminal.init.loc,
|
||||
});
|
||||
if (terminal.init.instructions.length !== 2) {
|
||||
cx.errors.push({
|
||||
reason: 'Support non-trivial for..in inits',
|
||||
category: ErrorCategory.Todo,
|
||||
loc: terminal.init.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
cx.recordError(
|
||||
new CompilerErrorDetail({
|
||||
reason: 'Support non-trivial for..in inits',
|
||||
category: ErrorCategory.Todo,
|
||||
loc: terminal.init.loc,
|
||||
suggestions: null,
|
||||
}),
|
||||
);
|
||||
return t.emptyStatement();
|
||||
}
|
||||
const iterableCollection = terminal.init.instructions[0];
|
||||
@@ -796,12 +801,14 @@ function codegenTerminal(
|
||||
break;
|
||||
}
|
||||
case 'StoreContext': {
|
||||
cx.errors.push({
|
||||
reason: 'Support non-trivial for..in inits',
|
||||
category: ErrorCategory.Todo,
|
||||
loc: terminal.init.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
cx.recordError(
|
||||
new CompilerErrorDetail({
|
||||
reason: 'Support non-trivial for..in inits',
|
||||
category: ErrorCategory.Todo,
|
||||
loc: terminal.init.loc,
|
||||
suggestions: null,
|
||||
}),
|
||||
);
|
||||
return t.emptyStatement();
|
||||
}
|
||||
default:
|
||||
@@ -872,12 +879,14 @@ function codegenTerminal(
|
||||
loc: terminal.test.loc,
|
||||
});
|
||||
if (terminal.test.instructions.length !== 2) {
|
||||
cx.errors.push({
|
||||
reason: 'Support non-trivial for..of inits',
|
||||
category: ErrorCategory.Todo,
|
||||
loc: terminal.init.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
cx.recordError(
|
||||
new CompilerErrorDetail({
|
||||
reason: 'Support non-trivial for..of inits',
|
||||
category: ErrorCategory.Todo,
|
||||
loc: terminal.init.loc,
|
||||
suggestions: null,
|
||||
}),
|
||||
);
|
||||
return t.emptyStatement();
|
||||
}
|
||||
const iterableItem = terminal.test.instructions[1];
|
||||
@@ -892,12 +901,14 @@ function codegenTerminal(
|
||||
break;
|
||||
}
|
||||
case 'StoreContext': {
|
||||
cx.errors.push({
|
||||
reason: 'Support non-trivial for..of inits',
|
||||
category: ErrorCategory.Todo,
|
||||
loc: terminal.init.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
cx.recordError(
|
||||
new CompilerErrorDetail({
|
||||
reason: 'Support non-trivial for..of inits',
|
||||
category: ErrorCategory.Todo,
|
||||
loc: terminal.init.loc,
|
||||
suggestions: null,
|
||||
}),
|
||||
);
|
||||
return t.emptyStatement();
|
||||
}
|
||||
default:
|
||||
@@ -1957,22 +1968,26 @@ function codegenInstructionValue(
|
||||
} else {
|
||||
if (t.isVariableDeclaration(stmt)) {
|
||||
const declarator = stmt.declarations[0];
|
||||
cx.errors.push({
|
||||
reason: `(CodegenReactiveFunction::codegenInstructionValue) Cannot declare variables in a value block, tried to declare '${
|
||||
(declarator.id as t.Identifier).name
|
||||
}'`,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: declarator.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
cx.recordError(
|
||||
new CompilerErrorDetail({
|
||||
reason: `(CodegenReactiveFunction::codegenInstructionValue) Cannot declare variables in a value block, tried to declare '${
|
||||
(declarator.id as t.Identifier).name
|
||||
}'`,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: declarator.loc ?? null,
|
||||
suggestions: null,
|
||||
}),
|
||||
);
|
||||
return t.stringLiteral(`TODO handle ${declarator.id}`);
|
||||
} else {
|
||||
cx.errors.push({
|
||||
reason: `(CodegenReactiveFunction::codegenInstructionValue) Handle conversion of ${stmt.type} to expression`,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: stmt.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
cx.recordError(
|
||||
new CompilerErrorDetail({
|
||||
reason: `(CodegenReactiveFunction::codegenInstructionValue) Handle conversion of ${stmt.type} to expression`,
|
||||
category: ErrorCategory.Todo,
|
||||
loc: stmt.loc ?? null,
|
||||
suggestions: null,
|
||||
}),
|
||||
);
|
||||
return t.stringLiteral(`TODO handle ${stmt.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,7 +102,6 @@ export function validateExhaustiveDependencies(fn: HIRFunction): void {
|
||||
loc: place.loc,
|
||||
});
|
||||
}
|
||||
const error = new CompilerError();
|
||||
let startMemo: StartMemoize | null = null;
|
||||
|
||||
function onStartMemoize(
|
||||
@@ -143,7 +142,7 @@ export function validateExhaustiveDependencies(fn: HIRFunction): void {
|
||||
'all',
|
||||
);
|
||||
if (diagnostic != null) {
|
||||
error.pushDiagnostic(diagnostic);
|
||||
fn.env.recordError(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,15 +207,12 @@ export function validateExhaustiveDependencies(fn: HIRFunction): void {
|
||||
effectReportMode,
|
||||
);
|
||||
if (diagnostic != null) {
|
||||
error.pushDiagnostic(diagnostic);
|
||||
fn.env.recordError(diagnostic);
|
||||
}
|
||||
},
|
||||
},
|
||||
false, // isFunctionExpression
|
||||
);
|
||||
if (error.hasAnyErrors()) {
|
||||
fn.env.recordErrors(error);
|
||||
}
|
||||
}
|
||||
|
||||
function validateDependencies(
|
||||
|
||||
@@ -6,13 +6,9 @@
|
||||
*/
|
||||
|
||||
import * as t from '@babel/types';
|
||||
import {
|
||||
CompilerError,
|
||||
CompilerErrorDetail,
|
||||
ErrorCategory,
|
||||
} from '../CompilerError';
|
||||
import {CompilerErrorDetail, ErrorCategory} from '../CompilerError';
|
||||
import {computeUnconditionalBlocks} from '../HIR/ComputeUnconditionalBlocks';
|
||||
import {isHookName} from '../HIR/Environment';
|
||||
import {Environment, isHookName} from '../HIR/Environment';
|
||||
import {
|
||||
HIRFunction,
|
||||
IdentifierId,
|
||||
@@ -90,15 +86,14 @@ function joinKinds(a: Kind, b: Kind): Kind {
|
||||
export function validateHooksUsage(fn: HIRFunction): void {
|
||||
const unconditionalBlocks = computeUnconditionalBlocks(fn);
|
||||
|
||||
const errors = new CompilerError();
|
||||
const errorsByPlace = new Map<t.SourceLocation, CompilerErrorDetail>();
|
||||
|
||||
function recordError(
|
||||
function trackError(
|
||||
loc: SourceLocation,
|
||||
errorDetail: CompilerErrorDetail,
|
||||
): void {
|
||||
if (typeof loc === 'symbol') {
|
||||
errors.pushErrorDetail(errorDetail);
|
||||
fn.env.recordError(errorDetail);
|
||||
} else {
|
||||
errorsByPlace.set(loc, errorDetail);
|
||||
}
|
||||
@@ -118,7 +113,7 @@ export function validateHooksUsage(fn: HIRFunction): void {
|
||||
* If that same place is also used as a conditional call, upgrade the error to a conditonal hook error
|
||||
*/
|
||||
if (previousError === undefined || previousError.reason !== reason) {
|
||||
recordError(
|
||||
trackError(
|
||||
place.loc,
|
||||
new CompilerErrorDetail({
|
||||
category: ErrorCategory.Hooks,
|
||||
@@ -134,7 +129,7 @@ export function validateHooksUsage(fn: HIRFunction): void {
|
||||
const previousError =
|
||||
typeof place.loc !== 'symbol' ? errorsByPlace.get(place.loc) : undefined;
|
||||
if (previousError === undefined) {
|
||||
recordError(
|
||||
trackError(
|
||||
place.loc,
|
||||
new CompilerErrorDetail({
|
||||
category: ErrorCategory.Hooks,
|
||||
@@ -151,7 +146,7 @@ export function validateHooksUsage(fn: HIRFunction): void {
|
||||
const previousError =
|
||||
typeof place.loc !== 'symbol' ? errorsByPlace.get(place.loc) : undefined;
|
||||
if (previousError === undefined) {
|
||||
recordError(
|
||||
trackError(
|
||||
place.loc,
|
||||
new CompilerErrorDetail({
|
||||
category: ErrorCategory.Hooks,
|
||||
@@ -396,7 +391,7 @@ export function validateHooksUsage(fn: HIRFunction): void {
|
||||
}
|
||||
case 'ObjectMethod':
|
||||
case 'FunctionExpression': {
|
||||
visitFunctionExpression(errors, instr.value.loweredFunc.func);
|
||||
visitFunctionExpression(fn.env, instr.value.loweredFunc.func);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
@@ -421,20 +416,17 @@ export function validateHooksUsage(fn: HIRFunction): void {
|
||||
}
|
||||
|
||||
for (const [, error] of errorsByPlace) {
|
||||
errors.pushErrorDetail(error);
|
||||
}
|
||||
if (errors.hasAnyErrors()) {
|
||||
fn.env.recordErrors(errors);
|
||||
fn.env.recordError(error);
|
||||
}
|
||||
}
|
||||
|
||||
function visitFunctionExpression(errors: CompilerError, fn: HIRFunction): void {
|
||||
function visitFunctionExpression(env: Environment, fn: HIRFunction): void {
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
for (const instr of block.instructions) {
|
||||
switch (instr.value.kind) {
|
||||
case 'ObjectMethod':
|
||||
case 'FunctionExpression': {
|
||||
visitFunctionExpression(errors, instr.value.loweredFunc.func);
|
||||
visitFunctionExpression(env, instr.value.loweredFunc.func);
|
||||
break;
|
||||
}
|
||||
case 'MethodCall':
|
||||
@@ -445,7 +437,7 @@ function visitFunctionExpression(errors: CompilerError, fn: HIRFunction): void {
|
||||
: instr.value.property;
|
||||
const hookKind = getHookKind(fn.env, callee.identifier);
|
||||
if (hookKind != null) {
|
||||
errors.pushErrorDetail(
|
||||
env.recordError(
|
||||
new CompilerErrorDetail({
|
||||
category: ErrorCategory.Hooks,
|
||||
reason:
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerError, CompilerErrorDetail, EnvironmentConfig} from '..';
|
||||
import {CompilerErrorDetail, EnvironmentConfig} from '..';
|
||||
import {ErrorCategory} from '../CompilerError';
|
||||
import {HIRFunction, IdentifierId} from '../HIR';
|
||||
import {DEFAULT_GLOBALS} from '../HIR/Globals';
|
||||
@@ -20,7 +20,6 @@ export function validateNoCapitalizedCalls(fn: HIRFunction): void {
|
||||
return ALLOW_LIST.has(name);
|
||||
};
|
||||
|
||||
const errors = new CompilerError();
|
||||
const capitalLoadGlobals = new Map<IdentifierId, string>();
|
||||
const capitalizedProperties = new Map<IdentifierId, string>();
|
||||
const reason =
|
||||
@@ -72,20 +71,19 @@ export function validateNoCapitalizedCalls(fn: HIRFunction): void {
|
||||
const propertyIdentifier = value.property.identifier.id;
|
||||
const propertyName = capitalizedProperties.get(propertyIdentifier);
|
||||
if (propertyName != null) {
|
||||
errors.push({
|
||||
category: ErrorCategory.CapitalizedCalls,
|
||||
reason,
|
||||
description: `${propertyName} may be a component`,
|
||||
loc: value.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
fn.env.recordError(
|
||||
new CompilerErrorDetail({
|
||||
category: ErrorCategory.CapitalizedCalls,
|
||||
reason,
|
||||
description: `${propertyName} may be a component`,
|
||||
loc: value.loc,
|
||||
suggestions: null,
|
||||
}),
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (errors.hasAnyErrors()) {
|
||||
fn.env.recordErrors(errors);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
import {CompilerError, SourceLocation} from '..';
|
||||
import {ErrorCategory} from '../CompilerError';
|
||||
import {CompilerErrorDetail, ErrorCategory} from '../CompilerError';
|
||||
import {
|
||||
ArrayExpression,
|
||||
BlockId,
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
eachInstructionValueOperand,
|
||||
eachTerminalOperand,
|
||||
} from '../HIR/visitors';
|
||||
import {Environment} from '../HIR/Environment';
|
||||
|
||||
/**
|
||||
* Validates that useEffect is not used for derived computations which could/should
|
||||
@@ -49,8 +50,6 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
|
||||
const functions: Map<IdentifierId, FunctionExpression> = new Map();
|
||||
const locals: Map<IdentifierId, IdentifierId> = new Map();
|
||||
|
||||
const errors = new CompilerError();
|
||||
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
for (const instr of block.instructions) {
|
||||
const {lvalue, value} = instr;
|
||||
@@ -90,20 +89,19 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
|
||||
validateEffect(
|
||||
effectFunction.loweredFunc.func,
|
||||
dependencies,
|
||||
errors,
|
||||
fn.env,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fn.env.recordErrors(errors);
|
||||
}
|
||||
|
||||
function validateEffect(
|
||||
effectFunction: HIRFunction,
|
||||
effectDeps: Array<IdentifierId>,
|
||||
errors: CompilerError,
|
||||
env: Environment,
|
||||
): void {
|
||||
for (const operand of effectFunction.context) {
|
||||
if (isSetStateType(operand.identifier)) {
|
||||
@@ -217,13 +215,15 @@ function validateEffect(
|
||||
}
|
||||
|
||||
for (const loc of setStateLocations) {
|
||||
errors.push({
|
||||
category: ErrorCategory.EffectDerivationsOfState,
|
||||
reason:
|
||||
'Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)',
|
||||
description: null,
|
||||
loc,
|
||||
suggestions: null,
|
||||
});
|
||||
env.recordError(
|
||||
new CompilerErrorDetail({
|
||||
category: ErrorCategory.EffectDerivationsOfState,
|
||||
reason:
|
||||
'Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)',
|
||||
description: null,
|
||||
loc,
|
||||
suggestions: null,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerDiagnostic, CompilerError, Effect} from '..';
|
||||
import {CompilerDiagnostic, Effect} from '..';
|
||||
import {ErrorCategory} from '../CompilerError';
|
||||
import {
|
||||
HIRFunction,
|
||||
@@ -43,7 +43,6 @@ import {AliasingEffect} from '../Inference/AliasingEffects';
|
||||
* that are passed where a frozen value is expected and rejects them.
|
||||
*/
|
||||
export function validateNoFreezingKnownMutableFunctions(fn: HIRFunction): void {
|
||||
const errors = new CompilerError();
|
||||
const contextMutationEffects: Map<
|
||||
IdentifierId,
|
||||
Extract<AliasingEffect, {kind: 'Mutate'} | {kind: 'MutateTransitive'}>
|
||||
@@ -60,7 +59,7 @@ export function validateNoFreezingKnownMutableFunctions(fn: HIRFunction): void {
|
||||
place.identifier.name.kind === 'named'
|
||||
? `\`${place.identifier.name.value}\``
|
||||
: 'a local variable';
|
||||
errors.pushDiagnostic(
|
||||
fn.env.recordError(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Immutability,
|
||||
reason: 'Cannot modify local variables after render completes',
|
||||
@@ -159,7 +158,4 @@ export function validateNoFreezingKnownMutableFunctions(fn: HIRFunction): void {
|
||||
visitOperand(operand);
|
||||
}
|
||||
}
|
||||
if (errors.hasAnyErrors()) {
|
||||
fn.env.recordErrors(errors);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerDiagnostic, CompilerError} from '..';
|
||||
import {CompilerDiagnostic} from '..';
|
||||
import {ErrorCategory} from '../CompilerError';
|
||||
import {HIRFunction} from '../HIR';
|
||||
import {getFunctionCallSignature} from '../Inference/InferMutationAliasingEffects';
|
||||
@@ -20,7 +20,6 @@ import {getFunctionCallSignature} from '../Inference/InferMutationAliasingEffect
|
||||
* and use it here.
|
||||
*/
|
||||
export function validateNoImpureFunctionsInRender(fn: HIRFunction): void {
|
||||
const errors = new CompilerError();
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
for (const instr of block.instructions) {
|
||||
const value = instr.value;
|
||||
@@ -32,7 +31,7 @@ export function validateNoImpureFunctionsInRender(fn: HIRFunction): void {
|
||||
callee.identifier.type,
|
||||
);
|
||||
if (signature != null && signature.impure === true) {
|
||||
errors.pushDiagnostic(
|
||||
fn.env.recordError(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Purity,
|
||||
reason: 'Cannot call impure function during render',
|
||||
@@ -52,7 +51,4 @@ export function validateNoImpureFunctionsInRender(fn: HIRFunction): void {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (errors.hasAnyErrors()) {
|
||||
fn.env.recordErrors(errors);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,8 +124,8 @@ export function validateNoRefAccessInRender(fn: HIRFunction): void {
|
||||
collectTemporariesSidemap(fn, env);
|
||||
const errors = new CompilerError();
|
||||
validateNoRefAccessInRenderImpl(fn, env, errors);
|
||||
if (errors.hasAnyErrors()) {
|
||||
fn.env.recordErrors(errors);
|
||||
for (const detail of errors.details) {
|
||||
fn.env.recordError(detail);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -48,8 +48,8 @@ export function validateNoSetStateInRender(fn: HIRFunction): void {
|
||||
fn,
|
||||
unconditionalSetStateFunctions,
|
||||
);
|
||||
if (errors.hasAnyErrors()) {
|
||||
fn.env.recordErrors(errors);
|
||||
for (const detail of errors.details) {
|
||||
fn.env.recordError(detail);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
ScopeId,
|
||||
SourceLocation,
|
||||
} from '../HIR';
|
||||
import {Environment} from '../HIR/Environment';
|
||||
import {printIdentifier, printManualMemoDependency} from '../HIR/PrintHIR';
|
||||
import {
|
||||
eachInstructionValueLValue,
|
||||
@@ -48,11 +49,10 @@ import {getOrInsertDefault} from '../Utils/utils';
|
||||
*/
|
||||
export function validatePreservedManualMemoization(fn: ReactiveFunction): void {
|
||||
const state = {
|
||||
errors: new CompilerError(),
|
||||
env: fn.env,
|
||||
manualMemoState: null,
|
||||
};
|
||||
visitReactiveFunction(fn, new Visitor(), state);
|
||||
fn.env.recordErrors(state.errors);
|
||||
}
|
||||
|
||||
const DEBUG = false;
|
||||
@@ -110,7 +110,7 @@ type ManualMemoBlockState = {
|
||||
};
|
||||
|
||||
type VisitorState = {
|
||||
errors: CompilerError;
|
||||
env: Environment;
|
||||
manualMemoState: ManualMemoBlockState | null;
|
||||
};
|
||||
|
||||
@@ -230,7 +230,7 @@ function validateInferredDep(
|
||||
temporaries: Map<IdentifierId, ManualMemoDependency>,
|
||||
declsWithinMemoBlock: Set<DeclarationId>,
|
||||
validDepsInMemoBlock: Array<ManualMemoDependency>,
|
||||
errorState: CompilerError,
|
||||
env: Environment,
|
||||
memoLocation: SourceLocation,
|
||||
): void {
|
||||
let normalizedDep: ManualMemoDependency;
|
||||
@@ -280,7 +280,7 @@ function validateInferredDep(
|
||||
errorDiagnostic = merge(errorDiagnostic ?? compareResult, compareResult);
|
||||
}
|
||||
}
|
||||
errorState.pushDiagnostic(
|
||||
env.recordError(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.PreserveManualMemo,
|
||||
reason: 'Existing memoization could not be preserved',
|
||||
@@ -426,7 +426,7 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
|
||||
this.temporaries,
|
||||
state.manualMemoState.decls,
|
||||
state.manualMemoState.depsFromSource,
|
||||
state.errors,
|
||||
state.env,
|
||||
state.manualMemoState.loc,
|
||||
);
|
||||
}
|
||||
@@ -529,7 +529,7 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
|
||||
!this.scopes.has(identifier.scope.id) &&
|
||||
!this.prunedScopes.has(identifier.scope.id)
|
||||
) {
|
||||
state.errors.pushDiagnostic(
|
||||
state.env.recordError(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.PreserveManualMemo,
|
||||
reason: 'Existing memoization could not be preserved',
|
||||
@@ -575,7 +575,7 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
|
||||
|
||||
for (const identifier of decls) {
|
||||
if (isUnmemoized(identifier, this.scopes)) {
|
||||
state.errors.pushDiagnostic(
|
||||
state.env.recordError(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.PreserveManualMemo,
|
||||
reason: 'Existing memoization could not be preserved',
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import {NodePath} from '@babel/traverse';
|
||||
import * as t from '@babel/types';
|
||||
import {CompilerDiagnostic, CompilerError, ErrorCategory} from '..';
|
||||
import {CompilerDiagnostic, ErrorCategory} from '..';
|
||||
import {CodegenFunction} from '../ReactiveScopes';
|
||||
import {Environment} from '../HIR/Environment';
|
||||
|
||||
@@ -125,8 +125,6 @@ export function validateSourceLocations(
|
||||
generatedAst: CodegenFunction,
|
||||
env: Environment,
|
||||
): void {
|
||||
const errors = new CompilerError();
|
||||
|
||||
/*
|
||||
* Step 1: Collect important locations from the original source
|
||||
* Note: Multiple node types can share the same location (e.g. VariableDeclarator and Identifier)
|
||||
@@ -241,7 +239,7 @@ export function validateSourceLocations(
|
||||
loc: t.SourceLocation,
|
||||
nodeType: string,
|
||||
): void => {
|
||||
errors.pushDiagnostic(
|
||||
env.recordError(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Todo,
|
||||
reason: 'Important source location missing in generated code',
|
||||
@@ -261,7 +259,7 @@ export function validateSourceLocations(
|
||||
expectedType: string,
|
||||
actualTypes: Set<string>,
|
||||
): void => {
|
||||
errors.pushDiagnostic(
|
||||
env.recordError(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.Todo,
|
||||
reason:
|
||||
@@ -309,6 +307,4 @@ export function validateSourceLocations(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
env.recordErrors(errors);
|
||||
}
|
||||
|
||||
@@ -16,13 +16,13 @@ import {
|
||||
IdentifierId,
|
||||
SourceLocation,
|
||||
} from '../HIR';
|
||||
import {Environment} from '../HIR/Environment';
|
||||
import {
|
||||
eachInstructionValueOperand,
|
||||
eachTerminalOperand,
|
||||
} from '../HIR/visitors';
|
||||
|
||||
export function validateUseMemo(fn: HIRFunction): void {
|
||||
const errors = new CompilerError();
|
||||
const voidMemoErrors = new CompilerError();
|
||||
const useMemos = new Set<IdentifierId>();
|
||||
const react = new Set<IdentifierId>();
|
||||
@@ -90,7 +90,7 @@ export function validateUseMemo(fn: HIRFunction): void {
|
||||
firstParam.kind === 'Identifier'
|
||||
? firstParam.loc
|
||||
: firstParam.place.loc;
|
||||
errors.pushDiagnostic(
|
||||
fn.env.recordError(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.UseMemo,
|
||||
reason: 'useMemo() callbacks may not accept parameters',
|
||||
@@ -106,7 +106,7 @@ export function validateUseMemo(fn: HIRFunction): void {
|
||||
}
|
||||
|
||||
if (body.loweredFunc.func.async || body.loweredFunc.func.generator) {
|
||||
errors.pushDiagnostic(
|
||||
fn.env.recordError(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.UseMemo,
|
||||
reason:
|
||||
@@ -122,7 +122,7 @@ export function validateUseMemo(fn: HIRFunction): void {
|
||||
);
|
||||
}
|
||||
|
||||
validateNoContextVariableAssignment(body.loweredFunc.func, errors);
|
||||
validateNoContextVariableAssignment(body.loweredFunc.func, fn.env);
|
||||
|
||||
if (fn.env.config.validateNoVoidUseMemo) {
|
||||
if (!hasNonVoidReturn(body.loweredFunc.func)) {
|
||||
@@ -176,14 +176,11 @@ export function validateUseMemo(fn: HIRFunction): void {
|
||||
}
|
||||
}
|
||||
fn.env.logErrors(voidMemoErrors.asResult());
|
||||
if (errors.hasAnyErrors()) {
|
||||
fn.env.recordErrors(errors);
|
||||
}
|
||||
}
|
||||
|
||||
function validateNoContextVariableAssignment(
|
||||
fn: HIRFunction,
|
||||
errors: CompilerError,
|
||||
env: Environment,
|
||||
): void {
|
||||
const context = new Set(fn.context.map(place => place.identifier.id));
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
@@ -192,7 +189,7 @@ function validateNoContextVariableAssignment(
|
||||
switch (value.kind) {
|
||||
case 'StoreContext': {
|
||||
if (context.has(value.lvalue.place.identifier.id)) {
|
||||
errors.pushDiagnostic(
|
||||
env.recordError(
|
||||
CompilerDiagnostic.create({
|
||||
category: ErrorCategory.UseMemo,
|
||||
reason:
|
||||
|
||||
@@ -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 |
|
||||
```
|
||||
|
||||
|
||||
@@ -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