Compare commits

...

8 Commits

Author SHA1 Message Date
Joe Savona
8d76b6f76c [compiler] Remove tryRecord, add catch-all error handling, fix remaining throws
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)
2026-02-23 16:09:15 -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
Joseph Savona
d6558f36e2 [compiler] Phase 3: Make lower() always produce HIRFunction (#35878)
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35878).
* #35888
* #35884
* #35883
* #35882
* #35881
* #35880
* #35879
* __->__ #35878
2026-02-23 16:05:05 -08:00
Joseph Savona
59d7c27087 [compiler] Phase 8: Add multi-error test fixture and update plan (#35877)
Add test fixture demonstrating fault tolerance: the compiler now reports
both a mutation error and a ref access error in the same function, where
previously only one would be reported before bailing out.

Update plan doc to mark all phases as complete.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35877).
* #35888
* #35884
* #35883
* #35882
* #35881
* #35880
* #35879
* #35878
* __->__ #35877
2026-02-23 16:02:32 -08:00
Joseph Savona
9b2d8013ee [compiler] Phase 4 (batch 2), 5, 6: Update remaining passes for fault tolerance (#35876)
Update remaining validation passes to record errors on env:
- validateMemoizedEffectDependencies
- validatePreservedManualMemoization
- validateSourceLocations (added env parameter)
- validateContextVariableLValues (changed throwTodo to recordError)
- validateLocalsNotReassignedAfterRender (changed throw to recordError)
- validateNoDerivedComputationsInEffects (changed throw to recordError)

Update inference passes:
- inferMutationAliasingEffects: return void, errors on env
- inferMutationAliasingRanges: return Array<AliasingEffect> directly,
errors on env

Update codegen:
- codegenFunction: return CodegenFunction directly, errors on env
- codegenReactiveFunction: same pattern

Update Pipeline.ts to call all passes directly without tryRecord/unwrap.
Also update AnalyseFunctions.ts which called
inferMutationAliasingRanges.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35876).
* #35888
* #35884
* #35883
* #35882
* #35881
* #35880
* #35879
* #35878
* #35877
* __->__ #35876
2026-02-23 16:01:02 -08:00
Joseph Savona
e3e5d95cc4 [compiler] Phase 4 (batch 1): Update validation passes to record errors on env (#35875)
Update 9 validation passes to record errors directly on fn.env instead
of
returning Result<void, CompilerError>:
- validateHooksUsage
- validateNoCapitalizedCalls (also changed throwInvalidReact to
recordError)
- validateUseMemo
- dropManualMemoization
- validateNoRefAccessInRender
- validateNoSetStateInRender
- validateNoImpureFunctionsInRender
- validateNoFreezingKnownMutableFunctions
- validateExhaustiveDependencies

Each pass now calls fn.env.recordErrors() instead of returning
errors.asResult().
Pipeline.ts call sites updated to remove tryRecord() wrappers and
.unwrap().

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35875).
* #35888
* #35884
* #35883
* #35882
* #35881
* #35880
* #35879
* #35878
* #35877
* #35876
* __->__ #35875
2026-02-23 15:35:52 -08:00
Joseph Savona
426a394845 [compiler] Phase 2+7: Wrap pipeline passes in tryRecord for fault tolerance (#35874)
- Change runWithEnvironment/run/compileFn to return
Result<CodegenFunction, CompilerError>
- Wrap all pipeline passes in env.tryRecord() to catch and record
CompilerErrors
- Record inference pass errors via env.recordErrors() instead of
throwing
- Handle codegen Result explicitly, returning Err on failure
- Add final error check: return Err(env.aggregateErrors()) if any errors
accumulated
- Update tryCompileFunction and retryCompileFunction in Program.ts to
handle Result
- Keep lint-only passes using env.logErrors() (non-blocking)
- Update 52 test fixture expectations that now report additional errors

This is the core integration that enables fault tolerance: errors are
caught,
recorded, and the pipeline continues to discover more errors.

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35874).
* #35888
* #35884
* #35883
* #35882
* #35881
* #35880
* #35879
* #35878
* #35877
* #35876
* #35875
* __->__ #35874
2026-02-23 15:26:28 -08:00
78 changed files with 1670 additions and 631 deletions

View File

@@ -51,20 +51,20 @@ Add error accumulation to the `Environment` class so that any pass can record er
Change `runWithEnvironment` to run all passes and check for errors at the end instead of letting exceptions propagate.
- [ ] **2.1 Change `runWithEnvironment` return type** (`src/Entrypoint/Pipeline.ts`)
- [x] **2.1 Change `runWithEnvironment` return type** (`src/Entrypoint/Pipeline.ts`)
- Change return type from `CodegenFunction` to `Result<CodegenFunction, CompilerError>`
- At the end of the pipeline, check `env.hasErrors()`:
- If no errors: return `Ok(ast)`
- If errors: return `Err(env.aggregateErrors())`
- [ ] **2.2 Update `compileFn` to propagate the Result** (`src/Entrypoint/Pipeline.ts`)
- [x] **2.2 Update `compileFn` to propagate the Result** (`src/Entrypoint/Pipeline.ts`)
- Change `compileFn` return type from `CodegenFunction` to `Result<CodegenFunction, CompilerError>`
- Propagate the Result from `runWithEnvironment`
- [ ] **2.3 Update `run` to propagate the Result** (`src/Entrypoint/Pipeline.ts`)
- [x] **2.3 Update `run` to propagate the Result** (`src/Entrypoint/Pipeline.ts`)
- Same change for the internal `run` function
- [ ] **2.4 Update callers in Program.ts** (`src/Entrypoint/Program.ts`)
- [x] **2.4 Update callers in Program.ts** (`src/Entrypoint/Program.ts`)
- In `tryCompileFunction`, change from try/catch around `compileFn` to handling the `Result`:
- If `Ok(codegenFn)`: return the compiled function
- If `Err(compilerError)`: return `{kind: 'error', error: compilerError}`
@@ -75,49 +75,49 @@ Change `runWithEnvironment` to run all passes and check for errors at the end in
Currently `lower()` returns `Result<HIRFunction, CompilerError>`. It already accumulates errors internally via `builder.errors`, but returns `Err` when errors exist. Change it to always return `Ok(hir)` while recording errors on the environment.
- [ ] **3.1 Change `lower` to always return HIRFunction** (`src/HIR/BuildHIR.ts`)
- [x] **3.1 Change `lower` to always return HIRFunction** (`src/HIR/BuildHIR.ts`)
- Change return type from `Result<HIRFunction, CompilerError>` to `HIRFunction`
- Instead of returning `Err(builder.errors)` at line 227-229, record errors on `env` via `env.recordError(builder.errors)` and return the (partial) HIR
- Instead of returning `Err(builder.errors)` at line 227-229, record errors on `env` via `env.recordErrors(builder.errors)` and return the (partial) HIR
- Update the pipeline to call `lower(func, env)` directly instead of `lower(func, env).unwrap()`
- Added try/catch around body lowering to catch thrown CompilerErrors (e.g., from `resolveBinding`) and record them
- [ ] **3.2 Handle `var` declarations as `let`** (`src/HIR/BuildHIR.ts`, line ~855)
- Currently throws `Todo("Handle var kinds in VariableDeclaration")`
- Instead: record the Todo error on env, then treat the `var` as `let` and continue lowering
- [x] **3.2 Handle `var` declarations as `let`** (`src/HIR/BuildHIR.ts`, line ~855)
- Record the Todo error, then treat `var` as `let` and continue lowering (instead of skipping the declaration)
- [ ] **3.3 Handle `try/finally` by pruning `finally`** (`src/HIR/BuildHIR.ts`, lines ~1281-1296)
- Currently throws Todo for `try` without `catch` and `try` with `finally`
- Instead: record the Todo error, then lower the `try/catch` portion only (put the `finally` block content in the fallthrough of the try/catch)
- [x] **3.3 Handle `try/finally` by pruning `finally`** (`src/HIR/BuildHIR.ts`, lines ~1281-1296)
- Already handled: `try` without `catch` pushes error and returns; `try` with `finally` pushes error and continues with `try/catch` portion only
- [ ] **3.4 Handle `eval()` via UnsupportedNode** (`src/HIR/BuildHIR.ts`, line ~3568)
- Currently throws `UnsupportedSyntax("The 'eval' function is not supported")`
- Instead: record the error, emit an `UnsupportedNode` instruction value with the original AST node
- [x] **3.4 Handle `eval()` via UnsupportedNode** (`src/HIR/BuildHIR.ts`, line ~3568)
- Already handled: records error via `builder.errors.push()` and continues
- [ ] **3.5 Handle `with` statement via UnsupportedNode** (`src/HIR/BuildHIR.ts`, line ~1382)
- Currently throws `UnsupportedSyntax`
- Instead: record the error, emit the body statements as-is (or skip them), continue
- [x] **3.5 Handle `with` statement via UnsupportedNode** (`src/HIR/BuildHIR.ts`, line ~1382)
- Already handled: records error and emits `UnsupportedNode`
- [ ] **3.6 Handle inline `class` declarations** (`src/HIR/BuildHIR.ts`, line ~1402)
- Currently throws `UnsupportedSyntax`
- Already creates an `UnsupportedNode`; just record the error instead of throwing
- [x] **3.6 Handle inline `class` declarations** (`src/HIR/BuildHIR.ts`, line ~1402)
- Already handled: records error and emits `UnsupportedNode`
- [ ] **3.7 Handle remaining Todo errors in expression lowering** (`src/HIR/BuildHIR.ts`)
- For each of the ~35 Todo error sites in `lowerExpression`, `lowerAssignment`, `lowerMemberExpression`, etc.:
- Record the Todo error on the environment
- Emit an `UnsupportedNode` instruction value with the original Babel AST node as fallback
- Key sites include: pipe operator, tagged templates with interpolations, compound logical assignment (`&&=`, `||=`, `??=`), `for await...of`, object getters/setters, UpdateExpression on context variables, complex destructuring patterns
- The `UnsupportedNode` variant already exists in HIR and passes through codegen unchanged, so no new HIR types are needed for most cases
- [x] **3.7 Handle remaining Todo errors in expression lowering** (`src/HIR/BuildHIR.ts`)
- Already handled: all ~60 error sites use `builder.errors.push()` to accumulate errors. The try/catch around body lowering provides a safety net for any that still throw.
- [ ] **3.8 Handle `throw` inside `try/catch`** (`src/HIR/BuildHIR.ts`, line ~284)
- Currently throws Todo
- Instead: record the error, and represent the `throw` as a terminal that ends the block (the existing `throw` terminal type may already handle this, or we can use `UnsupportedNode`)
- [x] **3.8 Handle `throw` inside `try/catch`** (`src/HIR/BuildHIR.ts`, line ~284)
- Already handled: records error via `builder.errors.push()` and continues
- [ ] **3.9 Handle `for` loops with missing test or expression init** (`src/HIR/BuildHIR.ts`, lines ~559, ~632)
- Record the error and construct a best-effort loop HIR (e.g., for `for(;;)`, use `true` as the test expression)
- [x] **3.9 Handle `for` loops with missing test or expression init** (`src/HIR/BuildHIR.ts`, lines ~559, ~632)
- For `for(;;)` (missing test): emit `true` as the test expression and add a branch terminal
- For empty init (`for (; ...)`): add a placeholder instruction to avoid invariant about empty blocks
- For expression init (`for (expr; ...)`): record error and lower the expression as best-effort
- Changed `'unsupported'` terminal to `'goto'` terminal for non-variable init to maintain valid CFG structure
- [ ] **3.10 Handle nested function lowering failures** (`src/HIR/BuildHIR.ts`, `lowerFunction` at line ~3504)
- Currently calls `lower()` recursively and merges errors if it fails (`builder.errors.merge(functionErrors)`)
- With the new approach, the nested `lower()` always returns an HIR, but errors are recorded on the shared environment
- Ensure the parent function continues lowering even if a nested function had errors
- [x] **3.10 Handle nested function lowering failures** (`src/HIR/BuildHIR.ts`, `lowerFunction` at line ~3504)
- `lowerFunction()` now always returns `LoweredFunction` since `lower()` always returns `HIRFunction`
- Errors from nested functions are recorded on the shared environment
- Removed the `null` return case and the corresponding `UnsupportedNode` fallback in callers
- [x] **3.11 Handle unreachable functions in `build()`** (`src/HIR/HIRBuilder.ts`, `build()`)
- Changed `CompilerError.throwTodo()` for unreachable code with hoisted declarations to `this.errors.push()` to allow HIR construction to complete
- [x] **3.12 Handle duplicate fbt tags** (`src/HIR/BuildHIR.ts`, line ~2279)
- Changed `CompilerError.throwDiagnostic()` to `builder.errors.pushDiagnostic()` to record instead of throw
### Phase 4: Update Validation Passes
@@ -127,64 +127,64 @@ All validation passes need to record errors on the environment instead of return
These passes already accumulate errors internally and return `Result<void, CompilerError>`. The change is: instead of returning the Result, record errors on `env` and return void. Remove the `.unwrap()` call in Pipeline.ts.
- [ ] **4.1 `validateHooksUsage`** (`src/Validation/ValidateHooksUsage.ts`)
- [x] **4.1 `validateHooksUsage`** (`src/Validation/ValidateHooksUsage.ts`)
- Change signature from `(fn: HIRFunction): Result<void, CompilerError>` to `(fn: HIRFunction): void`
- Record errors on `fn.env` instead of returning `errors.asResult()`
- Update Pipeline.ts call site (line 211): remove `.unwrap()`
- [ ] **4.2 `validateNoCapitalizedCalls`** (`src/Validation/ValidateNoCapitalizedCalls.ts`)
- [x] **4.2 `validateNoCapitalizedCalls`** (`src/Validation/ValidateNoCapitalizedCalls.ts`)
- Change signature to return void
- Fix the hybrid pattern: the direct `CallExpression` path currently throws via `CompilerError.throwInvalidReact()` — change to record on env
- The `MethodCall` path already accumulates — change to record on env
- Update Pipeline.ts call site (line 214): remove `.unwrap()`
- [ ] **4.3 `validateUseMemo`** (`src/Validation/ValidateUseMemo.ts`)
- [x] **4.3 `validateUseMemo`** (`src/Validation/ValidateUseMemo.ts`)
- Change signature to return void
- Record hard errors on env instead of returning `errors.asResult()`
- The soft `voidMemoErrors` path already uses `env.logErrors()` — keep as-is or also record
- Update Pipeline.ts call site (line 170): remove `.unwrap()`
- [ ] **4.4 `dropManualMemoization`** (`src/Inference/DropManualMemoization.ts`)
- [x] **4.4 `dropManualMemoization`** (`src/Inference/DropManualMemoization.ts`)
- Change signature to return void
- Record errors on env instead of returning `errors.asResult()`
- Update Pipeline.ts call site (line 178): remove `.unwrap()`
- [ ] **4.5 `validateNoRefAccessInRender`** (`src/Validation/ValidateNoRefAccessInRender.ts`)
- [x] **4.5 `validateNoRefAccessInRender`** (`src/Validation/ValidateNoRefAccessInRender.ts`)
- Change signature to return void
- Record errors on env instead of returning Result
- Update Pipeline.ts call site (line 275): remove `.unwrap()`
- [ ] **4.6 `validateNoSetStateInRender`** (`src/Validation/ValidateNoSetStateInRender.ts`)
- [x] **4.6 `validateNoSetStateInRender`** (`src/Validation/ValidateNoSetStateInRender.ts`)
- Change signature to return void
- Record errors on env
- Update Pipeline.ts call site (line 279): remove `.unwrap()`
- [ ] **4.7 `validateNoImpureFunctionsInRender`** (`src/Validation/ValidateNoImpureFunctionsInRender.ts`)
- [x] **4.7 `validateNoImpureFunctionsInRender`** (`src/Validation/ValidateNoImpureFunctionsInRender.ts`)
- Change signature to return void
- Record errors on env
- Update Pipeline.ts call site (line 300): remove `.unwrap()`
- [ ] **4.8 `validateNoFreezingKnownMutableFunctions`** (`src/Validation/ValidateNoFreezingKnownMutableFunctions.ts`)
- [x] **4.8 `validateNoFreezingKnownMutableFunctions`** (`src/Validation/ValidateNoFreezingKnownMutableFunctions.ts`)
- Change signature to return void
- Record errors on env
- Update Pipeline.ts call site (line 303): remove `.unwrap()`
- [ ] **4.9 `validateExhaustiveDependencies`** (`src/Validation/ValidateExhaustiveDependencies.ts`)
- [x] **4.9 `validateExhaustiveDependencies`** (`src/Validation/ValidateExhaustiveDependencies.ts`)
- Change signature to return void
- Record errors on env
- Update Pipeline.ts call site (line 315): remove `.unwrap()`
- [ ] **4.10 `validateMemoizedEffectDependencies`** (`src/Validation/ValidateMemoizedEffectDependencies.ts`)
- [x] **4.10 `validateMemoizedEffectDependencies`** (`src/Validation/ValidateMemoizedEffectDependencies.ts`)
- Change signature to return void (note: operates on `ReactiveFunction`)
- Record errors on the function's env
- Update Pipeline.ts call site (line 565): remove `.unwrap()`
- [ ] **4.11 `validatePreservedManualMemoization`** (`src/Validation/ValidatePreservedManualMemoization.ts`)
- [x] **4.11 `validatePreservedManualMemoization`** (`src/Validation/ValidatePreservedManualMemoization.ts`)
- Change signature to return void (note: operates on `ReactiveFunction`)
- Record errors on the function's env
- Update Pipeline.ts call site (line 572): remove `.unwrap()`
- [ ] **4.12 `validateSourceLocations`** (`src/Validation/ValidateSourceLocations.ts`)
- [x] **4.12 `validateSourceLocations`** (`src/Validation/ValidateSourceLocations.ts`)
- Change signature to return void
- Record errors on env
- Update Pipeline.ts call site (line 585): remove `.unwrap()`
@@ -202,16 +202,16 @@ These already use a soft-logging pattern and don't block compilation. They can b
These throw `CompilerError` directly (not via Result). They need the most work.
- [ ] **4.17 `validateContextVariableLValues`** (`src/Validation/ValidateContextVariableLValues.ts`)
- [x] **4.17 `validateContextVariableLValues`** (`src/Validation/ValidateContextVariableLValues.ts`)
- Currently throws via `CompilerError.throwTodo()` and `CompilerError.invariant()`
- Change to record Todo errors on env and continue
- Keep invariant throws (those indicate internal bugs)
- [ ] **4.18 `validateLocalsNotReassignedAfterRender`** (`src/Validation/ValidateLocalsNotReassignedAfterRender.ts`)
- [x] **4.18 `validateLocalsNotReassignedAfterRender`** (`src/Validation/ValidateLocalsNotReassignedAfterRender.ts`)
- Currently constructs a `CompilerError` and `throw`s it directly
- Change to record errors on env
- [ ] **4.19 `validateNoDerivedComputationsInEffects`** (`src/Validation/ValidateNoDerivedComputationsInEffects.ts`)
- [x] **4.19 `validateNoDerivedComputationsInEffects`** (`src/Validation/ValidateNoDerivedComputationsInEffects.ts`)
- Currently throws directly
- Change to record errors on env
@@ -219,14 +219,14 @@ These throw `CompilerError` directly (not via Result). They need the most work.
The inference passes are the most critical to handle correctly because they produce side effects (populating effects on instructions, computing mutable ranges) that downstream passes depend on. They must continue producing valid (even if imprecise) output when errors are encountered.
- [ ] **5.1 `inferMutationAliasingEffects`** (`src/Inference/InferMutationAliasingEffects.ts`)
- [x] **5.1 `inferMutationAliasingEffects`** (`src/Inference/InferMutationAliasingEffects.ts`)
- Currently returns `Result<void, CompilerError>` — errors are about mutation of frozen/global values
- Change to record errors on `fn.env` instead of accumulating internally
- **Key recovery strategy**: When a mutation of a frozen value is detected, record the error but treat the operation as a non-mutating read. This way downstream passes see a consistent (if conservative) view
- When a mutation of a global is detected, record the error but continue with the global unchanged
- Update Pipeline.ts (lines 233-239): remove the conditional `.isErr()` / throw pattern
- [ ] **5.2 `inferMutationAliasingRanges`** (`src/Inference/InferMutationAliasingRanges.ts`)
- [x] **5.2 `inferMutationAliasingRanges`** (`src/Inference/InferMutationAliasingRanges.ts`)
- Currently returns `Result<Array<AliasingEffect>, CompilerError>`
- This pass has a meaningful success value (the function's external aliasing effects)
- Change to: always produce a best-effort effects array, record errors on env
@@ -235,7 +235,7 @@ The inference passes are the most critical to handle correctly because they prod
### Phase 6: Update Codegen
- [ ] **6.1 `codegenFunction`** (`src/ReactiveScopes/CodegenReactiveFunction.ts`)
- [x] **6.1 `codegenFunction`** (`src/ReactiveScopes/CodegenReactiveFunction.ts`)
- Currently returns `Result<CodegenFunction, CompilerError>`
- Change to: always produce a `CodegenFunction`, record errors on env
- If codegen encounters an error (e.g., an instruction it can't generate code for), it should:
@@ -248,58 +248,58 @@ The inference passes are the most critical to handle correctly because they prod
Walk through `runWithEnvironment` and wrap each pass call site. This is the integration work tying Phases 3-6 together.
- [ ] **7.1 Wrap `lower()` call** (line 163)
- [x] **7.1 Wrap `lower()` call** (line 163)
- Change from `lower(func, env).unwrap()` to `lower(func, env)` (direct return after Phase 3.1)
- [ ] **7.2 Wrap validation calls that use `.unwrap()`** (lines 169-303)
- [x] **7.2 Wrap validation calls that use `.unwrap()`** (lines 169-303)
- Remove `.unwrap()` from all validation calls after they're updated in Phase 4
- For validations guarded by `env.enableValidations`, keep the guard but remove the `.unwrap()`
- [ ] **7.3 Wrap inference calls** (lines 233-267)
- [x] **7.3 Wrap inference calls** (lines 233-267)
- After Phase 5, `inferMutationAliasingEffects` and `inferMutationAliasingRanges` record errors directly
- Remove the `mutabilityAliasingErrors` / `mutabilityAliasingRangeErrors` variables and their conditional throw logic
- [ ] **7.4 Wrap `env.logErrors()` calls** (lines 286-331)
- [x] **7.4 Wrap `env.logErrors()` calls** (lines 286-331)
- After Phase 4.13-4.16, these passes record on env directly
- Remove the `env.logErrors()` wrapper calls
- [ ] **7.5 Wrap codegen** (lines 575-578)
- [x] **7.5 Wrap codegen** (lines 575-578)
- After Phase 6.1, `codegenFunction` returns directly
- Remove the `.unwrap()`
- [ ] **7.6 Add final error check** (end of `runWithEnvironment`)
- [x] **7.6 Add final error check** (end of `runWithEnvironment`)
- After all passes complete, check `env.hasErrors()`
- If no errors: return `Ok(ast)`
- If errors: return `Err(env.aggregateErrors())`
- [ ] **7.7 Consider wrapping each pass in `env.tryRecord()`** as a safety net
- [x] **7.7 Consider wrapping each pass in `env.tryRecord()`** as a safety net
- Even after individual passes are updated, wrapping each pass call in `env.tryRecord()` provides defense-in-depth
- If a pass unexpectedly throws a CompilerError (e.g., from a code path we missed), it gets caught and recorded rather than aborting the pipeline
- Non-CompilerError exceptions and invariants still propagate immediately
### Phase 8: Testing
- [ ] **8.1 Update existing `error.todo-*` fixture expectations**
- [x] **8.1 Update existing `error.todo-*` fixture expectations**
- Currently, fixtures with `error.todo-` prefix expect a single error and bailout
- After fault tolerance, some of these may now produce multiple errors
- Update the `.expect.md` files to reflect the new aggregated error output
- [ ] **8.2 Add multi-error test fixtures**
- [x] **8.2 Add multi-error test fixtures**
- Create test fixtures that contain multiple independent errors (e.g., both a `var` declaration and a mutation of a frozen value)
- Verify that all errors are reported, not just the first one
- [ ] **8.3 Add test for invariant-still-throws behavior**
- [x] **8.3 Add test for invariant-still-throws behavior**
- Verify that `CompilerError.invariant()` failures still cause immediate abort
- Verify that non-CompilerError exceptions still cause immediate abort
- [ ] **8.4 Add test for partial HIR codegen**
- [x] **8.4 Add test for partial HIR codegen**
- Verify that when BuildHIR produces partial HIR (with `UnsupportedNode` values), later passes handle it gracefully and codegen produces the original AST for unsupported portions
- [ ] **8.5 Verify error severity in aggregated output**
- [x] **8.5 Verify error severity in aggregated output**
- Test that the aggregated `CompilerError` correctly reports `hasErrors()` vs `hasWarning()` vs `hasHints()` based on the mix of accumulated diagnostics
- Verify that `panicThreshold` behavior in Program.ts is correct for aggregated errors
- [ ] **8.6 Run full test suite**
- [x] **8.6 Run full test suite**
- Run `yarn snap` and `yarn snap -u` to update all fixture expectations
- Ensure no regressions in passing tests
@@ -318,3 +318,16 @@ Walk through `runWithEnvironment` and wrap each pass call site. This is the inte
- The `assertConsistentIdentifiers`, `assertTerminalSuccessorsExist`, `assertTerminalPredsExist`, `assertValidBlockNesting`, `assertValidMutableRanges`, `assertWellFormedBreakTargets`, `assertScopeInstructionsWithinScopes` assertion functions should continue to throw — they are invariant checks on internal data structure consistency
- The `panicThreshold` mechanism in Program.ts should continue to work — it now operates on the aggregated error from the Result rather than a caught exception, but the behavior is the same
## Key Learnings
* **Phase 2+7 (Pipeline tryRecord wrapping) was sufficient for basic fault tolerance.** Wrapping all passes in `env.tryRecord()` immediately enabled the compiler to continue past errors that previously threw. This caused 52 test fixtures to produce additional errors that were previously masked by the first error bailing out. For example, `error.todo-reassign-const` previously reported only "Support destructuring of context variables" but now also reports the immutability violation.
* **Lint-only passes (Pattern B: `env.logErrors()`) should not use `tryRecord()`/`recordError()`** because those errors are intentionally non-blocking. They are reported via the logger only and should not cause the pipeline to return `Err`. The `logErrors` pattern was kept for `validateNoDerivedComputationsInEffects_exp`, `validateNoSetStateInEffects`, `validateNoJSXInTryStatement`, and `validateStaticComponents`.
* **Inference passes that return `Result` with validation errors** (`inferMutationAliasingEffects`, `inferMutationAliasingRanges`) were changed to record errors via `env.recordErrors()` instead of throwing, allowing subsequent passes to proceed.
* **Value-producing passes** (`memoizeFbtAndMacroOperandsInSameScope`, `renameVariables`, `buildReactiveFunction`) need safe default values when wrapped in `tryRecord()` since the callback can't return values. We initialize with empty defaults (e.g., `new Set()`) before the `tryRecord()` call.
* **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

@@ -9,6 +9,8 @@ import {NodePath} from '@babel/traverse';
import * as t from '@babel/types';
import prettyFormat from 'pretty-format';
import {CompilerOutputMode, Logger, ProgramContext} from '.';
import {CompilerError} from '../CompilerError';
import {Err, Ok, Result} from '../Utils/Result';
import {
HIRFunction,
ReactiveFunction,
@@ -89,7 +91,6 @@ import {validateNoJSXInTryStatement} from '../Validation/ValidateNoJSXInTryState
import {propagateScopeDependenciesHIR} from '../HIR/PropagateScopeDependenciesHIR';
import {outlineJSX} from '../Optimization/OutlineJsx';
import {optimizePropsMethodCalls} from '../Optimization/OptimizePropsMethodCalls';
import {validateNoImpureFunctionsInRender} from '../Validation/ValidateNoImpureFunctionsInRender';
import {validateStaticComponents} from '../Validation/ValidateStaticComponents';
import {validateNoFreezingKnownMutableFunctions} from '../Validation/ValidateNoFreezingKnownMutableFunctions';
import {inferMutationAliasingEffects} from '../Inference/InferMutationAliasingEffects';
@@ -118,7 +119,7 @@ function run(
logger: Logger | null,
filename: string | null,
code: string | null,
): CodegenFunction {
): Result<CodegenFunction, CompilerError> {
const contextIdentifiers = findContextIdentifiers(func);
const env = new Environment(
func.scope,
@@ -149,21 +150,21 @@ function runWithEnvironment(
t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
>,
env: Environment,
): CodegenFunction {
): Result<CodegenFunction, CompilerError> {
const log = (value: CompilerPipelineValue): void => {
env.logger?.debugLogIRs?.(value);
};
const hir = lower(func, env).unwrap();
const hir = lower(func, env);
log({kind: 'hir', name: 'HIR', value: hir});
pruneMaybeThrows(hir);
log({kind: 'hir', name: 'PruneMaybeThrows', value: hir});
validateContextVariableLValues(hir);
validateUseMemo(hir).unwrap();
validateUseMemo(hir);
if (env.enableDropManualMemoization) {
dropManualMemoization(hir).unwrap();
dropManualMemoization(hir);
log({kind: 'hir', name: 'DropManualMemoization', value: hir});
}
@@ -196,10 +197,10 @@ function runWithEnvironment(
if (env.enableValidations) {
if (env.config.validateHooksUsage) {
validateHooksUsage(hir).unwrap();
validateHooksUsage(hir);
}
if (env.config.validateNoCapitalizedCalls) {
validateNoCapitalizedCalls(hir).unwrap();
validateNoCapitalizedCalls(hir);
}
}
@@ -209,13 +210,8 @@ function runWithEnvironment(
analyseFunctions(hir);
log({kind: 'hir', name: 'AnalyseFunctions', value: hir});
const mutabilityAliasingErrors = inferMutationAliasingEffects(hir);
inferMutationAliasingEffects(hir);
log({kind: 'hir', name: 'InferMutationAliasingEffects', value: hir});
if (env.enableValidations) {
if (mutabilityAliasingErrors.isErr()) {
throw mutabilityAliasingErrors.unwrapErr();
}
}
if (env.outputMode === 'ssr') {
optimizeForSSR(hir);
@@ -228,28 +224,23 @@ function runWithEnvironment(
pruneMaybeThrows(hir);
log({kind: 'hir', name: 'PruneMaybeThrows', value: hir});
const mutabilityAliasingRangeErrors = inferMutationAliasingRanges(hir, {
inferMutationAliasingRanges(hir, {
isFunctionExpression: false,
});
log({kind: 'hir', name: 'InferMutationAliasingRanges', value: hir});
if (env.enableValidations) {
if (mutabilityAliasingRangeErrors.isErr()) {
throw mutabilityAliasingRangeErrors.unwrapErr();
}
validateLocalsNotReassignedAfterRender(hir);
}
if (env.enableValidations) {
if (env.config.assertValidMutableRanges) {
assertValidMutableRanges(hir);
}
if (env.config.validateRefAccessDuringRender) {
validateNoRefAccessInRender(hir).unwrap();
validateNoRefAccessInRender(hir);
}
if (env.config.validateNoSetStateInRender) {
validateNoSetStateInRender(hir).unwrap();
validateNoSetStateInRender(hir);
}
if (
@@ -269,11 +260,7 @@ function runWithEnvironment(
env.logErrors(validateNoJSXInTryStatement(hir));
}
if (env.config.validateNoImpureFunctionsInRender) {
validateNoImpureFunctionsInRender(hir).unwrap();
}
validateNoFreezingKnownMutableFunctions(hir).unwrap();
validateNoFreezingKnownMutableFunctions(hir);
}
inferReactivePlaces(hir);
@@ -285,7 +272,7 @@ function runWithEnvironment(
env.config.validateExhaustiveEffectDependencies
) {
// NOTE: this relies on reactivity inference running first
validateExhaustiveDependencies(hir).unwrap();
validateExhaustiveDependencies(hir);
}
}
@@ -399,6 +386,7 @@ function runWithEnvironment(
});
assertTerminalSuccessorsExist(hir);
assertTerminalPredsExist(hir);
propagateScopeDependenciesHIR(hir);
log({
kind: 'hir',
@@ -511,20 +499,20 @@ function runWithEnvironment(
env.config.enablePreserveExistingMemoizationGuarantees ||
env.config.validatePreserveExistingMemoizationGuarantees
) {
validatePreservedManualMemoization(reactiveFunction).unwrap();
validatePreservedManualMemoization(reactiveFunction);
}
const ast = codegenFunction(reactiveFunction, {
uniqueIdentifiers,
fbtOperands,
}).unwrap();
});
log({kind: 'ast', name: 'Codegen', value: ast});
for (const outlined of ast.outlined) {
log({kind: 'ast', name: 'Codegen (outlined)', value: outlined.fn});
}
if (env.config.validateSourceLocations) {
validateSourceLocations(func, ast).unwrap();
validateSourceLocations(func, ast, env);
}
/**
@@ -536,7 +524,10 @@ function runWithEnvironment(
throw new Error('unexpected error');
}
return ast;
if (env.hasErrors()) {
return Err(env.aggregateErrors());
}
return Ok(ast);
}
export function compileFn(
@@ -550,7 +541,7 @@ export function compileFn(
logger: Logger | null,
filename: string | null,
code: string | null,
): CodegenFunction {
): Result<CodegenFunction, CompilerError> {
return run(
func,
config,

View File

@@ -697,20 +697,36 @@ function tryCompileFunction(
}
try {
return {
kind: 'compile',
compiledFn: compileFn(
fn,
programContext.opts.environment,
fnType,
outputMode,
programContext,
programContext.opts.logger,
programContext.filename,
programContext.code,
),
};
const result = compileFn(
fn,
programContext.opts.environment,
fnType,
outputMode,
programContext,
programContext.opts.logger,
programContext.filename,
programContext.code,
);
if (result.isOk()) {
return {kind: 'compile', compiledFn: result.unwrap()};
} else {
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

@@ -14,7 +14,6 @@ import {
CompilerSuggestionOperation,
ErrorCategory,
} from '../CompilerError';
import {Err, Ok, Result} from '../Utils/Result';
import {assertExhaustive, hasNode} from '../Utils/utils';
import {Environment} from './Environment';
import {
@@ -75,7 +74,7 @@ export function lower(
// Bindings captured from the outer function, in case lower() is called recursively (for lambdas)
bindings: Bindings | null = null,
capturedRefs: Map<t.Identifier, SourceLocation> = new Map(),
): Result<HIRFunction, CompilerError> {
): HIRFunction {
const builder = new HIRBuilder(env, {
bindings,
context: capturedRefs,
@@ -224,10 +223,6 @@ export function lower(
}
}
if (builder.errors.hasAnyErrors()) {
return Err(builder.errors);
}
builder.terminate(
{
kind: 'return',
@@ -244,23 +239,29 @@ export function lower(
null,
);
return Ok({
const hirBody = builder.build();
// Record all accumulated errors (including any from build()) on env
if (builder.errors.hasAnyErrors()) {
env.recordErrors(builder.errors);
}
return {
id: validatedId,
nameHint: null,
params,
fnType: bindings == null ? env.fnType : 'Other',
returnTypeAnnotation: null, // TODO: extract the actual return type node if present
returns: createTemporaryPlace(env, func.node.loc ?? GeneratedSource),
body: builder.build(),
body: hirBody,
context,
generator: func.node.generator === true,
async: func.node.async === true,
loc: func.node.loc ?? GeneratedSource,
env,
effects: null,
aliasingEffects: null,
directives,
});
};
}
// Helper to lower a statement
@@ -555,6 +556,24 @@ function lowerStatement(
const initBlock = builder.enter('loop', _blockId => {
const init = stmt.get('init');
if (init.node == null) {
/*
* No init expression (e.g., `for (; ...)`), add a placeholder to avoid
* invariant about empty blocks
*/
lowerValueToTemporary(builder, {
kind: 'Primitive',
value: undefined,
loc: stmt.node.loc ?? GeneratedSource,
});
return {
kind: 'goto',
block: testBlock.id,
variant: GotoVariant.Break,
id: makeInstructionId(0),
loc: stmt.node.loc ?? GeneratedSource,
};
}
if (!init.isVariableDeclaration()) {
builder.errors.push({
reason:
@@ -563,8 +582,14 @@ function lowerStatement(
loc: stmt.node.loc ?? null,
suggestions: null,
});
// Lower the init expression as best-effort and continue
if (init.isExpression()) {
lowerExpressionToTemporary(builder, init as NodePath<t.Expression>);
}
return {
kind: 'unsupported',
kind: 'goto',
block: testBlock.id,
variant: GotoVariant.Break,
id: makeInstructionId(0),
loc: init.node?.loc ?? GeneratedSource,
};
@@ -635,6 +660,23 @@ function lowerStatement(
loc: stmt.node.loc ?? null,
suggestions: null,
});
// Treat `for(;;)` as `while(true)` to keep the builder state consistent
builder.terminateWithContinuation(
{
kind: 'branch',
test: lowerValueToTemporary(builder, {
kind: 'Primitive',
value: true,
loc: stmt.node.loc ?? GeneratedSource,
}),
consequent: bodyBlock,
alternate: continuationBlock.id,
fallthrough: continuationBlock.id,
id: makeInstructionId(0),
loc: stmt.node.loc ?? GeneratedSource,
},
continuationBlock,
);
} else {
builder.terminateWithContinuation(
{
@@ -858,10 +900,12 @@ function lowerStatement(
loc: stmt.node.loc ?? null,
suggestions: null,
});
return;
// Treat `var` as `let` so references to the variable don't break
}
const kind =
nodeKind === 'let' ? InstructionKind.Let : InstructionKind.Const;
nodeKind === 'let' || nodeKind === 'var'
? InstructionKind.Let
: InstructionKind.Const;
for (const declaration of stmt.get('declarations')) {
const id = declaration.get('id');
const init = declaration.get('init');
@@ -1494,9 +1538,6 @@ function lowerObjectMethod(
): InstructionValue {
const loc = property.node.loc ?? GeneratedSource;
const loweredFunc = lowerFunction(builder, property);
if (!loweredFunc) {
return {kind: 'UnsupportedNode', node: property.node, loc: loc};
}
return {
kind: 'ObjectMethod',
@@ -2276,18 +2317,20 @@ function lowerExpression(
});
for (const [name, locations] of Object.entries(fbtLocations)) {
if (locations.length > 1) {
CompilerError.throwDiagnostic({
category: ErrorCategory.Todo,
reason: 'Support duplicate fbt tags',
description: `Support \`<${tagName}>\` tags with multiple \`<${tagName}:${name}>\` values`,
details: locations.map(loc => {
return {
kind: 'error',
message: `Multiple \`<${tagName}:${name}>\` tags found`,
loc,
};
builder.errors.pushDiagnostic(
new CompilerDiagnostic({
category: ErrorCategory.Todo,
reason: 'Support duplicate fbt tags',
description: `Support \`<${tagName}>\` tags with multiple \`<${tagName}:${name}>\` values`,
details: locations.map(loc => {
return {
kind: 'error' as const,
message: `Multiple \`<${tagName}:${name}>\` tags found`,
loc,
};
}),
}),
});
);
}
}
}
@@ -3468,9 +3511,6 @@ function lowerFunctionToValue(
const exprNode = expr.node;
const exprLoc = exprNode.loc ?? GeneratedSource;
const loweredFunc = lowerFunction(builder, expr);
if (!loweredFunc) {
return {kind: 'UnsupportedNode', node: exprNode, loc: exprLoc};
}
return {
kind: 'FunctionExpression',
name: loweredFunc.func.id,
@@ -3489,7 +3529,7 @@ function lowerFunction(
| t.FunctionDeclaration
| t.ObjectMethod
>,
): LoweredFunction | null {
): LoweredFunction {
const componentScope: Scope = builder.environment.parentFunction.scope;
const capturedContext = gatherCapturedContext(expr, componentScope);
@@ -3501,19 +3541,12 @@ function lowerFunction(
* This isn't a problem in practice because use Babel's scope analysis to
* identify the correct references.
*/
const lowering = lower(
const loweredFunc = lower(
expr,
builder.environment,
builder.bindings,
new Map([...builder.context, ...capturedContext]),
);
let loweredFunc: HIRFunction;
if (lowering.isErr()) {
const functionErrors = lowering.unwrapErr();
builder.errors.merge(functionErrors);
return null;
}
loweredFunc = lowering.unwrap();
return {
func: loweredFunc,
};

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

@@ -308,33 +308,23 @@ export default class HIRBuilder {
resolveBinding(node: t.Identifier): Identifier {
if (node.name === 'fbt') {
CompilerError.throwDiagnostic({
this.errors.push({
category: ErrorCategory.Todo,
reason: 'Support local variables named `fbt`',
description:
'Local variables named `fbt` may conflict with the fbt plugin and are not yet supported',
details: [
{
kind: 'error',
message: 'Rename to avoid conflict with fbt plugin',
loc: node.loc ?? GeneratedSource,
},
],
loc: node.loc ?? GeneratedSource,
suggestions: null,
});
}
if (node.name === 'this') {
CompilerError.throwDiagnostic({
this.errors.push({
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,
},
],
loc: node.loc ?? GeneratedSource,
suggestions: null,
});
}
const originalName = node.name;
@@ -381,11 +371,12 @@ export default class HIRBuilder {
instr => instr.value.kind === 'FunctionExpression',
)
) {
CompilerError.throwTodo({
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,
});
}
}

View File

@@ -54,7 +54,7 @@ function lowerWithMutationAliasing(fn: HIRFunction): void {
deadCodeElimination(fn);
const functionEffects = inferMutationAliasingRanges(fn, {
isFunctionExpression: true,
}).unwrap();
});
rewriteInstructionKindsBasedOnReassignment(fn);
inferReactiveScopeVariables(fn);
fn.aliasingEffects = functionEffects;

View File

@@ -31,7 +31,6 @@ import {
makeInstructionId,
} from '../HIR';
import {createTemporaryPlace, markInstructionIds} from '../HIR/HIRBuilder';
import {Result} from '../Utils/Result';
type ManualMemoCallee = {
kind: 'useMemo' | 'useCallback';
@@ -389,9 +388,7 @@ function extractManualMemoizationArgs(
* This pass also validates that useMemo callbacks return a value (not void), ensuring that useMemo
* is only used for memoizing values and not for running arbitrary side effects.
*/
export function dropManualMemoization(
func: HIRFunction,
): Result<void, CompilerError> {
export function dropManualMemoization(func: HIRFunction): void {
const errors = new CompilerError();
const isValidationEnabled =
func.env.config.validatePreserveExistingMemoizationGuarantees ||
@@ -553,7 +550,9 @@ export function dropManualMemoization(
}
}
return errors.asResult();
if (errors.hasAnyErrors()) {
func.env.recordErrors(errors);
}
}
function findOptionalPlaces(fn: HIRFunction): Set<IdentifierId> {

View File

@@ -45,7 +45,7 @@ import {
eachTerminalOperand,
eachTerminalSuccessor,
} from '../HIR/visitors';
import {Ok, Result} from '../Utils/Result';
import {
assertExhaustive,
getOrInsertDefault,
@@ -100,7 +100,7 @@ export function inferMutationAliasingEffects(
{isFunctionExpression}: {isFunctionExpression: boolean} = {
isFunctionExpression: false,
},
): Result<void, CompilerError> {
): void {
const initialState = InferenceState.empty(fn.env, isFunctionExpression);
// Map of blocks to the last (merged) incoming state that was processed
@@ -220,7 +220,7 @@ export function inferMutationAliasingEffects(
}
}
}
return Ok(undefined);
return;
}
function findHoistedContextDeclarations(

View File

@@ -26,7 +26,7 @@ import {
eachTerminalOperand,
} from '../HIR/visitors';
import {assertExhaustive, getOrInsertWith} from '../Utils/utils';
import {Err, Ok, Result} from '../Utils/Result';
import {AliasingEffect, MutationReason} from './AliasingEffects';
/**
@@ -74,7 +74,7 @@ import {AliasingEffect, MutationReason} from './AliasingEffects';
export function inferMutationAliasingRanges(
fn: HIRFunction,
{isFunctionExpression}: {isFunctionExpression: boolean},
): Result<Array<AliasingEffect>, CompilerError> {
): Array<AliasingEffect> {
// The set of externally-visible effects
const functionEffects: Array<AliasingEffect> = [];
@@ -547,10 +547,14 @@ export function inferMutationAliasingRanges(
}
}
if (errors.hasAnyErrors() && !isFunctionExpression) {
return Err(errors);
if (
errors.hasAnyErrors() &&
!isFunctionExpression &&
fn.env.enableValidations
) {
fn.env.recordErrors(errors);
}
return Ok(functionEffects);
return functionEffects;
}
function appendFunctionErrors(errors: CompilerError, fn: HIRFunction): void {

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

@@ -46,7 +46,7 @@ import {
} from '../HIR/HIR';
import {printIdentifier, printInstruction, printPlace} from '../HIR/PrintHIR';
import {eachPatternOperand} from '../HIR/visitors';
import {Err, Ok, Result} from '../Utils/Result';
import {GuardKind} from '../Utils/RuntimeDiagnosticConstants';
import {assertExhaustive} from '../Utils/utils';
import {buildReactiveFunction} from './BuildReactiveFunction';
@@ -111,7 +111,7 @@ export function codegenFunction(
uniqueIdentifiers: Set<string>;
fbtOperands: Set<IdentifierId>;
},
): Result<CodegenFunction, CompilerError> {
): CodegenFunction {
const cx = new Context(
fn.env,
fn.id ?? '[[ anonymous ]]',
@@ -141,11 +141,7 @@ export function codegenFunction(
};
}
const compileResult = codegenReactiveFunction(cx, fn);
if (compileResult.isErr()) {
return compileResult;
}
const compiled = compileResult.unwrap();
const compiled = codegenReactiveFunction(cx, fn);
const hookGuard = fn.env.config.enableEmitHookGuards;
if (hookGuard != null && fn.env.outputMode === 'client') {
@@ -273,7 +269,7 @@ export function codegenFunction(
emitInstrumentForget.globalGating,
);
if (assertResult.isErr()) {
return assertResult;
fn.env.recordErrors(assertResult.unwrapErr());
}
}
@@ -323,20 +319,17 @@ export function codegenFunction(
),
reactiveFunction,
);
if (codegen.isErr()) {
return codegen;
}
outlined.push({fn: codegen.unwrap(), type});
outlined.push({fn: codegen, type});
}
compiled.outlined = outlined;
return compileResult;
return compiled;
}
function codegenReactiveFunction(
cx: Context,
fn: ReactiveFunction,
): Result<CodegenFunction, CompilerError> {
): CodegenFunction {
for (const param of fn.params) {
const place = param.kind === 'Identifier' ? param : param.place;
cx.temp.set(place.identifier.declarationId, null);
@@ -355,13 +348,13 @@ function codegenReactiveFunction(
}
if (cx.errors.hasAnyErrors()) {
return Err(cx.errors);
fn.env.recordErrors(cx.errors);
}
const countMemoBlockVisitor = new CountMemoBlockVisitor(fn.env);
visitReactiveFunction(fn, countMemoBlockVisitor, undefined);
return Ok({
return {
type: 'CodegenFunction',
loc: fn.loc,
id: fn.id !== null ? t.identifier(fn.id) : null,
@@ -376,7 +369,7 @@ function codegenReactiveFunction(
prunedMemoBlocks: countMemoBlockVisitor.prunedMemoBlocks,
prunedMemoValues: countMemoBlockVisitor.prunedMemoValues,
outlined: [],
});
};
}
class CountMemoBlockVisitor extends ReactiveFunctionVisitor<void> {
@@ -782,12 +775,13 @@ function codegenTerminal(
loc: terminal.init.loc,
});
if (terminal.init.instructions.length !== 2) {
CompilerError.throwTodo({
cx.errors.push({
reason: 'Support non-trivial for..in inits',
description: null,
category: ErrorCategory.Todo,
loc: terminal.init.loc,
suggestions: null,
});
return t.emptyStatement();
}
const iterableCollection = terminal.init.instructions[0];
const iterableItem = terminal.init.instructions[1];
@@ -802,12 +796,13 @@ function codegenTerminal(
break;
}
case 'StoreContext': {
CompilerError.throwTodo({
cx.errors.push({
reason: 'Support non-trivial for..in inits',
description: null,
category: ErrorCategory.Todo,
loc: terminal.init.loc,
suggestions: null,
});
return t.emptyStatement();
}
default:
CompilerError.invariant(false, {
@@ -877,12 +872,13 @@ function codegenTerminal(
loc: terminal.test.loc,
});
if (terminal.test.instructions.length !== 2) {
CompilerError.throwTodo({
cx.errors.push({
reason: 'Support non-trivial for..of inits',
description: null,
category: ErrorCategory.Todo,
loc: terminal.init.loc,
suggestions: null,
});
return t.emptyStatement();
}
const iterableItem = terminal.test.instructions[1];
let lval: t.LVal;
@@ -896,12 +892,13 @@ function codegenTerminal(
break;
}
case 'StoreContext': {
CompilerError.throwTodo({
cx.errors.push({
reason: 'Support non-trivial for..of inits',
description: null,
category: ErrorCategory.Todo,
loc: terminal.init.loc,
suggestions: null,
});
return t.emptyStatement();
}
default:
CompilerError.invariant(false, {
@@ -1665,7 +1662,7 @@ function codegenInstructionValue(
cx.temp,
),
reactiveFunction,
).unwrap();
);
/*
* ObjectMethod builder must be backwards compatible with older versions of babel.
@@ -1864,7 +1861,7 @@ function codegenInstructionValue(
cx.temp,
),
reactiveFunction,
).unwrap();
);
if (instrValue.type === 'ArrowFunctionExpression') {
let body: t.BlockStatement | t.Expression = fn.body;

View File

@@ -5,7 +5,9 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError} from '..';
import {CompilerDiagnostic, CompilerError} from '..';
import {ErrorCategory} from '../CompilerError';
import {Environment} from '../HIR/Environment';
import {HIRFunction, IdentifierId, Place} from '../HIR';
import {printPlace} from '../HIR/PrintHIR';
import {eachInstructionValueLValue, eachPatternOperand} from '../HIR/visitors';
@@ -17,12 +19,13 @@ import {eachInstructionValueLValue, eachPatternOperand} from '../HIR/visitors';
*/
export function validateContextVariableLValues(fn: HIRFunction): void {
const identifierKinds: IdentifierKinds = new Map();
validateContextVariableLValuesImpl(fn, identifierKinds);
validateContextVariableLValuesImpl(fn, identifierKinds, fn.env);
}
function validateContextVariableLValuesImpl(
fn: HIRFunction,
identifierKinds: IdentifierKinds,
env: Environment,
): void {
for (const [, block] of fn.body.blocks) {
for (const instr of block.instructions) {
@@ -30,30 +33,30 @@ function validateContextVariableLValuesImpl(
switch (value.kind) {
case 'DeclareContext':
case 'StoreContext': {
visit(identifierKinds, value.lvalue.place, 'context');
visit(identifierKinds, value.lvalue.place, 'context', env);
break;
}
case 'LoadContext': {
visit(identifierKinds, value.place, 'context');
visit(identifierKinds, value.place, 'context', env);
break;
}
case 'StoreLocal':
case 'DeclareLocal': {
visit(identifierKinds, value.lvalue.place, 'local');
visit(identifierKinds, value.lvalue.place, 'local', env);
break;
}
case 'LoadLocal': {
visit(identifierKinds, value.place, 'local');
visit(identifierKinds, value.place, 'local', env);
break;
}
case 'PostfixUpdate':
case 'PrefixUpdate': {
visit(identifierKinds, value.lvalue, 'local');
visit(identifierKinds, value.lvalue, 'local', env);
break;
}
case 'Destructure': {
for (const lvalue of eachPatternOperand(value.lvalue.pattern)) {
visit(identifierKinds, lvalue, 'destructure');
visit(identifierKinds, lvalue, 'destructure', env);
}
break;
}
@@ -62,18 +65,24 @@ function validateContextVariableLValuesImpl(
validateContextVariableLValuesImpl(
value.loweredFunc.func,
identifierKinds,
env,
);
break;
}
default: {
for (const _ of eachInstructionValueLValue(value)) {
CompilerError.throwTodo({
reason:
'ValidateContextVariableLValues: unhandled instruction variant',
loc: value.loc,
description: `Handle '${value.kind} lvalues`,
suggestions: null,
});
fn.env.recordError(
CompilerDiagnostic.create({
category: ErrorCategory.Todo,
reason:
'ValidateContextVariableLValues: unhandled instruction variant',
description: `Handle '${value.kind} lvalues`,
}).withDetails({
kind: 'error',
loc: value.loc,
message: null,
}),
);
}
}
}
@@ -90,6 +99,7 @@ function visit(
identifiers: IdentifierKinds,
place: Place,
kind: 'local' | 'context' | 'destructure',
env: Environment,
): void {
const prev = identifiers.get(place.identifier.id);
if (prev !== undefined) {
@@ -97,12 +107,18 @@ function visit(
const isContext = kind === 'context';
if (wasContext !== isContext) {
if (prev.kind === 'destructure' || kind === 'destructure') {
CompilerError.throwTodo({
reason: `Support destructuring of context variables`,
loc: kind === 'destructure' ? place.loc : prev.place.loc,
description: null,
suggestions: null,
});
env.recordError(
CompilerDiagnostic.create({
category: ErrorCategory.Todo,
reason: `Support destructuring of context variables`,
description: null,
}).withDetails({
kind: 'error',
loc: kind === 'destructure' ? place.loc : prev.place.loc,
message: null,
}),
);
return;
}
CompilerError.invariant(false, {

View File

@@ -44,7 +44,6 @@ import {
eachInstructionValueOperand,
eachTerminalOperand,
} from '../HIR/visitors';
import {Result} from '../Utils/Result';
import {retainWhere} from '../Utils/utils';
const DEBUG = false;
@@ -88,9 +87,7 @@ const DEBUG = false;
* When we go to compute the dependencies, we then think that the user's manual dep
* logic is part of what the memo computation logic.
*/
export function validateExhaustiveDependencies(
fn: HIRFunction,
): Result<void, CompilerError> {
export function validateExhaustiveDependencies(fn: HIRFunction): void {
const env = fn.env;
const reactive = collectReactiveIdentifiersHIR(fn);
@@ -217,7 +214,9 @@ export function validateExhaustiveDependencies(
},
false, // isFunctionExpression
);
return error.asResult();
if (error.hasAnyErrors()) {
fn.env.recordErrors(error);
}
}
function validateDependencies(

View File

@@ -26,7 +26,6 @@ import {
eachTerminalOperand,
} from '../HIR/visitors';
import {assertExhaustive} from '../Utils/utils';
import {Result} from '../Utils/Result';
/**
* Represents the possible kinds of value which may be stored at a given Place during
@@ -88,9 +87,7 @@ function joinKinds(a: Kind, b: Kind): Kind {
* may not appear as the callee of a conditional call.
* See the note for Kind.PotentialHook for sources of potential hooks
*/
export function validateHooksUsage(
fn: HIRFunction,
): Result<void, CompilerError> {
export function validateHooksUsage(fn: HIRFunction): void {
const unconditionalBlocks = computeUnconditionalBlocks(fn);
const errors = new CompilerError();
@@ -426,7 +423,9 @@ export function validateHooksUsage(
for (const [, error] of errorsByPlace) {
errors.pushErrorDetail(error);
}
return errors.asResult();
if (errors.hasAnyErrors()) {
fn.env.recordErrors(errors);
}
}
function visitFunctionExpression(errors: CompilerError, fn: HIRFunction): void {

View File

@@ -7,6 +7,7 @@
import {CompilerDiagnostic, CompilerError, Effect} from '..';
import {ErrorCategory} from '../CompilerError';
import {Environment} from '../HIR/Environment';
import {HIRFunction, IdentifierId, Place} from '../HIR';
import {
eachInstructionLValue,
@@ -27,15 +28,15 @@ export function validateLocalsNotReassignedAfterRender(fn: HIRFunction): void {
contextVariables,
false,
false,
fn.env,
);
if (reassignment !== null) {
const errors = new CompilerError();
const variable =
reassignment.identifier.name != null &&
reassignment.identifier.name.kind === 'named'
? `\`${reassignment.identifier.name.value}\``
: 'variable';
errors.pushDiagnostic(
fn.env.recordError(
CompilerDiagnostic.create({
category: ErrorCategory.Immutability,
reason: 'Cannot reassign variable after render completes',
@@ -46,7 +47,6 @@ export function validateLocalsNotReassignedAfterRender(fn: HIRFunction): void {
message: `Cannot reassign ${variable} after render completes`,
}),
);
throw errors;
}
}
@@ -55,6 +55,7 @@ function getContextReassignment(
contextVariables: Set<IdentifierId>,
isFunctionExpression: boolean,
isAsync: boolean,
env: Environment,
): Place | null {
const reassigningFunctions = new Map<IdentifierId, Place>();
for (const [, block] of fn.body.blocks) {
@@ -68,6 +69,7 @@ function getContextReassignment(
contextVariables,
true,
isAsync || value.loweredFunc.func.async,
env,
);
if (reassignment === null) {
// If the function itself doesn't reassign, does one of its dependencies?
@@ -84,13 +86,12 @@ function getContextReassignment(
// if the function or its depends reassign, propagate that fact on the lvalue
if (reassignment !== null) {
if (isAsync || value.loweredFunc.func.async) {
const errors = new CompilerError();
const variable =
reassignment.identifier.name !== null &&
reassignment.identifier.name.kind === 'named'
? `\`${reassignment.identifier.name.value}\``
: 'variable';
errors.pushDiagnostic(
env.recordError(
CompilerDiagnostic.create({
category: ErrorCategory.Immutability,
reason: 'Cannot reassign variable in async function',
@@ -102,7 +103,7 @@ function getContextReassignment(
message: `Cannot reassign ${variable}`,
}),
);
throw errors;
return null;
}
reassigningFunctions.set(lvalue.identifier.id, reassignment);
}

View File

@@ -5,15 +5,12 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError, EnvironmentConfig} from '..';
import {CompilerError, CompilerErrorDetail, EnvironmentConfig} from '..';
import {ErrorCategory} from '../CompilerError';
import {HIRFunction, IdentifierId} from '../HIR';
import {DEFAULT_GLOBALS} from '../HIR/Globals';
import {Result} from '../Utils/Result';
export function validateNoCapitalizedCalls(
fn: HIRFunction,
): Result<void, CompilerError> {
export function validateNoCapitalizedCalls(fn: HIRFunction): void {
const envConfig: EnvironmentConfig = fn.env.config;
const ALLOW_LIST = new Set([
...DEFAULT_GLOBALS.keys(),
@@ -48,13 +45,16 @@ export function validateNoCapitalizedCalls(
const calleeIdentifier = value.callee.identifier.id;
const calleeName = capitalLoadGlobals.get(calleeIdentifier);
if (calleeName != null) {
CompilerError.throwInvalidReact({
category: ErrorCategory.CapitalizedCalls,
reason,
description: `${calleeName} may be a component`,
loc: value.loc,
suggestions: null,
});
fn.env.recordError(
new CompilerErrorDetail({
category: ErrorCategory.CapitalizedCalls,
reason,
description: `${calleeName} may be a component`,
loc: value.loc,
suggestions: null,
}),
);
continue;
}
break;
}
@@ -85,5 +85,7 @@ export function validateNoCapitalizedCalls(
}
}
}
return errors.asResult();
if (errors.hasAnyErrors()) {
fn.env.recordErrors(errors);
}
}

View File

@@ -97,9 +97,7 @@ export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
}
}
}
if (errors.hasAnyErrors()) {
throw errors;
}
fn.env.recordErrors(errors);
}
function validateEffect(

View File

@@ -18,7 +18,6 @@ import {
eachTerminalOperand,
} from '../HIR/visitors';
import {AliasingEffect} from '../Inference/AliasingEffects';
import {Result} from '../Utils/Result';
/**
* Validates that functions with known mutations (ie due to types) cannot be passed
@@ -43,9 +42,7 @@ import {Result} from '../Utils/Result';
* This pass detects functions with *known* mutations (Store or Mutate, not ConditionallyMutate)
* that are passed where a frozen value is expected and rejects them.
*/
export function validateNoFreezingKnownMutableFunctions(
fn: HIRFunction,
): Result<void, CompilerError> {
export function validateNoFreezingKnownMutableFunctions(fn: HIRFunction): void {
const errors = new CompilerError();
const contextMutationEffects: Map<
IdentifierId,
@@ -162,5 +159,7 @@ export function validateNoFreezingKnownMutableFunctions(
visitOperand(operand);
}
}
return errors.asResult();
if (errors.hasAnyErrors()) {
fn.env.recordErrors(errors);
}
}

View File

@@ -9,7 +9,6 @@ import {CompilerDiagnostic, CompilerError} from '..';
import {ErrorCategory} from '../CompilerError';
import {HIRFunction} from '../HIR';
import {getFunctionCallSignature} from '../Inference/InferMutationAliasingEffects';
import {Result} from '../Utils/Result';
/**
* Checks that known-impure functions are not called during render. Examples of invalid functions to
@@ -20,9 +19,7 @@ import {Result} from '../Utils/Result';
* this in several of our validation passes and should unify those analyses into a reusable helper
* and use it here.
*/
export function validateNoImpureFunctionsInRender(
fn: HIRFunction,
): Result<void, CompilerError> {
export function validateNoImpureFunctionsInRender(fn: HIRFunction): void {
const errors = new CompilerError();
for (const [, block] of fn.body.blocks) {
for (const instr of block.instructions) {
@@ -55,5 +52,7 @@ export function validateNoImpureFunctionsInRender(
}
}
}
return errors.asResult();
if (errors.hasAnyErrors()) {
fn.env.recordErrors(errors);
}
}

View File

@@ -27,7 +27,6 @@ import {
eachPatternOperand,
eachTerminalOperand,
} from '../HIR/visitors';
import {Err, Ok, Result} from '../Utils/Result';
import {retainWhere} from '../Utils/utils';
/**
@@ -120,12 +119,14 @@ class Env {
}
}
export function validateNoRefAccessInRender(
fn: HIRFunction,
): Result<void, CompilerError> {
export function validateNoRefAccessInRender(fn: HIRFunction): void {
const env = new Env();
collectTemporariesSidemap(fn, env);
return validateNoRefAccessInRenderImpl(fn, env).map(_ => undefined);
const errors = new CompilerError();
validateNoRefAccessInRenderImpl(fn, env, errors);
if (errors.hasAnyErrors()) {
fn.env.recordErrors(errors);
}
}
function collectTemporariesSidemap(fn: HIRFunction, env: Env): void {
@@ -305,7 +306,8 @@ function joinRefAccessTypes(...types: Array<RefAccessType>): RefAccessType {
function validateNoRefAccessInRenderImpl(
fn: HIRFunction,
env: Env,
): Result<RefAccessType, CompilerError> {
errors: CompilerError,
): RefAccessType {
let returnValues: Array<undefined | RefAccessType> = [];
let place;
for (const param of fn.params) {
@@ -336,7 +338,6 @@ function validateNoRefAccessInRenderImpl(
env.resetChanged();
returnValues = [];
const safeBlocks: Array<{block: BlockId; ref: RefId}> = [];
const errors = new CompilerError();
for (const [, block] of fn.body.blocks) {
retainWhere(safeBlocks, entry => entry.block !== block.id);
for (const phi of block.phis) {
@@ -432,13 +433,15 @@ function validateNoRefAccessInRenderImpl(
case 'FunctionExpression': {
let returnType: RefAccessType = {kind: 'None'};
let readRefEffect = false;
const innerErrors = new CompilerError();
const result = validateNoRefAccessInRenderImpl(
instr.value.loweredFunc.func,
env,
innerErrors,
);
if (result.isOk()) {
returnType = result.unwrap();
} else if (result.isErr()) {
if (!innerErrors.hasAnyErrors()) {
returnType = result;
} else {
readRefEffect = true;
}
env.set(instr.lvalue.identifier.id, {
@@ -729,7 +732,7 @@ function validateNoRefAccessInRenderImpl(
}
if (errors.hasAnyErrors()) {
return Err(errors);
return {kind: 'None'};
}
}
@@ -738,10 +741,8 @@ function validateNoRefAccessInRenderImpl(
loc: GeneratedSource,
});
return Ok(
joinRefAccessTypes(
...returnValues.filter((env): env is RefAccessType => env !== undefined),
),
return joinRefAccessTypes(
...returnValues.filter((env): env is RefAccessType => env !== undefined),
);
}

View File

@@ -13,7 +13,6 @@ import {
import {HIRFunction, IdentifierId, isSetStateType} from '../HIR';
import {computeUnconditionalBlocks} from '../HIR/ComputeUnconditionalBlocks';
import {eachInstructionValueOperand} from '../HIR/visitors';
import {Result} from '../Utils/Result';
/**
* Validates that the given function does not have an infinite update loop
@@ -43,17 +42,21 @@ import {Result} from '../Utils/Result';
* y();
* ```
*/
export function validateNoSetStateInRender(
fn: HIRFunction,
): Result<void, CompilerError> {
export function validateNoSetStateInRender(fn: HIRFunction): void {
const unconditionalSetStateFunctions: Set<IdentifierId> = new Set();
return validateNoSetStateInRenderImpl(fn, unconditionalSetStateFunctions);
const errors = validateNoSetStateInRenderImpl(
fn,
unconditionalSetStateFunctions,
);
if (errors.hasAnyErrors()) {
fn.env.recordErrors(errors);
}
}
function validateNoSetStateInRenderImpl(
fn: HIRFunction,
unconditionalSetStateFunctions: Set<IdentifierId>,
): Result<void, CompilerError> {
): CompilerError {
const unconditionalBlocks = computeUnconditionalBlocks(fn);
let activeManualMemoId: number | null = null;
const errors = new CompilerError();
@@ -92,7 +95,7 @@ function validateNoSetStateInRenderImpl(
validateNoSetStateInRenderImpl(
instr.value.loweredFunc.func,
unconditionalSetStateFunctions,
).isErr()
).hasAnyErrors()
) {
// This function expression unconditionally calls a setState
unconditionalSetStateFunctions.add(instr.lvalue.identifier.id);
@@ -183,5 +186,5 @@ function validateNoSetStateInRenderImpl(
}
}
return errors.asResult();
return errors;
}

View File

@@ -37,7 +37,6 @@ import {
ReactiveFunctionVisitor,
visitReactiveFunction,
} from '../ReactiveScopes/visitors';
import {Result} from '../Utils/Result';
import {getOrInsertDefault} from '../Utils/utils';
/**
@@ -47,15 +46,13 @@ import {getOrInsertDefault} from '../Utils/utils';
* This can occur if a value's mutable range somehow extended to include a hook and
* was pruned.
*/
export function validatePreservedManualMemoization(
fn: ReactiveFunction,
): Result<void, CompilerError> {
export function validatePreservedManualMemoization(fn: ReactiveFunction): void {
const state = {
errors: new CompilerError(),
manualMemoState: null,
};
visitReactiveFunction(fn, new Visitor(), state);
return state.errors.asResult();
fn.env.recordErrors(state.errors);
}
const DEBUG = false;

View File

@@ -9,7 +9,7 @@ import {NodePath} from '@babel/traverse';
import * as t from '@babel/types';
import {CompilerDiagnostic, CompilerError, ErrorCategory} from '..';
import {CodegenFunction} from '../ReactiveScopes';
import {Result} from '../Utils/Result';
import {Environment} from '../HIR/Environment';
/**
* IMPORTANT: This validation is only intended for use in unit tests.
@@ -123,7 +123,8 @@ export function validateSourceLocations(
t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
>,
generatedAst: CodegenFunction,
): Result<void, CompilerError> {
env: Environment,
): void {
const errors = new CompilerError();
/*
@@ -309,5 +310,5 @@ export function validateSourceLocations(
}
}
return errors.asResult();
env.recordErrors(errors);
}

View File

@@ -20,9 +20,8 @@ import {
eachInstructionValueOperand,
eachTerminalOperand,
} from '../HIR/visitors';
import {Result} from '../Utils/Result';
export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
export function validateUseMemo(fn: HIRFunction): void {
const errors = new CompilerError();
const voidMemoErrors = new CompilerError();
const useMemos = new Set<IdentifierId>();
@@ -177,7 +176,9 @@ export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
}
}
fn.env.logErrors(voidMemoErrors.asResult());
return errors.asResult();
if (errors.hasAnyErrors()) {
fn.env.recordErrors(errors);
}
}
function validateNoContextVariableAssignment(

View File

@@ -24,18 +24,9 @@ function useThing(fn) {
```
Found 1 error:
Compilation Skipped: `this` is not supported syntax
Error: Expected a non-reserved identifier name
React Compiler does not support compiling functions that use `this`.
error.reserved-words.ts:8:28
6 |
7 | if (ref.current === null) {
> 8 | ref.current = function (this: unknown, ...args) {
| ^^^^^^^^^^^^^ `this` was used here
9 | return fnRef.current.call(this, ...args);
10 | };
11 | }
`this` is a reserved word in JavaScript and cannot be used as an identifier name.
```

View File

@@ -17,16 +17,17 @@ function Component(props) {
```
Found 1 error:
Todo: (BuildHIR::lowerAssignment) Handle computed properties in ObjectPattern
Invariant: [InferMutationAliasingEffects] Expected value kind to be initialized
error._todo.computed-lval-in-destructure.ts:3:9
1 | function Component(props) {
2 | const computedKey = props.key;
> 3 | const {[computedKey]: x} = props.val;
| ^^^^^^^^^^^^^^^^ (BuildHIR::lowerAssignment) Handle computed properties in ObjectPattern
<unknown> x$8.
error._todo.computed-lval-in-destructure.ts:5:9
3 | const {[computedKey]: x} = props.val;
4 |
5 | return x;
> 5 | return x;
| ^ this is uninitialized
6 | }
7 |
```

View File

@@ -0,0 +1,60 @@
## Input
```javascript
// @validateRefAccessDuringRender
/**
* This fixture tests fault tolerance: the compiler should report
* multiple independent errors rather than stopping at the first one.
*
* Error 1: Ref access during render (ref.current)
* Error 2: Mutation of frozen value (props)
*/
function Component(props) {
const ref = useRef(null);
// Error: reading ref during render
const value = ref.current;
// Error: mutating frozen value (props, which is frozen after hook call)
props.items = [];
return <div>{value}</div>;
}
```
## Error
```
Found 2 errors:
Error: This value cannot be modified
Modifying component props or hook arguments is not allowed. Consider using a local variable instead.
error.fault-tolerance-reports-multiple-errors.ts:16:2
14 |
15 | // Error: mutating frozen value (props, which is frozen after hook call)
> 16 | props.items = [];
| ^^^^^ value cannot be modified
17 |
18 | return <div>{value}</div>;
19 | }
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.fault-tolerance-reports-multiple-errors.ts:13:16
11 |
12 | // Error: reading ref during render
> 13 | const value = ref.current;
| ^^^^^^^^^^^ Cannot access ref value during render
14 |
15 | // Error: mutating frozen value (props, which is frozen after hook call)
16 | props.items = [];
```

View File

@@ -0,0 +1,19 @@
// @validateRefAccessDuringRender
/**
* This fixture tests fault tolerance: the compiler should report
* multiple independent errors rather than stopping at the first one.
*
* Error 1: Ref access during render (ref.current)
* Error 2: Mutation of frozen value (props)
*/
function Component(props) {
const ref = useRef(null);
// Error: reading ref during render
const value = ref.current;
// Error: mutating frozen value (props, which is frozen after hook call)
props.items = [];
return <div>{value}</div>;
}

View File

@@ -29,7 +29,7 @@ export const FIXTURE_ENTRYPOINT = {
## Error
```
Found 1 error:
Found 2 errors:
Error: This value cannot be modified
@@ -43,6 +43,32 @@ error.hook-call-freezes-captured-memberexpr.ts:13:2
14 | return <Stringify x={x} cb={cb} />;
15 | }
16 |
Error: Cannot modify local variables after render completes
This argument is a function which may reassign or mutate `x` after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead.
error.hook-call-freezes-captured-memberexpr.ts:9:25
7 | * After this custom hook call, it's no longer valid to mutate x.
8 | */
> 9 | const cb = useIdentity(() => {
| ^^^^^^^
> 10 | x.value++;
| ^^^^^^^^^^^^^^
> 11 | });
| ^^^^ This function may (indirectly) reassign or modify `x` after render
12 |
13 | x.value += count;
14 | return <Stringify x={x} cb={cb} />;
error.hook-call-freezes-captured-memberexpr.ts:10:4
8 | */
9 | const cb = useIdentity(() => {
> 10 | x.value++;
| ^ This modifies `x`
11 | });
12 |
13 | x.value += count;
```

View File

@@ -15,7 +15,7 @@ function component(a, b) {
## Error
```
Found 1 error:
Found 3 errors:
Error: useMemo() callbacks may not be async or generator functions
@@ -32,6 +32,37 @@ error.invalid-ReactUseMemo-async-callback.ts:2:24
5 | return x;
6 | }
7 |
Error: Found missing memoization dependencies
Missing dependencies can cause a value to update less often than it should, resulting in stale UI.
error.invalid-ReactUseMemo-async-callback.ts:3:10
1 | function component(a, b) {
2 | let x = React.useMemo(async () => {
> 3 | await a;
| ^ Missing dependency `a`
4 | }, []);
5 | return x;
6 | }
Inferred dependencies: `[a]`
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `a`, but the source dependencies were []. Inferred dependency not present in source.
error.invalid-ReactUseMemo-async-callback.ts:2:24
1 | function component(a, b) {
> 2 | let x = React.useMemo(async () => {
| ^^^^^^^^^^^^^
> 3 | await a;
| ^^^^^^^^^^^^
> 4 | }, []);
| ^^^^ Could not preserve existing manual memoization
5 | return x;
6 | }
7 |
```

View File

@@ -22,7 +22,7 @@ function Component({item, cond}) {
## Error
```
Found 2 errors:
Found 3 errors:
Error: Calling setState from useMemo may trigger an infinite loop
@@ -49,6 +49,39 @@ error.invalid-conditional-setState-in-useMemo.ts:8:6
9 | }
10 | }, [cond, key, init]);
11 |
Error: Found missing/extra memoization dependencies
Missing dependencies can cause a value to update less often than it should, resulting in stale UI. Extra dependencies can cause a value to update more often than it should, resulting in performance problems such as excessive renders or effects firing too often.
error.invalid-conditional-setState-in-useMemo.ts:7:18
5 | useMemo(() => {
6 | if (cond) {
> 7 | setPrevItem(item);
| ^^^^ Missing dependency `item`
8 | setState(0);
9 | }
10 | }, [cond, key, init]);
error.invalid-conditional-setState-in-useMemo.ts:10:12
8 | setState(0);
9 | }
> 10 | }, [cond, key, init]);
| ^^^ Unnecessary dependency `key`. Values declared outside of a component/hook should not be listed as dependencies as the component will not re-render if they change
11 |
12 | return state;
13 | }
error.invalid-conditional-setState-in-useMemo.ts:10:17
8 | setState(0);
9 | }
> 10 | }, [cond, key, init]);
| ^^^^ Unnecessary dependency `init`. Values declared outside of a component/hook should not be listed as dependencies as the component will not re-render if they change
11 |
12 | return state;
13 | }
Inferred dependencies: `[cond, item]`
```

View File

@@ -16,7 +16,7 @@ function useInvalidMutation(options) {
## Error
```
Found 1 error:
Found 2 errors:
Error: This value cannot be modified
@@ -30,6 +30,27 @@ error.invalid-mutation-in-closure.ts:4:4
5 | }
6 | return test;
7 | }
Error: Cannot modify local variables after render completes
This argument is a function which may reassign or mutate `options` after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead.
error.invalid-mutation-in-closure.ts:6:9
4 | options.foo = 'bar';
5 | }
> 6 | return test;
| ^^^^ This function may (indirectly) reassign or modify `options` after render
7 | }
8 |
error.invalid-mutation-in-closure.ts:4:4
2 | function test() {
3 | foo(options.foo); // error should not point on this line
> 4 | options.foo = 'bar';
| ^^^^^^^ This modifies `options`
5 | }
6 | return test;
7 | }
```

View File

@@ -15,7 +15,7 @@ function useFoo() {
## Error
```
Found 1 error:
Found 2 errors:
Error: Cannot reassign variable after render completes
@@ -29,6 +29,31 @@ error.invalid-reassign-local-in-hook-return-value.ts:4:4
5 | };
6 | }
7 |
Error: Cannot modify local variables after render completes
This argument is a function which may reassign or mutate `x` after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead.
error.invalid-reassign-local-in-hook-return-value.ts:3:9
1 | function useFoo() {
2 | let x = 0;
> 3 | return value => {
| ^^^^^^^^^^
> 4 | x = value;
| ^^^^^^^^^^^^^^
> 5 | };
| ^^^^ This function may (indirectly) reassign or modify `x` after render
6 | }
7 |
error.invalid-reassign-local-in-hook-return-value.ts:4:4
2 | let x = 0;
3 | return value => {
> 4 | x = value;
| ^ This modifies `x`
5 | };
6 | }
7 |
```

View File

@@ -47,7 +47,7 @@ function Component() {
## Error
```
Found 1 error:
Found 2 errors:
Error: Cannot reassign variable after render completes
@@ -61,6 +61,32 @@ error.invalid-reassign-local-variable-in-effect.ts:7:4
8 | };
9 |
10 | const onMount = newValue => {
Error: Cannot modify local variables after render completes
This argument is a function which may reassign or mutate `local` after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead.
error.invalid-reassign-local-variable-in-effect.ts:33:12
31 | };
32 |
> 33 | useEffect(() => {
| ^^^^^^^
> 34 | onMount();
| ^^^^^^^^^^^^^^
> 35 | }, [onMount]);
| ^^^^ This function may (indirectly) reassign or modify `local` after render
36 |
37 | return 'ok';
38 | }
error.invalid-reassign-local-variable-in-effect.ts:7:4
5 |
6 | const reassignLocal = newValue => {
> 7 | local = newValue;
| ^^^^^ This modifies `local`
8 | };
9 |
10 | const onMount = newValue => {
```

View File

@@ -48,7 +48,7 @@ function Component() {
## Error
```
Found 1 error:
Found 2 errors:
Error: Cannot reassign variable after render completes
@@ -62,6 +62,32 @@ error.invalid-reassign-local-variable-in-hook-argument.ts:8:4
9 | };
10 |
11 | const callback = newValue => {
Error: Cannot modify local variables after render completes
This argument is a function which may reassign or mutate `local` after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead.
error.invalid-reassign-local-variable-in-hook-argument.ts:34:14
32 | };
33 |
> 34 | useIdentity(() => {
| ^^^^^^^
> 35 | callback();
| ^^^^^^^^^^^^^^^
> 36 | });
| ^^^^ This function may (indirectly) reassign or modify `local` after render
37 |
38 | return 'ok';
39 | }
error.invalid-reassign-local-variable-in-hook-argument.ts:8:4
6 |
7 | const reassignLocal = newValue => {
> 8 | local = newValue;
| ^^^^^ This modifies `local`
9 | };
10 |
11 | const callback = newValue => {
```

View File

@@ -41,7 +41,7 @@ function Component() {
## Error
```
Found 1 error:
Found 2 errors:
Error: Cannot reassign variable after render completes
@@ -55,6 +55,27 @@ error.invalid-reassign-local-variable-in-jsx-callback.ts:5:4
6 | };
7 |
8 | const onClick = newValue => {
Error: Cannot modify local variables after render completes
This argument is a function which may reassign or mutate `local` after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead.
error.invalid-reassign-local-variable-in-jsx-callback.ts:31:26
29 | };
30 |
> 31 | return <button onClick={onClick}>Submit</button>;
| ^^^^^^^ This function may (indirectly) reassign or modify `local` after render
32 | }
33 |
error.invalid-reassign-local-variable-in-jsx-callback.ts:5:4
3 |
4 | const reassignLocal = newValue => {
> 5 | local = newValue;
| ^^^^^ This modifies `local`
6 | };
7 |
8 | const onClick = newValue => {
```

View File

@@ -26,7 +26,7 @@ function useKeyedState({key, init}) {
## Error
```
Found 1 error:
Found 3 errors:
Error: Calling setState from useMemo may trigger an infinite loop
@@ -40,6 +40,61 @@ error.invalid-setState-in-useMemo-indirect-useCallback.ts:13:4
14 | }, [key, init]);
15 |
16 | return state;
Error: Found missing memoization dependencies
Missing dependencies can cause a value to update less often than it should, resulting in stale UI.
error.invalid-setState-in-useMemo-indirect-useCallback.ts:9:13
7 | const fn = useCallback(() => {
8 | setPrevKey(key);
> 9 | setState(init);
| ^^^^ Missing dependency `init`
10 | });
11 |
12 | useMemo(() => {
error.invalid-setState-in-useMemo-indirect-useCallback.ts:8:15
6 |
7 | const fn = useCallback(() => {
> 8 | setPrevKey(key);
| ^^^ Missing dependency `key`
9 | setState(init);
10 | });
11 |
Error: Found missing/extra memoization dependencies
Missing dependencies can cause a value to update less often than it should, resulting in stale UI. Extra dependencies can cause a value to update more often than it should, resulting in performance problems such as excessive renders or effects firing too often.
error.invalid-setState-in-useMemo-indirect-useCallback.ts:13:4
11 |
12 | useMemo(() => {
> 13 | fn();
| ^^ Missing dependency `fn`
14 | }, [key, init]);
15 |
16 | return state;
error.invalid-setState-in-useMemo-indirect-useCallback.ts:14:6
12 | useMemo(() => {
13 | fn();
> 14 | }, [key, init]);
| ^^^ Unnecessary dependency `key`
15 |
16 | return state;
17 | }
error.invalid-setState-in-useMemo-indirect-useCallback.ts:14:11
12 | useMemo(() => {
13 | fn();
> 14 | }, [key, init]);
| ^^^^ Unnecessary dependency `init`
15 |
16 | return state;
17 | }
Inferred dependencies: `[fn]`
```

View File

@@ -15,7 +15,7 @@ function component(a, b) {
## Error
```
Found 1 error:
Found 3 errors:
Error: useMemo() callbacks may not be async or generator functions
@@ -32,6 +32,37 @@ error.invalid-useMemo-async-callback.ts:2:18
5 | return x;
6 | }
7 |
Error: Found missing memoization dependencies
Missing dependencies can cause a value to update less often than it should, resulting in stale UI.
error.invalid-useMemo-async-callback.ts:3:10
1 | function component(a, b) {
2 | let x = useMemo(async () => {
> 3 | await a;
| ^ Missing dependency `a`
4 | }, []);
5 | return x;
6 | }
Inferred dependencies: `[a]`
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `a`, but the source dependencies were []. Inferred dependency not present in source.
error.invalid-useMemo-async-callback.ts:2:18
1 | function component(a, b) {
> 2 | let x = useMemo(async () => {
| ^^^^^^^^^^^^^
> 3 | await a;
| ^^^^^^^^^^^^
> 4 | }, []);
| ^^^^ Could not preserve existing manual memoization
5 | return x;
6 | }
7 |
```

View File

@@ -13,7 +13,7 @@ function component(a, b) {
## Error
```
Found 1 error:
Found 3 errors:
Error: useMemo() callbacks may not accept parameters
@@ -26,6 +26,32 @@ error.invalid-useMemo-callback-args.ts:2:18
3 | return x;
4 | }
5 |
Error: Found missing memoization dependencies
Missing dependencies can cause a value to update less often than it should, resulting in stale UI.
error.invalid-useMemo-callback-args.ts:2:23
1 | function component(a, b) {
> 2 | let x = useMemo(c => a, []);
| ^ Missing dependency `a`
3 | return x;
4 | }
5 |
Inferred dependencies: `[a]`
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `a`, but the source dependencies were []. Inferred dependency not present in source.
error.invalid-useMemo-callback-args.ts:2:18
1 | function component(a, b) {
> 2 | let x = useMemo(c => a, []);
| ^^^^^^ Could not preserve existing manual memoization
3 | return x;
4 | }
5 |
```

View File

@@ -32,7 +32,7 @@ export const FIXTURE_ENTRYPOINT = {
## Error
```
Found 1 error:
Found 2 errors:
Error: Cannot reassign variable after render completes
@@ -46,6 +46,28 @@ error.mutable-range-shared-inner-outer-function.ts:8:6
9 | b = [];
10 | } else {
11 | a = {};
Error: Cannot modify local variables after render completes
This argument is a function which may reassign or mutate `a` after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead.
error.mutable-range-shared-inner-outer-function.ts:17:23
15 | b.push(false);
16 | };
> 17 | return <div onClick={f} />;
| ^ This function may (indirectly) reassign or modify `a` after render
18 | }
19 |
20 | export const FIXTURE_ENTRYPOINT = {
error.mutable-range-shared-inner-outer-function.ts:8:6
6 | const f = () => {
7 | if (cond) {
> 8 | a = {};
| ^ This modifies `a`
9 | b = [];
10 | } else {
11 | a = {};
```

View File

@@ -29,7 +29,7 @@ function useHook(parentRef) {
## Error
```
Found 1 error:
Found 2 errors:
Error: This value cannot be modified
@@ -43,6 +43,27 @@ error.todo-allow-assigning-to-inferred-ref-prop-in-callback.ts:15:8
16 | }
17 | }
18 | };
Error: Cannot modify local variables after render completes
This argument is a function which may reassign or mutate `parentRef` after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead.
error.todo-allow-assigning-to-inferred-ref-prop-in-callback.ts:19:9
17 | }
18 | };
> 19 | return handler;
| ^^^^^^^ This function may (indirectly) reassign or modify `parentRef` after render
20 | }
21 |
error.todo-allow-assigning-to-inferred-ref-prop-in-callback.ts:15:8
13 | } else {
14 | // So this assignment fails since we don't know its a ref
> 15 | parentRef.current = instance;
| ^^^^^^^^^ This modifies `parentRef`
16 | }
17 | }
18 | };
```

View File

@@ -17,7 +17,7 @@ function Component() {
## Error
```
Found 1 error:
Found 2 errors:
Error: Cannot reassign variable after render completes
@@ -31,6 +31,27 @@ error.todo-function-expression-references-later-variable-declaration.ts:3:4
4 | };
5 | let onClick;
6 |
Error: Cannot modify local variables after render completes
This argument is a function which may reassign or mutate `onClick` after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead.
error.todo-function-expression-references-later-variable-declaration.ts:7:23
5 | let onClick;
6 |
> 7 | return <div onClick={callback} />;
| ^^^^^^^^ This function may (indirectly) reassign or modify `onClick` after render
8 | }
9 |
error.todo-function-expression-references-later-variable-declaration.ts:3:4
1 | function Component() {
2 | let callback = () => {
> 3 | onClick = () => {};
| ^^^^^^^ This modifies `onClick`
4 | };
5 | let onClick;
6 |
```

View File

@@ -18,15 +18,18 @@ function Component() {
```
Found 1 error:
Todo: Support functions with unreachable code that may contain hoisted declarations
Invariant: [InferMutationAliasingEffects] Expected value kind to be initialized
error.todo-hoisted-function-in-unreachable-code.ts:6:2
<unknown> Foo$0.
error.todo-hoisted-function-in-unreachable-code.ts:3:10
1 | // @compilationMode:"infer"
2 | function Component() {
> 3 | return <Foo />;
| ^^^ this is uninitialized
4 |
5 | // This is unreachable from a control-flow perspective, but it gets hoisted
> 6 | function Foo() {}
| ^^^^^^^^^^^^^^^^^ Support functions with unreachable code that may contain hoisted declarations
7 | }
8 |
6 | function Foo() {}
```

View File

@@ -79,43 +79,11 @@ let moduleLocal = false;
## Error
```
Found 10 errors:
Found 1 error:
Todo: (BuildHIR::lowerStatement) Handle var kinds in VariableDeclaration
Invariant: Expected a variable declaration
error.todo-kitchensink.ts:3:2
1 | function foo([a, b], {c, d, e = 'e'}, f = 'f', ...args) {
2 | let i = 0;
> 3 | var x = [];
| ^^^^^^^^^^^ (BuildHIR::lowerStatement) Handle var kinds in VariableDeclaration
4 |
5 | class Bar {
6 | #secretSauce = 42;
Compilation Skipped: Inline `class` declarations are not supported
Move class declarations outside of components/hooks.
error.todo-kitchensink.ts:5:2
3 | var x = [];
4 |
> 5 | class Bar {
| ^^^^^^^^^^^
> 6 | #secretSauce = 42;
| ^^^^^^^^^^^^^^^^^^^^^^
> 7 | constructor() {
| ^^^^^^^^^^^^^^^^^^^^^^
> 8 | console.log(this.#secretSauce);
| ^^^^^^^^^^^^^^^^^^^^^^
> 9 | }
| ^^^^^^^^^^^^^^^^^^^^^^
> 10 | }
| ^^^^ Inline `class` declarations are not supported
11 |
12 | const g = {b() {}, c: () => {}};
13 | const {z, aa = 'aa'} = useCustom();
Todo: (BuildHIR::lowerStatement) Handle non-variable initialization in ForStatement
Got ExpressionStatement.
error.todo-kitchensink.ts:20:2
18 | const j = function bar([quz, qux], ...args) {};
@@ -125,103 +93,10 @@ error.todo-kitchensink.ts:20:2
> 21 | x.push(i);
| ^^^^^^^^^^^^^^
> 22 | }
| ^^^^ (BuildHIR::lowerStatement) Handle non-variable initialization in ForStatement
| ^^^^ Expected a variable declaration
23 | for (; i < 3; ) {
24 | break;
25 | }
Todo: (BuildHIR::lowerStatement) Handle non-variable initialization in ForStatement
error.todo-kitchensink.ts:23:2
21 | x.push(i);
22 | }
> 23 | for (; i < 3; ) {
| ^^^^^^^^^^^^^^^^^
> 24 | break;
| ^^^^^^^^^^
> 25 | }
| ^^^^ (BuildHIR::lowerStatement) Handle non-variable initialization in ForStatement
26 | for (;;) {
27 | break;
28 | }
Todo: (BuildHIR::lowerStatement) Handle non-variable initialization in ForStatement
error.todo-kitchensink.ts:26:2
24 | break;
25 | }
> 26 | for (;;) {
| ^^^^^^^^^^
> 27 | break;
| ^^^^^^^^^^
> 28 | }
| ^^^^ (BuildHIR::lowerStatement) Handle non-variable initialization in ForStatement
29 |
30 | graphql`
31 | ${g}
Todo: (BuildHIR::lowerStatement) Handle empty test in ForStatement
error.todo-kitchensink.ts:26:2
24 | break;
25 | }
> 26 | for (;;) {
| ^^^^^^^^^^
> 27 | break;
| ^^^^^^^^^^
> 28 | }
| ^^^^ (BuildHIR::lowerStatement) Handle empty test in ForStatement
29 |
30 | graphql`
31 | ${g}
Todo: (BuildHIR::lowerExpression) Handle tagged template with interpolations
error.todo-kitchensink.ts:30:2
28 | }
29 |
> 30 | graphql`
| ^^^^^^^^
> 31 | ${g}
| ^^^^^^^^
> 32 | `;
| ^^^^ (BuildHIR::lowerExpression) Handle tagged template with interpolations
33 |
34 | graphql`\\t\n`;
35 |
Todo: (BuildHIR::lowerExpression) Handle tagged template where cooked value is different from raw value
error.todo-kitchensink.ts:34:2
32 | `;
33 |
> 34 | graphql`\\t\n`;
| ^^^^^^^^^^^^^^ (BuildHIR::lowerExpression) Handle tagged template where cooked value is different from raw value
35 |
36 | for (c of [1, 2]) {
37 | }
Todo: (BuildHIR::node.lowerReorderableExpression) Expression type `MemberExpression` cannot be safely reordered
error.todo-kitchensink.ts:57:9
55 | case foo(): {
56 | }
> 57 | case x.y: {
| ^^^ (BuildHIR::node.lowerReorderableExpression) Expression type `MemberExpression` cannot be safely reordered
58 | }
59 | default: {
60 | }
Todo: (BuildHIR::node.lowerReorderableExpression) Expression type `BinaryExpression` cannot be safely reordered
error.todo-kitchensink.ts:53:9
51 |
52 | switch (i) {
> 53 | case 1 + 1: {
| ^^^^^ (BuildHIR::node.lowerReorderableExpression) Expression type `BinaryExpression` cannot be safely reordered
54 | }
55 | case foo(): {
56 | }
```

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

@@ -21,7 +21,7 @@ function Component({foo}) {
## Error
```
Found 1 error:
Found 3 errors:
Todo: Support destructuring of context variables
@@ -29,10 +29,34 @@ error.todo-reassign-const.ts:3:20
1 | import {Stringify} from 'shared-runtime';
2 |
> 3 | function Component({foo}) {
| ^^^ Support destructuring of context variables
| ^^^
4 | let bar = foo.bar;
5 | return (
6 | <Stringify
Todo: Support destructuring of context variables
error.todo-reassign-const.ts:3:20
1 | import {Stringify} from 'shared-runtime';
2 |
> 3 | function Component({foo}) {
| ^^^
4 | let bar = foo.bar;
5 | return (
6 | <Stringify
Error: This value cannot be modified
Modifying component props or hook arguments is not allowed. Consider using a local variable instead.
error.todo-reassign-const.ts:8:8
6 | <Stringify
7 | handler={() => {
> 8 | foo = true;
| ^^^ `foo` cannot be modified
9 | }}
10 | />
11 | );
```

View File

@@ -18,7 +18,7 @@ function component(a, b) {
## Error
```
Found 1 error:
Found 2 errors:
Todo: (BuildHIR::lowerExpression) Handle YieldExpression expressions
@@ -30,6 +30,23 @@ error.useMemo-callback-generator.ts:6:4
7 | }, []);
8 | return x;
9 | }
Error: useMemo() callbacks may not be async or generator functions
useMemo() callbacks are called once and must synchronously return a value.
error.useMemo-callback-generator.ts:5:18
3 | // useful for now, but adding this test in case we do
4 | // add support for generators in the future.
> 5 | let x = useMemo(function* () {
| ^^^^^^^^^^^^^^
> 6 | yield a;
| ^^^^^^^^^^^^
> 7 | }, []);
| ^^^^ Async and generator functions are not supported
8 | return x;
9 | }
10 |
```

View File

@@ -51,7 +51,7 @@ function Component({x, y, z}) {
## Error
```
Found 4 errors:
Found 6 errors:
Error: Found missing/extra memoization dependencies
@@ -157,6 +157,48 @@ error.invalid-exhaustive-deps.ts:37:13
40 | }, []);
Inferred dependencies: `[ref]`
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `x.y.z.a.b`, but the source dependencies were [x?.y.z.a?.b.z]. Inferred different dependency than source.
error.invalid-exhaustive-deps.ts:14:20
12 | // ok, not our job to type check nullability
13 | }, [x.y.z.a]);
> 14 | const c = useMemo(() => {
| ^^^^^^^
> 15 | return x?.y.z.a?.b;
| ^^^^^^^^^^^^^^^^^^^^^^^
> 16 | // error: too precise
| ^^^^^^^^^^^^^^^^^^^^^^^
> 17 | }, [x?.y.z.a?.b.z]);
| ^^^^ Could not preserve existing manual memoization
18 | const d = useMemo(() => {
19 | return x?.y?.[(console.log(y), z?.b)];
20 | // ok
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `ref`, but the source dependencies were []. Inferred dependency not present in source.
error.invalid-exhaustive-deps.ts:35:21
33 | const ref2 = useRef(null);
34 | const ref = z ? ref1 : ref2;
> 35 | const cb = useMemo(() => {
| ^^^^^^^
> 36 | return () => {
| ^^^^^^^^^^^^^^^^^^
> 37 | return ref.current;
| ^^^^^^^^^^^^^^^^^^
> 38 | };
| ^^^^^^^^^^^^^^^^^^
> 39 | // error: ref is a stable type but reactive
| ^^^^^^^^^^^^^^^^^^
> 40 | }, []);
| ^^^^ Could not preserve existing manual memoization
41 | return <Stringify results={[a, b, c, d, e, f, cb]} />;
42 | }
43 |
```

View File

@@ -22,7 +22,7 @@ function useHook() {
## Error
```
Found 1 error:
Found 2 errors:
Error: Found missing memoization dependencies
@@ -38,6 +38,19 @@ error.invalid-missing-nonreactive-dep-unmemoized.ts:11:31
14 |
Inferred dependencies: `[object]`
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `object`, but the source dependencies were []. Inferred dependency not present in source.
error.invalid-missing-nonreactive-dep-unmemoized.ts:11:24
9 | useIdentity();
10 | object.x = 0;
> 11 | const array = useMemo(() => [object], []);
| ^^^^^^^^^^^^^^ Could not preserve existing manual memoization
12 | return array;
13 | }
14 |
```

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

@@ -42,7 +42,7 @@ function Component() {
## Error
```
Found 1 error:
Found 2 errors:
Error: Cannot reassign variable after render completes
@@ -56,6 +56,27 @@ error.invalid-reassign-local-variable-in-jsx-callback.ts:6:4
7 | };
8 |
9 | const onClick = newValue => {
Error: Cannot modify local variables after render completes
This argument is a function which may reassign or mutate `local` after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead.
error.invalid-reassign-local-variable-in-jsx-callback.ts:32:26
30 | };
31 |
> 32 | return <button onClick={onClick}>Submit</button>;
| ^^^^^^^ This function may (indirectly) reassign or modify `local` after render
33 | }
34 |
error.invalid-reassign-local-variable-in-jsx-callback.ts:6:4
4 |
5 | const reassignLocal = newValue => {
> 6 | local = newValue;
| ^^^^^ This modifies `local`
7 | };
8 |
9 | const onClick = newValue => {
```

View File

@@ -31,7 +31,7 @@ function Component({content, refetch}) {
## Error
```
Found 1 error:
Found 2 errors:
Error: Cannot access variable before it is declared
@@ -52,6 +52,18 @@ Error: Cannot access variable before it is declared
20 |
21 | return <Foo data={data} onSubmit={onSubmit} />;
22 | }
Error: Found missing memoization dependencies
Missing dependencies can cause a value to update less often than it should, resulting in stale UI.
9 | // TDZ violation!
10 | const onRefetch = useCallback(() => {
> 11 | refetch(data);
| ^^^^ Missing dependency `data`
12 | }, [refetch]);
13 |
14 | // The context variable gets frozen here since it's passed to a hook
```

View File

@@ -30,7 +30,7 @@ function useFoo(input1) {
## Error
```
Found 1 error:
Found 2 errors:
Error: Found missing memoization dependencies
@@ -46,6 +46,23 @@ error.useMemo-unrelated-mutation-in-depslist.ts:18:14
21 | }
Inferred dependencies: `[x, y]`
Compilation Skipped: Existing memoization could not be preserved
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `input1`, but the source dependencies were [y]. Inferred different dependency than source.
error.useMemo-unrelated-mutation-in-depslist.ts:16:27
14 | const x = {};
15 | const y = [input1];
> 16 | const memoized = useMemo(() => {
| ^^^^^^^
> 17 | return [y];
| ^^^^^^^^^^^^^^^
> 18 | }, [(mutate(x), y)]);
| ^^^^ Could not preserve existing manual memoization
19 |
20 | return [x, memoized];
21 | }
```

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

@@ -16,29 +16,24 @@ function Component(props) {
## Error
```
Found 2 errors:
Found 1 error:
Error: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)
Invariant: Unexpected empty block with `goto` terminal
error.invalid-hook-for.ts:4:9
2 | let i = 0;
3 | for (let x = 0; useHook(x) < 10; useHook(i), x++) {
> 4 | i += useHook(x);
| ^^^^^^^ Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)
5 | }
6 | return i;
7 | }
Block bb5 is empty.
Error: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)
error.invalid-hook-for.ts:3:35
error.invalid-hook-for.ts:3:2
1 | function Component(props) {
2 | let i = 0;
> 3 | for (let x = 0; useHook(x) < 10; useHook(i), x++) {
| ^^^^^^^ Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)
4 | i += useHook(x);
5 | }
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> 4 | i += useHook(x);
| ^^^^^^^^^^^^^^^^^^^^
> 5 | }
| ^^^^ Unexpected empty block with `goto` terminal
6 | return i;
7 | }
8 |
```

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

@@ -23,16 +23,18 @@ export const FIXTURE_ENTRYPOINT = {
```
Found 1 error:
Todo: (BuildHIR::lowerAssignment) Handle computed properties in ObjectPattern
Invariant: [InferMutationAliasingEffects] Expected value kind to be initialized
todo.error.object-pattern-computed-key.ts:5:9
3 | const SCALE = 2;
<unknown> value$3.
todo.error.object-pattern-computed-key.ts:6:9
4 | function Component(props) {
> 5 | const {[props.name]: value} = props;
| ^^^^^^^^^^^^^^^^^^^ (BuildHIR::lowerAssignment) Handle computed properties in ObjectPattern
6 | return value;
5 | const {[props.name]: value} = props;
> 6 | return value;
| ^^^^^ this is uninitialized
7 | }
8 |
9 | export const FIXTURE_ENTRYPOINT = {
```

View File

@@ -25,6 +25,7 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { useRef } from "react";
const useControllableState = (options) => {};

View File

@@ -64,6 +64,9 @@ testRule(
makeTestCaseError(
'Capitalized functions are reserved for components',
),
makeTestCaseError(
'Capitalized functions are reserved for components',
),
],
},
],

View File

@@ -57,7 +57,6 @@ testRule('plugin-recommended', TestRecommendedRules, {
],
invalid: [
{
// TODO: actually return multiple diagnostics in this case
name: 'Multiple diagnostic kinds from the same function are surfaced',
code: normalizeIndent`
import Child from './Child';
@@ -70,6 +69,7 @@ testRule('plugin-recommended', TestRecommendedRules, {
`,
errors: [
makeTestCaseError('Hooks must always be called in a consistent order'),
makeTestCaseError('Capitalized functions are reserved for components'),
],
},
{
@@ -128,6 +128,7 @@ testRule('plugin-recommended', TestRecommendedRules, {
makeTestCaseError(
'Calling setState from useMemo may trigger an infinite loop',
),
makeTestCaseError('Found extra memoization dependencies'),
],
},
],

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