Compare commits

...

5 Commits

Author SHA1 Message Date
Joe Savona
8efe0fc7ab [compiler] Rename mismatched variable names after type changes
Rename `state: Environment` to `env: Environment` in
ValidateMemoizedEffectDependencies visitor methods, and
`errorState: Environment` to `env: Environment` in
ValidatePreservedManualMemoization's validateInferredDep.
2026-02-23 16:13:00 -08:00
Joseph Savona
2e0927dc70 [compiler] Remove local CompilerError accumulators, emit directly to env.recordError() (#35882)
Removes unnecessary indirection in 17 compiler passes that previously
accumulated errors in a local `CompilerError` instance before flushing
them to `env.recordErrors()` at the end of each pass. Errors are now
emitted directly via `env.recordError()` as they're discovered.

For passes with recursive error-detection patterns
(ValidateNoRefAccessInRender,
ValidateNoSetStateInRender), the internal accumulator is kept but
flushed
via individual `recordError()` calls. For InferMutationAliasingRanges,
a `shouldRecordErrors` flag preserves the conditional suppression logic.
For TransformFire, the throw-based error propagation is replaced with
direct recording plus an early-exit check in Pipeline.ts.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35882).
* #35888
* #35884
* #35883
* __->__ #35882
2026-02-23 16:11:50 -08:00
Joseph Savona
9075330979 [compiler] Remove tryRecord, add catch-all error handling, fix remaining throws (#35881)
Remove `tryRecord()` from the compilation pipeline now that all passes
record
errors directly via `env.recordError()` / `env.recordErrors()`. A single
catch-all try/catch in Program.ts provides the safety net for any pass
that
incorrectly throws instead of recording.

Key changes:
- Remove all ~64 `env.tryRecord()` wrappers in Pipeline.ts
- Delete `tryRecord()` method from Environment.ts
- Add `CompileUnexpectedThrow` logger event so thrown errors are
detectable
- Log `CompileUnexpectedThrow` in Program.ts catch-all for non-invariant
throws
- Fail snap tests on `CompileUnexpectedThrow` to surface pass bugs in
dev
- Convert throwTodo/throwDiagnostic calls in HIRBuilder (fbt, this),
  CodegenReactiveFunction (for-in/for-of), and BuildReactiveFunction to
  record errors or use invariants as appropriate
- Remove try/catch from BuildHIR's lower() since inner throws are now
recorded
- CollectOptionalChainDependencies: return null instead of throwing on
  unsupported optional chain patterns (graceful optimization skip)

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35881).
* #35888
* #35884
* #35883
* #35882
* __->__ #35881
2026-02-23 16:10:17 -08:00
Joseph Savona
8a33fb3a1c [compiler] Cleanup: consistent tryRecord() wrapping and error recording (#35880)
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35880).
* #35888
* #35884
* #35883
* #35882
* #35881
* __->__ #35880
2026-02-23 16:08:04 -08:00
Joseph Savona
cebe42e245 [compiler] Add fault tolerance test fixtures (#35879)
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35879).
* #35888
* #35884
* #35883
* #35882
* #35881
* #35880
* __->__ #35879
2026-02-23 16:06:39 -08:00
43 changed files with 1456 additions and 849 deletions

View File

@@ -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.

View File

@@ -252,6 +252,7 @@ export type LoggerEvent =
| CompileErrorEvent
| CompileDiagnosticEvent
| CompileSkipEvent
| CompileUnexpectedThrowEvent
| PipelineErrorEvent
| TimingEvent;
@@ -286,6 +287,11 @@ export type PipelineErrorEvent = {
fnLoc: t.SourceLocation | null;
data: string;
};
export type CompileUnexpectedThrowEvent = {
kind: 'CompileUnexpectedThrow';
fnLoc: t.SourceLocation | null;
data: string;
};
export type TimingEvent = {
kind: 'Timing';
measurement: PerformanceMeasure;

View File

@@ -13,7 +13,6 @@ import {CompilerError} from '../CompilerError';
import {Err, Ok, Result} from '../Utils/Result';
import {
HIRFunction,
IdentifierId,
ReactiveFunction,
assertConsistentIdentifiers,
assertTerminalPredsExist,
@@ -231,9 +230,7 @@ function runWithEnvironment(
log({kind: 'hir', name: 'InferMutationAliasingRanges', value: hir});
if (env.enableValidations) {
validateLocalsNotReassignedAfterRender(hir);
}
if (env.enableValidations) {
if (env.config.assertValidMutableRanges) {
assertValidMutableRanges(hir);
}
@@ -263,9 +260,7 @@ function runWithEnvironment(
env.logErrors(validateNoJSXInTryStatement(hir));
}
env.tryRecord(() => {
validateNoFreezingKnownMutableFunctions(hir);
});
validateNoFreezingKnownMutableFunctions(hir);
}
inferReactivePlaces(hir);
@@ -306,8 +301,7 @@ function runWithEnvironment(
log({kind: 'hir', name: 'InferReactiveScopeVariables', value: hir});
}
let fbtOperands: Set<IdentifierId> = new Set();
fbtOperands = memoizeFbtAndMacroOperandsInSameScope(hir);
const fbtOperands = memoizeFbtAndMacroOperandsInSameScope(hir);
log({
kind: 'hir',
name: 'MemoizeFbtAndMacroOperandsInSameScope',
@@ -392,6 +386,7 @@ function runWithEnvironment(
});
assertTerminalSuccessorsExist(hir);
assertTerminalPredsExist(hir);
propagateScopeDependenciesHIR(hir);
log({
kind: 'hir',
@@ -399,8 +394,7 @@ function runWithEnvironment(
value: hir,
});
let reactiveFunction!: ReactiveFunction;
reactiveFunction = buildReactiveFunction(hir);
const reactiveFunction = buildReactiveFunction(hir);
log({
kind: 'reactive',
name: 'BuildReactiveFunction',
@@ -487,8 +481,7 @@ function runWithEnvironment(
value: reactiveFunction,
});
let uniqueIdentifiers: Set<string> = new Set();
uniqueIdentifiers = renameVariables(reactiveFunction);
const uniqueIdentifiers = renameVariables(reactiveFunction);
log({
kind: 'reactive',
name: 'RenameVariables',

View File

@@ -713,6 +713,20 @@ function tryCompileFunction(
return {kind: 'error', error: result.unwrapErr()};
}
} catch (err) {
/**
* A pass incorrectly threw instead of recording the error.
* Log for detection in development.
*/
if (
err instanceof CompilerError &&
err.details.every(detail => detail.category !== ErrorCategory.Invariant)
) {
programContext.logEvent({
kind: 'CompileUnexpectedThrow',
fnLoc: fn.node.loc ?? null,
data: err.toString(),
});
}
return {kind: 'error', error: err};
}
}

View File

@@ -310,16 +310,13 @@ function traverseOptionalBlock(
* - a optional base block with a separate nested optional-chain (e.g. a(c?.d)?.d)
*/
const testBlock = context.blocks.get(maybeTest.terminal.fallthrough)!;
if (testBlock!.terminal.kind !== 'branch') {
/**
* Fallthrough of the inner optional should be a block with no
* instructions, terminating with Test($<temporary written to from
* StoreLocal>)
*/
CompilerError.throwTodo({
reason: `Unexpected terminal kind \`${testBlock.terminal.kind}\` for optional fallthrough block`,
loc: maybeTest.terminal.loc,
});
/**
* Fallthrough of the inner optional should be a block with no
* instructions, terminating with Test($<temporary written to from
* StoreLocal>)
*/
if (testBlock.terminal.kind !== 'branch') {
return null;
}
/**
* Recurse into inner optional blocks to collect inner optional-chain

View File

@@ -759,29 +759,6 @@ export class Environment {
return this.#errors;
}
/**
* Wraps a callback in try/catch: if the callback throws a CompilerError
* that is NOT an invariant, the error is recorded and execution continues.
* Non-CompilerError exceptions and invariants are re-thrown.
*/
tryRecord(fn: () => void): void {
try {
fn();
} catch (err) {
if (err instanceof CompilerError) {
// Check if any detail is an invariant — if so, re-throw
for (const detail of err.details) {
if (detail.category === ErrorCategory.Invariant) {
throw err;
}
}
this.recordErrors(err);
} else {
throw err;
}
}
}
isContextIdentifier(node: t.Identifier): boolean {
return this.#contextIdentifiers.has(node);
}

View File

@@ -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,34 +316,28 @@ export default class HIRBuilder {
resolveBinding(node: t.Identifier): Identifier {
if (node.name === 'fbt') {
CompilerError.throwDiagnostic({
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',
details: [
{
kind: 'error',
message: 'Rename to avoid conflict with fbt plugin',
loc: node.loc ?? GeneratedSource,
},
],
});
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') {
CompilerError.throwDiagnostic({
category: ErrorCategory.UnsupportedSyntax,
reason: '`this` is not supported syntax',
description:
'React Compiler does not support compiling functions that use `this`',
details: [
{
kind: 'error',
message: '`this` was used here',
loc: node.loc ?? GeneratedSource,
},
],
});
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;
@@ -381,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;

View File

@@ -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> {

View File

@@ -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) {

View File

@@ -1007,11 +1007,10 @@ class Driver {
const test = this.visitValueBlock(testBlockId, loc);
const testBlock = this.cx.ir.blocks.get(test.block)!;
if (testBlock.terminal.kind !== 'branch') {
CompilerError.throwTodo({
reason: `Unexpected terminal kind \`${testBlock.terminal.kind}\` for ${terminalKind} test block`,
description: null,
CompilerError.invariant(false, {
reason: `Expected a branch terminal for ${terminalKind} test block`,
description: `Got \`${testBlock.terminal.kind}\``,
loc: testBlock.terminal.loc,
suggestions: null,
});
}
return {

View File

@@ -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,15 @@ function codegenTerminal(
loc: terminal.init.loc,
});
if (terminal.init.instructions.length !== 2) {
CompilerError.throwTodo({
reason: 'Support non-trivial for..in inits',
description: null,
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];
const iterableItem = terminal.init.instructions[1];
@@ -795,12 +801,15 @@ function codegenTerminal(
break;
}
case 'StoreContext': {
CompilerError.throwTodo({
reason: 'Support non-trivial for..in inits',
description: null,
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:
CompilerError.invariant(false, {
@@ -870,12 +879,15 @@ function codegenTerminal(
loc: terminal.test.loc,
});
if (terminal.test.instructions.length !== 2) {
CompilerError.throwTodo({
reason: 'Support non-trivial for..of inits',
description: null,
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];
let lval: t.LVal;
@@ -889,12 +901,15 @@ function codegenTerminal(
break;
}
case 'StoreContext': {
CompilerError.throwTodo({
reason: 'Support non-trivial for..of inits',
description: null,
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:
CompilerError.invariant(false, {
@@ -1953,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}`);
}
}

View File

@@ -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(

View File

@@ -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:

View File

@@ -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);
}
}

View File

@@ -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,22 +89,19 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
validateEffect(
effectFunction.loweredFunc.func,
dependencies,
errors,
fn.env,
);
}
}
}
}
}
for (const detail of errors.details) {
fn.env.recordError(detail);
}
}
function validateEffect(
effectFunction: HIRFunction,
effectDeps: Array<IdentifierId>,
errors: CompilerError,
env: Environment,
): void {
for (const operand of effectFunction.context) {
if (isSetStateType(operand.identifier)) {
@@ -219,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,
}),
);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -27,6 +27,7 @@ import {
ScopeId,
SourceLocation,
} from '../HIR';
import {Environment} from '../HIR/Environment';
import {printIdentifier, printManualMemoDependency} from '../HIR/PrintHIR';
import {
eachInstructionValueLValue,
@@ -48,13 +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);
for (const detail of state.errors.details) {
fn.env.recordError(detail);
}
}
const DEBUG = false;
@@ -112,7 +110,7 @@ type ManualMemoBlockState = {
};
type VisitorState = {
errors: CompilerError;
env: Environment;
manualMemoState: ManualMemoBlockState | null;
};
@@ -232,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;
@@ -282,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',
@@ -428,7 +426,7 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
this.temporaries,
state.manualMemoState.decls,
state.manualMemoState.depsFromSource,
state.errors,
state.env,
state.manualMemoState.loc,
);
}
@@ -531,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',
@@ -577,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',

View File

@@ -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,8 +307,4 @@ export function validateSourceLocations(
}
}
}
for (const detail of errors.details) {
env.recordError(detail);
}
}

View File

@@ -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:

View File

@@ -24,9 +24,9 @@ function useThing(fn) {
```
Found 1 error:
Invariant: [HIRBuilder] Unexpected null block
Error: Expected a non-reserved identifier name
expected block 0 to exist.
`this` is a reserved word in JavaScript and cannot be used as an identifier name.
```

View File

@@ -1,39 +0,0 @@
## Input
```javascript
function useFoo(props: {value: {x: string; y: string} | null}) {
const value = props.value;
return createArray(value?.x, value?.y)?.join(', ');
}
function createArray<T>(...args: Array<T>): Array<T> {
return args;
}
export const FIXTURE_ENTRYPONT = {
fn: useFoo,
props: [{value: null}],
};
```
## Error
```
Found 1 error:
Todo: Unexpected terminal kind `optional` for optional fallthrough block
error.todo-optional-call-chain-in-optional.ts:3:21
1 | function useFoo(props: {value: {x: string; y: string} | null}) {
2 | const value = props.value;
> 3 | return createArray(value?.x, value?.y)?.join(', ');
| ^^^^^^^^ Unexpected terminal kind `optional` for optional fallthrough block
4 | }
5 |
6 | function createArray<T>(...args: Array<T>): Array<T> {
```

View File

@@ -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 | }
```

View File

@@ -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>;
}

View File

@@ -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 | }
```

View File

@@ -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>;
}

View File

@@ -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 = [];
```

View File

@@ -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>;
}

View File

@@ -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 | }
```

View File

@@ -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>;
}

View File

@@ -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>
```

View File

@@ -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>
);
}

View File

@@ -50,7 +50,7 @@ export const FIXTURE_ENTRYPOINT = {
## Error
```
Found 1 error:
Found 4 errors:
Todo: Support local variables named `fbt`
@@ -60,10 +60,49 @@ error.todo-fbt-as-local.ts:18:19
16 |
17 | function Foo(props) {
> 18 | const getText1 = fbt =>
| ^^^ Rename to avoid conflict with fbt plugin
| ^^^ Support local variables named `fbt`
19 | fbt(
20 | `Hello, ${fbt.param('(key) name', identity(props.name))}!`,
21 | '(description) Greeting'
Todo: Support local variables named `fbt`
Local variables named `fbt` may conflict with the fbt plugin and are not yet supported.
error.todo-fbt-as-local.ts:18:19
16 |
17 | function Foo(props) {
> 18 | const getText1 = fbt =>
| ^^^ Support local variables named `fbt`
19 | fbt(
20 | `Hello, ${fbt.param('(key) name', identity(props.name))}!`,
21 | '(description) Greeting'
Todo: Support local variables named `fbt`
Local variables named `fbt` may conflict with the fbt plugin and are not yet supported.
error.todo-fbt-as-local.ts:18:19
16 |
17 | function Foo(props) {
> 18 | const getText1 = fbt =>
| ^^^ Support local variables named `fbt`
19 | fbt(
20 | `Hello, ${fbt.param('(key) name', identity(props.name))}!`,
21 | '(description) Greeting'
Todo: Support local variables named `fbt`
Local variables named `fbt` may conflict with the fbt plugin and are not yet supported.
error.todo-fbt-as-local.ts:24:19
22 | );
23 |
> 24 | const getText2 = fbt =>
| ^^^ Support local variables named `fbt`
25 | fbt(
26 | `Goodbye, ${fbt.param('(key) name', identity(props.name))}!`,
27 | '(description) Greeting2'
```

View File

@@ -16,17 +16,15 @@ function Component(props) {
```
Found 1 error:
Todo: Support local variables named `fbt`
Invariant: <fbt> tags should be module-level imports
Local variables named `fbt` may conflict with the fbt plugin and are not yet supported.
error.todo-locally-require-fbt.ts:2:8
1 | function Component(props) {
> 2 | const fbt = require('fbt');
| ^^^ Rename to avoid conflict with fbt plugin
error.todo-locally-require-fbt.ts:4:10
2 | const fbt = require('fbt');
3 |
4 | return <fbt desc="Description">{'Text'}</fbt>;
> 4 | return <fbt desc="Description">{'Text'}</fbt>;
| ^^^ <fbt> tags should be module-level imports
5 | }
6 |
```

View File

@@ -1,40 +0,0 @@
## Input
```javascript
// @enablePropagateDepsInHIR
function useFoo(props: {value: {x: string; y: string} | null}) {
const value = props.value;
return createArray(value?.x, value?.y)?.join(', ');
}
function createArray<T>(...args: Array<T>): Array<T> {
return args;
}
export const FIXTURE_ENTRYPONT = {
fn: useFoo,
props: [{value: null}],
};
```
## Error
```
Found 1 error:
Todo: Unexpected terminal kind `optional` for optional fallthrough block
error.todo-optional-call-chain-in-optional.ts:4:21
2 | function useFoo(props: {value: {x: string; y: string} | null}) {
3 | const value = props.value;
> 4 | return createArray(value?.x, value?.y)?.join(', ');
| ^^^^^^^^ Unexpected terminal kind `optional` for optional fallthrough block
5 | }
6 |
7 | function createArray<T>(...args: Array<T>): Array<T> {
```

View File

@@ -0,0 +1,54 @@
## Input
```javascript
// @enablePropagateDepsInHIR
function useFoo(props: {value: {x: string; y: string} | null}) {
const value = props.value;
return createArray(value?.x, value?.y)?.join(', ');
}
function createArray<T>(...args: Array<T>): Array<T> {
return args;
}
export const FIXTURE_ENTRYPONT = {
fn: useFoo,
props: [{value: null}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR
function useFoo(props) {
const $ = _c(3);
const value = props.value;
let t0;
if ($[0] !== value?.x || $[1] !== value?.y) {
t0 = createArray(value?.x, value?.y)?.join(", ");
$[0] = value?.x;
$[1] = value?.y;
$[2] = t0;
} else {
t0 = $[2];
}
return t0;
}
function createArray(...t0) {
const args = t0;
return args;
}
export const FIXTURE_ENTRYPONT = {
fn: useFoo,
props: [{ value: null }],
};
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -0,0 +1,53 @@
## Input
```javascript
function useFoo(props: {value: {x: string; y: string} | null}) {
const value = props.value;
return createArray(value?.x, value?.y)?.join(', ');
}
function createArray<T>(...args: Array<T>): Array<T> {
return args;
}
export const FIXTURE_ENTRYPONT = {
fn: useFoo,
props: [{value: null}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
function useFoo(props) {
const $ = _c(3);
const value = props.value;
let t0;
if ($[0] !== value?.x || $[1] !== value?.y) {
t0 = createArray(value?.x, value?.y)?.join(", ");
$[0] = value?.x;
$[1] = value?.y;
$[2] = t0;
} else {
t0 = $[2];
}
return t0;
}
function createArray(...t0) {
const args = t0;
return args;
}
export const FIXTURE_ENTRYPONT = {
fn: useFoo,
props: [{ value: null }],
};
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -378,6 +378,17 @@ export async function transformFixtureInput(
msg: 'Expected nothing to be compiled (from `// @expectNothingCompiled`), but some functions compiled or errored',
};
}
const unexpectedThrows = logs.filter(
log => log.event.kind === 'CompileUnexpectedThrow',
);
if (unexpectedThrows.length > 0) {
return {
kind: 'err',
msg:
`Compiler pass(es) threw instead of recording errors:\n` +
unexpectedThrows.map(l => (l.event as any).data).join('\n'),
};
}
return {
kind: 'ok',
value: {