Compare commits

..

1 Commits

Author SHA1 Message Date
Joe Savona
87fd43837c [compiler] Phase 3: Make lower() always produce HIRFunction 2026-02-23 16:04:35 -08:00
43 changed files with 849 additions and 1457 deletions

View File

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

View File

@@ -713,20 +713,6 @@ 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,13 +310,16 @@ 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)!;
/**
* 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;
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,
});
}
/**
* Recurse into inner optional blocks to collect inner optional-chain

View File

@@ -759,6 +759,29 @@ 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,12 +7,7 @@
import {Binding, NodePath} from '@babel/traverse';
import * as t from '@babel/types';
import {
CompilerError,
CompilerDiagnostic,
CompilerErrorDetail,
ErrorCategory,
} from '../CompilerError';
import {CompilerError, ErrorCategory} from '../CompilerError';
import {Environment} from './Environment';
import {
BasicBlock,
@@ -115,6 +110,7 @@ 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.
@@ -152,10 +148,6 @@ 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;
}
@@ -316,28 +308,34 @@ export default class HIRBuilder {
resolveBinding(node: t.Identifier): Identifier {
if (node.name === 'fbt') {
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,
}),
);
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,
},
],
});
}
if (node.name === 'this') {
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,
}),
);
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,
},
],
});
}
const originalName = node.name;
let name = originalName;
@@ -383,15 +381,13 @@ export default class HIRBuilder {
instr => instr.value.kind === 'FunctionExpression',
)
) {
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,
}),
);
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,
});
}
}
ir.blocks = rpoBlocks;

View File

@@ -293,7 +293,7 @@ function extractManualMemoizationArgs(
instr: TInstruction<CallExpression> | TInstruction<MethodCall>,
kind: 'useCallback' | 'useMemo',
sidemap: IdentifierSidemap,
env: Environment,
errors: CompilerError,
): {
fnPlace: Place;
depsList: Array<ManualMemoDependency> | null;
@@ -303,7 +303,7 @@ function extractManualMemoizationArgs(
Place | SpreadPattern | undefined
>;
if (fnPlace == null || fnPlace.kind !== 'Identifier') {
env.recordError(
errors.pushDiagnostic(
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) {
env.recordError(
errors.pushDiagnostic(
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) {
env.recordError(
errors.pushDiagnostic(
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,6 +389,7 @@ 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 ||
@@ -435,7 +436,7 @@ export function dropManualMemoization(func: HIRFunction): void {
instr as TInstruction<CallExpression> | TInstruction<MethodCall>,
manualMemo.kind,
sidemap,
func.env,
errors,
);
if (memoDetails == null) {
@@ -463,7 +464,7 @@ export function dropManualMemoization(func: HIRFunction): void {
* is rare and likely sketchy.
*/
if (!sidemap.functions.has(fnPlace.identifier.id)) {
func.env.recordError(
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
reason: `Expected the first argument to be an inline function expression`,
@@ -548,6 +549,10 @@ 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,7 +20,6 @@ import {
Place,
isPrimitiveType,
} from '../HIR/HIR';
import {Environment} from '../HIR/Environment';
import {
eachInstructionLValue,
eachInstructionValueOperand,
@@ -108,7 +107,7 @@ export function inferMutationAliasingRanges(
let index = 0;
const shouldRecordErrors = !isFunctionExpression && fn.env.enableValidations;
const errors = new CompilerError();
for (const param of [...fn.params, ...fn.context, fn.returns]) {
const place = param.kind === 'Identifier' ? param : param.place;
@@ -201,9 +200,7 @@ export function inferMutationAliasingRanges(
effect.kind === 'MutateGlobal' ||
effect.kind === 'Impure'
) {
if (shouldRecordErrors) {
fn.env.recordError(effect.error);
}
errors.pushDiagnostic(effect.error);
functionEffects.push(effect);
} else if (effect.kind === 'Render') {
renders.push({index: index++, place: effect.place});
@@ -248,15 +245,11 @@ export function inferMutationAliasingRanges(
mutation.kind,
mutation.place.loc,
mutation.reason,
shouldRecordErrors ? fn.env : null,
errors,
);
}
for (const render of renders) {
state.render(
render.index,
render.place.identifier,
shouldRecordErrors ? fn.env : null,
);
state.render(render.index, render.place.identifier, errors);
}
for (const param of [...fn.context, ...fn.params]) {
const place = param.kind === 'Identifier' ? param : param.place;
@@ -505,6 +498,7 @@ 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);
@@ -519,7 +513,7 @@ export function inferMutationAliasingRanges(
MutationKind.Conditional,
into.loc,
null,
null,
ignoredErrors,
);
for (const from of tracked) {
if (
@@ -553,17 +547,23 @@ export function inferMutationAliasingRanges(
}
}
if (
errors.hasAnyErrors() &&
!isFunctionExpression &&
fn.env.enableValidations
) {
fn.env.recordErrors(errors);
}
return functionEffects;
}
function appendFunctionErrors(env: Environment | null, fn: HIRFunction): void {
if (env == null) return;
function appendFunctionErrors(errors: CompilerError, fn: HIRFunction): void {
for (const effect of fn.aliasingEffects ?? []) {
switch (effect.kind) {
case 'Impure':
case 'MutateFrozen':
case 'MutateGlobal': {
env.recordError(effect.error);
errors.pushDiagnostic(effect.error);
break;
}
}
@@ -664,7 +664,7 @@ class AliasingState {
}
}
render(index: number, start: Identifier, env: Environment | null): void {
render(index: number, start: Identifier, errors: CompilerError): 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(env, node.value.function);
appendFunctionErrors(errors, 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,
env: Environment | null,
errors: CompilerError,
): void {
const seen = new Map<Identifier, MutationKind>();
const queue: Array<{
@@ -742,7 +742,7 @@ class AliasingState {
node.transitive == null &&
node.local == null
) {
appendFunctionErrors(env, node.value.function);
appendFunctionErrors(errors, node.value.function);
}
if (transitive) {
if (node.transitive == null || node.transitive.kind < kind) {

View File

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

View File

@@ -13,11 +13,7 @@ import {
pruneUnusedLabels,
renameVariables,
} from '.';
import {
CompilerError,
CompilerErrorDetail,
ErrorCategory,
} from '../CompilerError';
import {CompilerError, ErrorCategory} from '../CompilerError';
import {Environment, ExternalFunction} from '../HIR';
import {
ArrayPattern,
@@ -351,6 +347,10 @@ function codegenReactiveFunction(
}
}
if (cx.errors.hasAnyErrors()) {
fn.env.recordErrors(cx.errors);
}
const countMemoBlockVisitor = new CountMemoBlockVisitor(fn.env);
visitReactiveFunction(fn, countMemoBlockVisitor, undefined);
@@ -420,6 +420,7 @@ 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>;
@@ -438,11 +439,6 @@ 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++;
}
@@ -779,15 +775,12 @@ function codegenTerminal(
loc: terminal.init.loc,
});
if (terminal.init.instructions.length !== 2) {
cx.recordError(
new CompilerErrorDetail({
reason: 'Support non-trivial for..in inits',
category: ErrorCategory.Todo,
loc: terminal.init.loc,
suggestions: null,
}),
);
return t.emptyStatement();
CompilerError.throwTodo({
reason: 'Support non-trivial for..in inits',
description: null,
loc: terminal.init.loc,
suggestions: null,
});
}
const iterableCollection = terminal.init.instructions[0];
const iterableItem = terminal.init.instructions[1];
@@ -802,15 +795,12 @@ function codegenTerminal(
break;
}
case 'StoreContext': {
cx.recordError(
new CompilerErrorDetail({
reason: 'Support non-trivial for..in inits',
category: ErrorCategory.Todo,
loc: terminal.init.loc,
suggestions: null,
}),
);
return t.emptyStatement();
CompilerError.throwTodo({
reason: 'Support non-trivial for..in inits',
description: null,
loc: terminal.init.loc,
suggestions: null,
});
}
default:
CompilerError.invariant(false, {
@@ -880,15 +870,12 @@ function codegenTerminal(
loc: terminal.test.loc,
});
if (terminal.test.instructions.length !== 2) {
cx.recordError(
new CompilerErrorDetail({
reason: 'Support non-trivial for..of inits',
category: ErrorCategory.Todo,
loc: terminal.init.loc,
suggestions: null,
}),
);
return t.emptyStatement();
CompilerError.throwTodo({
reason: 'Support non-trivial for..of inits',
description: null,
loc: terminal.init.loc,
suggestions: null,
});
}
const iterableItem = terminal.test.instructions[1];
let lval: t.LVal;
@@ -902,15 +889,12 @@ function codegenTerminal(
break;
}
case 'StoreContext': {
cx.recordError(
new CompilerErrorDetail({
reason: 'Support non-trivial for..of inits',
category: ErrorCategory.Todo,
loc: terminal.init.loc,
suggestions: null,
}),
);
return t.emptyStatement();
CompilerError.throwTodo({
reason: 'Support non-trivial for..of inits',
description: null,
loc: terminal.init.loc,
suggestions: null,
});
}
default:
CompilerError.invariant(false, {
@@ -1969,26 +1953,22 @@ function codegenInstructionValue(
} else {
if (t.isVariableDeclaration(stmt)) {
const declarator = stmt.declarations[0];
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,
}),
);
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,
});
return t.stringLiteral(`TODO handle ${declarator.id}`);
} else {
cx.recordError(
new CompilerErrorDetail({
reason: `(CodegenReactiveFunction::codegenInstructionValue) Handle conversion of ${stmt.type} to expression`,
category: ErrorCategory.Todo,
loc: stmt.loc ?? null,
suggestions: null,
}),
);
cx.errors.push({
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,6 +102,7 @@ export function validateExhaustiveDependencies(fn: HIRFunction): void {
loc: place.loc,
});
}
const error = new CompilerError();
let startMemo: StartMemoize | null = null;
function onStartMemoize(
@@ -142,7 +143,7 @@ export function validateExhaustiveDependencies(fn: HIRFunction): void {
'all',
);
if (diagnostic != null) {
fn.env.recordError(diagnostic);
error.pushDiagnostic(diagnostic);
}
}
@@ -207,12 +208,15 @@ export function validateExhaustiveDependencies(fn: HIRFunction): void {
effectReportMode,
);
if (diagnostic != null) {
fn.env.recordError(diagnostic);
error.pushDiagnostic(diagnostic);
}
},
},
false, // isFunctionExpression
);
if (error.hasAnyErrors()) {
fn.env.recordErrors(error);
}
}
function validateDependencies(

View File

@@ -6,9 +6,13 @@
*/
import * as t from '@babel/types';
import {CompilerErrorDetail, ErrorCategory} from '../CompilerError';
import {
CompilerError,
CompilerErrorDetail,
ErrorCategory,
} from '../CompilerError';
import {computeUnconditionalBlocks} from '../HIR/ComputeUnconditionalBlocks';
import {Environment, isHookName} from '../HIR/Environment';
import {isHookName} from '../HIR/Environment';
import {
HIRFunction,
IdentifierId,
@@ -86,14 +90,15 @@ 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 trackError(
function recordError(
loc: SourceLocation,
errorDetail: CompilerErrorDetail,
): void {
if (typeof loc === 'symbol') {
fn.env.recordError(errorDetail);
errors.pushErrorDetail(errorDetail);
} else {
errorsByPlace.set(loc, errorDetail);
}
@@ -113,7 +118,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) {
trackError(
recordError(
place.loc,
new CompilerErrorDetail({
category: ErrorCategory.Hooks,
@@ -129,7 +134,7 @@ export function validateHooksUsage(fn: HIRFunction): void {
const previousError =
typeof place.loc !== 'symbol' ? errorsByPlace.get(place.loc) : undefined;
if (previousError === undefined) {
trackError(
recordError(
place.loc,
new CompilerErrorDetail({
category: ErrorCategory.Hooks,
@@ -146,7 +151,7 @@ export function validateHooksUsage(fn: HIRFunction): void {
const previousError =
typeof place.loc !== 'symbol' ? errorsByPlace.get(place.loc) : undefined;
if (previousError === undefined) {
trackError(
recordError(
place.loc,
new CompilerErrorDetail({
category: ErrorCategory.Hooks,
@@ -391,7 +396,7 @@ export function validateHooksUsage(fn: HIRFunction): void {
}
case 'ObjectMethod':
case 'FunctionExpression': {
visitFunctionExpression(fn.env, instr.value.loweredFunc.func);
visitFunctionExpression(errors, instr.value.loweredFunc.func);
break;
}
default: {
@@ -416,17 +421,20 @@ export function validateHooksUsage(fn: HIRFunction): void {
}
for (const [, error] of errorsByPlace) {
fn.env.recordError(error);
errors.pushErrorDetail(error);
}
if (errors.hasAnyErrors()) {
fn.env.recordErrors(errors);
}
}
function visitFunctionExpression(env: Environment, fn: HIRFunction): void {
function visitFunctionExpression(errors: CompilerError, 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(env, instr.value.loweredFunc.func);
visitFunctionExpression(errors, instr.value.loweredFunc.func);
break;
}
case 'MethodCall':
@@ -437,7 +445,7 @@ function visitFunctionExpression(env: Environment, fn: HIRFunction): void {
: instr.value.property;
const hookKind = getHookKind(fn.env, callee.identifier);
if (hookKind != null) {
env.recordError(
errors.pushErrorDetail(
new CompilerErrorDetail({
category: ErrorCategory.Hooks,
reason:

View File

@@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerErrorDetail, EnvironmentConfig} from '..';
import {CompilerError, CompilerErrorDetail, EnvironmentConfig} from '..';
import {ErrorCategory} from '../CompilerError';
import {HIRFunction, IdentifierId} from '../HIR';
import {DEFAULT_GLOBALS} from '../HIR/Globals';
@@ -20,6 +20,7 @@ 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 =
@@ -71,19 +72,20 @@ export function validateNoCapitalizedCalls(fn: HIRFunction): void {
const propertyIdentifier = value.property.identifier.id;
const propertyName = capitalizedProperties.get(propertyIdentifier);
if (propertyName != null) {
fn.env.recordError(
new CompilerErrorDetail({
category: ErrorCategory.CapitalizedCalls,
reason,
description: `${propertyName} may be a component`,
loc: value.loc,
suggestions: null,
}),
);
errors.push({
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 {CompilerErrorDetail, ErrorCategory} from '../CompilerError';
import {ErrorCategory} from '../CompilerError';
import {
ArrayExpression,
BlockId,
@@ -20,7 +20,6 @@ import {
eachInstructionValueOperand,
eachTerminalOperand,
} from '../HIR/visitors';
import {Environment} from '../HIR/Environment';
/**
* Validates that useEffect is not used for derived computations which could/should
@@ -50,6 +49,8 @@ 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;
@@ -89,19 +90,22 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
validateEffect(
effectFunction.loweredFunc.func,
dependencies,
fn.env,
errors,
);
}
}
}
}
}
for (const detail of errors.details) {
fn.env.recordError(detail);
}
}
function validateEffect(
effectFunction: HIRFunction,
effectDeps: Array<IdentifierId>,
env: Environment,
errors: CompilerError,
): void {
for (const operand of effectFunction.context) {
if (isSetStateType(operand.identifier)) {
@@ -215,15 +219,13 @@ function validateEffect(
}
for (const loc of setStateLocations) {
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,
}),
);
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,
});
}
}

View File

@@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerDiagnostic, Effect} from '..';
import {CompilerDiagnostic, CompilerError, Effect} from '..';
import {ErrorCategory} from '../CompilerError';
import {
HIRFunction,
@@ -43,6 +43,7 @@ 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'}>
@@ -59,7 +60,7 @@ export function validateNoFreezingKnownMutableFunctions(fn: HIRFunction): void {
place.identifier.name.kind === 'named'
? `\`${place.identifier.name.value}\``
: 'a local variable';
fn.env.recordError(
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Immutability,
reason: 'Cannot modify local variables after render completes',
@@ -158,4 +159,7 @@ 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} from '..';
import {CompilerDiagnostic, CompilerError} from '..';
import {ErrorCategory} from '../CompilerError';
import {HIRFunction} from '../HIR';
import {getFunctionCallSignature} from '../Inference/InferMutationAliasingEffects';
@@ -20,6 +20,7 @@ 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;
@@ -31,7 +32,7 @@ export function validateNoImpureFunctionsInRender(fn: HIRFunction): void {
callee.identifier.type,
);
if (signature != null && signature.impure === true) {
fn.env.recordError(
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Purity,
reason: 'Cannot call impure function during render',
@@ -51,4 +52,7 @@ 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);
for (const detail of errors.details) {
fn.env.recordError(detail);
if (errors.hasAnyErrors()) {
fn.env.recordErrors(errors);
}
}

View File

@@ -48,8 +48,8 @@ export function validateNoSetStateInRender(fn: HIRFunction): void {
fn,
unconditionalSetStateFunctions,
);
for (const detail of errors.details) {
fn.env.recordError(detail);
if (errors.hasAnyErrors()) {
fn.env.recordErrors(errors);
}
}

View File

@@ -27,7 +27,6 @@ import {
ScopeId,
SourceLocation,
} from '../HIR';
import {Environment} from '../HIR/Environment';
import {printIdentifier, printManualMemoDependency} from '../HIR/PrintHIR';
import {
eachInstructionValueLValue,
@@ -49,10 +48,13 @@ import {getOrInsertDefault} from '../Utils/utils';
*/
export function validatePreservedManualMemoization(fn: ReactiveFunction): void {
const state = {
env: fn.env,
errors: new CompilerError(),
manualMemoState: null,
};
visitReactiveFunction(fn, new Visitor(), state);
for (const detail of state.errors.details) {
fn.env.recordError(detail);
}
}
const DEBUG = false;
@@ -110,7 +112,7 @@ type ManualMemoBlockState = {
};
type VisitorState = {
env: Environment;
errors: CompilerError;
manualMemoState: ManualMemoBlockState | null;
};
@@ -230,7 +232,7 @@ function validateInferredDep(
temporaries: Map<IdentifierId, ManualMemoDependency>,
declsWithinMemoBlock: Set<DeclarationId>,
validDepsInMemoBlock: Array<ManualMemoDependency>,
env: Environment,
errorState: CompilerError,
memoLocation: SourceLocation,
): void {
let normalizedDep: ManualMemoDependency;
@@ -280,7 +282,7 @@ function validateInferredDep(
errorDiagnostic = merge(errorDiagnostic ?? compareResult, compareResult);
}
}
env.recordError(
errorState.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.PreserveManualMemo,
reason: 'Existing memoization could not be preserved',
@@ -426,7 +428,7 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
this.temporaries,
state.manualMemoState.decls,
state.manualMemoState.depsFromSource,
state.env,
state.errors,
state.manualMemoState.loc,
);
}
@@ -529,7 +531,7 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
!this.scopes.has(identifier.scope.id) &&
!this.prunedScopes.has(identifier.scope.id)
) {
state.env.recordError(
state.errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.PreserveManualMemo,
reason: 'Existing memoization could not be preserved',
@@ -575,7 +577,7 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
for (const identifier of decls) {
if (isUnmemoized(identifier, this.scopes)) {
state.env.recordError(
state.errors.pushDiagnostic(
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, ErrorCategory} from '..';
import {CompilerDiagnostic, CompilerError, ErrorCategory} from '..';
import {CodegenFunction} from '../ReactiveScopes';
import {Environment} from '../HIR/Environment';
@@ -125,6 +125,8 @@ 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)
@@ -239,7 +241,7 @@ export function validateSourceLocations(
loc: t.SourceLocation,
nodeType: string,
): void => {
env.recordError(
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Todo,
reason: 'Important source location missing in generated code',
@@ -259,7 +261,7 @@ export function validateSourceLocations(
expectedType: string,
actualTypes: Set<string>,
): void => {
env.recordError(
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Todo,
reason:
@@ -307,4 +309,8 @@ 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;
fn.env.recordError(
errors.pushDiagnostic(
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) {
fn.env.recordError(
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
reason:
@@ -122,7 +122,7 @@ export function validateUseMemo(fn: HIRFunction): void {
);
}
validateNoContextVariableAssignment(body.loweredFunc.func, fn.env);
validateNoContextVariableAssignment(body.loweredFunc.func, errors);
if (fn.env.config.validateNoVoidUseMemo) {
if (!hasNonVoidReturn(body.loweredFunc.func)) {
@@ -176,11 +176,14 @@ export function validateUseMemo(fn: HIRFunction): void {
}
}
fn.env.logErrors(voidMemoErrors.asResult());
if (errors.hasAnyErrors()) {
fn.env.recordErrors(errors);
}
}
function validateNoContextVariableAssignment(
fn: HIRFunction,
env: Environment,
errors: CompilerError,
): void {
const context = new Set(fn.context.map(place => place.identifier.id));
for (const block of fn.body.blocks.values()) {
@@ -189,7 +192,7 @@ function validateNoContextVariableAssignment(
switch (value.kind) {
case 'StoreContext': {
if (context.has(value.lvalue.place.identifier.id)) {
env.recordError(
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
reason:

View File

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

View File

@@ -0,0 +1,39 @@
## 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

@@ -1,66 +0,0 @@
## 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

@@ -1,19 +0,0 @@
/**
* 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

@@ -1,69 +0,0 @@
## 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

@@ -1,22 +0,0 @@
// @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

@@ -1,86 +0,0 @@
## 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

@@ -1,26 +0,0 @@
// @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

@@ -1,54 +0,0 @@
## 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

@@ -1,15 +0,0 @@
/**
* 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

@@ -1,62 +0,0 @@
## 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

@@ -1,23 +0,0 @@
// @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 4 errors:
Found 1 error:
Todo: Support local variables named `fbt`
@@ -60,49 +60,10 @@ error.todo-fbt-as-local.ts:18:19
16 |
17 | function Foo(props) {
> 18 | const getText1 = fbt =>
| ^^^ Support local variables named `fbt`
| ^^^ Rename to avoid conflict with fbt plugin
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,15 +16,17 @@ function Component(props) {
```
Found 1 error:
Invariant: <fbt> tags should be module-level imports
Todo: Support local variables named `fbt`
error.todo-locally-require-fbt.ts:4:10
2 | const fbt = require('fbt');
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
3 |
> 4 | return <fbt desc="Description">{'Text'}</fbt>;
| ^^^ <fbt> tags should be module-level imports
4 | return <fbt desc="Description">{'Text'}</fbt>;
5 | }
6 |
```

View File

@@ -0,0 +1,40 @@
## 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

@@ -1,54 +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}],
};
```
## 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

@@ -1,53 +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}],
};
```
## 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,17 +378,6 @@ 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: {