@samitouri / QOS-React-2 / commits / 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

Joseph Savona committed Feb 23, 2026 at 15:35 UTC e3e5d95cc457eb1ba54431bc95604aa931fc6adf
12 files changed +86 -95
compiler/fault-tolerance-overview.md
+9 -9
@@ -127,49 +127,49 @@ All validation passes need to record errors on the environment instead of return
127
128 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.
129
130 -- [ ] **4.1 `validateHooksUsage`** (`src/Validation/ValidateHooksUsage.ts`)
130 +- [x] **4.1 `validateHooksUsage`** (`src/Validation/ValidateHooksUsage.ts`)
131 - Change signature from `(fn: HIRFunction): Result<void, CompilerError>` to `(fn: HIRFunction): void`
132 - Record errors on `fn.env` instead of returning `errors.asResult()`
133 - Update Pipeline.ts call site (line 211): remove `.unwrap()`
134
135 -- [ ] **4.2 `validateNoCapitalizedCalls`** (`src/Validation/ValidateNoCapitalizedCalls.ts`)
135 +- [x] **4.2 `validateNoCapitalizedCalls`** (`src/Validation/ValidateNoCapitalizedCalls.ts`)
136 - Change signature to return void
137 - Fix the hybrid pattern: the direct `CallExpression` path currently throws via `CompilerError.throwInvalidReact()` — change to record on env
138 - The `MethodCall` path already accumulates — change to record on env
139 - Update Pipeline.ts call site (line 214): remove `.unwrap()`
140
141 -- [ ] **4.3 `validateUseMemo`** (`src/Validation/ValidateUseMemo.ts`)
141 +- [x] **4.3 `validateUseMemo`** (`src/Validation/ValidateUseMemo.ts`)
142 - Change signature to return void
143 - Record hard errors on env instead of returning `errors.asResult()`
144 - The soft `voidMemoErrors` path already uses `env.logErrors()` — keep as-is or also record
145 - Update Pipeline.ts call site (line 170): remove `.unwrap()`
146
147 -- [ ] **4.4 `dropManualMemoization`** (`src/Inference/DropManualMemoization.ts`)
147 +- [x] **4.4 `dropManualMemoization`** (`src/Inference/DropManualMemoization.ts`)
148 - Change signature to return void
149 - Record errors on env instead of returning `errors.asResult()`
150 - Update Pipeline.ts call site (line 178): remove `.unwrap()`
151
152 -- [ ] **4.5 `validateNoRefAccessInRender`** (`src/Validation/ValidateNoRefAccessInRender.ts`)
152 +- [x] **4.5 `validateNoRefAccessInRender`** (`src/Validation/ValidateNoRefAccessInRender.ts`)
153 - Change signature to return void
154 - Record errors on env instead of returning Result
155 - Update Pipeline.ts call site (line 275): remove `.unwrap()`
156
157 -- [ ] **4.6 `validateNoSetStateInRender`** (`src/Validation/ValidateNoSetStateInRender.ts`)
157 +- [x] **4.6 `validateNoSetStateInRender`** (`src/Validation/ValidateNoSetStateInRender.ts`)
158 - Change signature to return void
159 - Record errors on env
160 - Update Pipeline.ts call site (line 279): remove `.unwrap()`
161
162 -- [ ] **4.7 `validateNoImpureFunctionsInRender`** (`src/Validation/ValidateNoImpureFunctionsInRender.ts`)
162 +- [x] **4.7 `validateNoImpureFunctionsInRender`** (`src/Validation/ValidateNoImpureFunctionsInRender.ts`)
163 - Change signature to return void
164 - Record errors on env
165 - Update Pipeline.ts call site (line 300): remove `.unwrap()`
166
167 -- [ ] **4.8 `validateNoFreezingKnownMutableFunctions`** (`src/Validation/ValidateNoFreezingKnownMutableFunctions.ts`)
167 +- [x] **4.8 `validateNoFreezingKnownMutableFunctions`** (`src/Validation/ValidateNoFreezingKnownMutableFunctions.ts`)
168 - Change signature to return void
169 - Record errors on env
170 - Update Pipeline.ts call site (line 303): remove `.unwrap()`
171
172 -- [ ] **4.9 `validateExhaustiveDependencies`** (`src/Validation/ValidateExhaustiveDependencies.ts`)
172 +- [x] **4.9 `validateExhaustiveDependencies`** (`src/Validation/ValidateExhaustiveDependencies.ts`)
173 - Change signature to return void
174 - Record errors on env
175 - Update Pipeline.ts call site (line 315): remove `.unwrap()`
compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts
+8 -22
@@ -164,14 +164,10 @@ function runWithEnvironment(
164 env.tryRecord(() => {
165 validateContextVariableLValues(hir);
166 });
167 - env.tryRecord(() => {
168 - validateUseMemo(hir).unwrap();
169 - });
167 + validateUseMemo(hir);
168
169 if (env.enableDropManualMemoization) {
172 - env.tryRecord(() => {
173 - dropManualMemoization(hir).unwrap();
174 - });
170 + dropManualMemoization(hir);
171 log({kind: 'hir', name: 'DropManualMemoization', value: hir});
172 }
173
@@ -204,14 +200,10 @@ function runWithEnvironment(
200
201 if (env.enableValidations) {
202 if (env.config.validateHooksUsage) {
207 - env.tryRecord(() => {
208 - validateHooksUsage(hir).unwrap();
209 - });
203 + validateHooksUsage(hir);
204 }
205 if (env.config.validateNoCapitalizedCalls) {
212 - env.tryRecord(() => {
213 - validateNoCapitalizedCalls(hir).unwrap();
214 - });
206 + validateNoCapitalizedCalls(hir);
207 }
208 }
209
@@ -259,15 +251,11 @@ function runWithEnvironment(
251 }
252
253 if (env.config.validateRefAccessDuringRender) {
262 - env.tryRecord(() => {
263 - validateNoRefAccessInRender(hir).unwrap();
264 - });
254 + validateNoRefAccessInRender(hir);
255 }
256
257 if (env.config.validateNoSetStateInRender) {
268 - env.tryRecord(() => {
269 - validateNoSetStateInRender(hir).unwrap();
270 - });
258 + validateNoSetStateInRender(hir);
259 }
260
261 if (
@@ -290,7 +278,7 @@ function runWithEnvironment(
278 }
279
280 env.tryRecord(() => {
293 - validateNoFreezingKnownMutableFunctions(hir).unwrap();
281 + validateNoFreezingKnownMutableFunctions(hir);
282 });
283 }
284
@@ -303,9 +291,7 @@ function runWithEnvironment(
291 env.config.validateExhaustiveEffectDependencies
292 ) {
293 // NOTE: this relies on reactivity inference running first
306 - env.tryRecord(() => {
307 - validateExhaustiveDependencies(hir).unwrap();
308 - });
294 + validateExhaustiveDependencies(hir);
295 }
296 }
297
compiler/packages/babel-plugin-react-compiler/src/Inference/DropManualMemoization.ts
+4 -5
@@ -31,7 +31,6 @@ import {
31 makeInstructionId,
32 } from '../HIR';
33 import {createTemporaryPlace, markInstructionIds} from '../HIR/HIRBuilder';
34 -import {Result} from '../Utils/Result';
34
35 type ManualMemoCallee = {
36 kind: 'useMemo' | 'useCallback';
@@ -389,9 +388,7 @@ function extractManualMemoizationArgs(
388 * This pass also validates that useMemo callbacks return a value (not void), ensuring that useMemo
389 * is only used for memoizing values and not for running arbitrary side effects.
390 */
392 -export function dropManualMemoization(
393 - func: HIRFunction,
394 -): Result<void, CompilerError> {
391 +export function dropManualMemoization(func: HIRFunction): void {
392 const errors = new CompilerError();
393 const isValidationEnabled =
394 func.env.config.validatePreserveExistingMemoizationGuarantees ||
@@ -553,7 +550,9 @@ export function dropManualMemoization(
550 }
551 }
552
556 - return errors.asResult();
553 + if (errors.hasAnyErrors()) {
554 + func.env.recordErrors(errors);
555 + }
556 }
557
558 function findOptionalPlaces(fn: HIRFunction): Set<IdentifierId> {
compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateExhaustiveDependencies.ts
+4 -5
@@ -44,7 +44,6 @@ import {
44 eachInstructionValueOperand,
45 eachTerminalOperand,
46 } from '../HIR/visitors';
47 -import {Result} from '../Utils/Result';
47 import {retainWhere} from '../Utils/utils';
48
49 const DEBUG = false;
@@ -88,9 +87,7 @@ const DEBUG = false;
87 * When we go to compute the dependencies, we then think that the user's manual dep
88 * logic is part of what the memo computation logic.
89 */
91 -export function validateExhaustiveDependencies(
92 - fn: HIRFunction,
93 -): Result<void, CompilerError> {
90 +export function validateExhaustiveDependencies(fn: HIRFunction): void {
91 const env = fn.env;
92 const reactive = collectReactiveIdentifiersHIR(fn);
93
@@ -217,7 +214,9 @@ export function validateExhaustiveDependencies(
214 },
215 false, // isFunctionExpression
216 );
220 - return error.asResult();
217 + if (error.hasAnyErrors()) {
218 + fn.env.recordErrors(error);
219 + }
220 }
221
222 function validateDependencies(
compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateHooksUsage.ts
+4 -5
@@ -26,7 +26,6 @@ import {
26 eachTerminalOperand,
27 } from '../HIR/visitors';
28 import {assertExhaustive} from '../Utils/utils';
29 -import {Result} from '../Utils/Result';
29
30 /**
31 * 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 {
87 * may not appear as the callee of a conditional call.
88 * See the note for Kind.PotentialHook for sources of potential hooks
89 */
91 -export function validateHooksUsage(
92 - fn: HIRFunction,
93 -): Result<void, CompilerError> {
90 +export function validateHooksUsage(fn: HIRFunction): void {
91 const unconditionalBlocks = computeUnconditionalBlocks(fn);
92
93 const errors = new CompilerError();
@@ -426,7 +423,9 @@ export function validateHooksUsage(
423 for (const [, error] of errorsByPlace) {
424 errors.pushErrorDetail(error);
425 }
429 - return errors.asResult();
426 + if (errors.hasAnyErrors()) {
427 + fn.env.recordErrors(errors);
428 + }
429 }
430
431 function visitFunctionExpression(errors: CompilerError, fn: HIRFunction): void {
compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoCapitalizedCalls.ts
+15 -13
@@ -5,15 +5,12 @@
5 * LICENSE file in the root directory of this source tree.
6 */
7
8 -import {CompilerError, EnvironmentConfig} from '..';
8 +import {CompilerError, CompilerErrorDetail, EnvironmentConfig} from '..';
9 import {ErrorCategory} from '../CompilerError';
10 import {HIRFunction, IdentifierId} from '../HIR';
11 import {DEFAULT_GLOBALS} from '../HIR/Globals';
12 -import {Result} from '../Utils/Result';
12
14 -export function validateNoCapitalizedCalls(
15 - fn: HIRFunction,
16 -): Result<void, CompilerError> {
13 +export function validateNoCapitalizedCalls(fn: HIRFunction): void {
14 const envConfig: EnvironmentConfig = fn.env.config;
15 const ALLOW_LIST = new Set([
16 ...DEFAULT_GLOBALS.keys(),
@@ -48,13 +45,16 @@ export function validateNoCapitalizedCalls(
45 const calleeIdentifier = value.callee.identifier.id;
46 const calleeName = capitalLoadGlobals.get(calleeIdentifier);
47 if (calleeName != null) {
51 - CompilerError.throwInvalidReact({
52 - category: ErrorCategory.CapitalizedCalls,
53 - reason,
54 - description: `${calleeName} may be a component`,
55 - loc: value.loc,
56 - suggestions: null,
57 - });
48 + fn.env.recordError(
49 + new CompilerErrorDetail({
50 + category: ErrorCategory.CapitalizedCalls,
51 + reason,
52 + description: `${calleeName} may be a component`,
53 + loc: value.loc,
54 + suggestions: null,
55 + }),
56 + );
57 + continue;
58 }
59 break;
60 }
@@ -85,5 +85,7 @@ export function validateNoCapitalizedCalls(
85 }
86 }
87 }
88 - return errors.asResult();
88 + if (errors.hasAnyErrors()) {
89 + fn.env.recordErrors(errors);
90 + }
91 }
compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoFreezingKnownMutableFunctions.ts
+4 -5
@@ -18,7 +18,6 @@ import {
18 eachTerminalOperand,
19 } from '../HIR/visitors';
20 import {AliasingEffect} from '../Inference/AliasingEffects';
21 -import {Result} from '../Utils/Result';
21
22 /**
23 * Validates that functions with known mutations (ie due to types) cannot be passed
@@ -43,9 +42,7 @@ import {Result} from '../Utils/Result';
42 * This pass detects functions with *known* mutations (Store or Mutate, not ConditionallyMutate)
43 * that are passed where a frozen value is expected and rejects them.
44 */
46 -export function validateNoFreezingKnownMutableFunctions(
47 - fn: HIRFunction,
48 -): Result<void, CompilerError> {
45 +export function validateNoFreezingKnownMutableFunctions(fn: HIRFunction): void {
46 const errors = new CompilerError();
47 const contextMutationEffects: Map<
48 IdentifierId,
@@ -162,5 +159,7 @@ export function validateNoFreezingKnownMutableFunctions(
159 visitOperand(operand);
160 }
161 }
165 - return errors.asResult();
162 + if (errors.hasAnyErrors()) {
163 + fn.env.recordErrors(errors);
164 + }
165 }
compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoImpureFunctionsInRender.ts
+4 -5
@@ -9,7 +9,6 @@ import {CompilerDiagnostic, CompilerError} from '..';
9 import {ErrorCategory} from '../CompilerError';
10 import {HIRFunction} from '../HIR';
11 import {getFunctionCallSignature} from '../Inference/InferMutationAliasingEffects';
12 -import {Result} from '../Utils/Result';
12
13 /**
14 * Checks that known-impure functions are not called during render. Examples of invalid functions to
@@ -20,9 +19,7 @@ import {Result} from '../Utils/Result';
19 * this in several of our validation passes and should unify those analyses into a reusable helper
20 * and use it here.
21 */
23 -export function validateNoImpureFunctionsInRender(
24 - fn: HIRFunction,
25 -): Result<void, CompilerError> {
22 +export function validateNoImpureFunctionsInRender(fn: HIRFunction): void {
23 const errors = new CompilerError();
24 for (const [, block] of fn.body.blocks) {
25 for (const instr of block.instructions) {
@@ -55,5 +52,7 @@ export function validateNoImpureFunctionsInRender(
52 }
53 }
54 }
58 - return errors.asResult();
55 + if (errors.hasAnyErrors()) {
56 + fn.env.recordErrors(errors);
57 + }
58 }
compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts
+16 -15
@@ -27,7 +27,6 @@ import {
27 eachPatternOperand,
28 eachTerminalOperand,
29 } from '../HIR/visitors';
30 -import {Err, Ok, Result} from '../Utils/Result';
30 import {retainWhere} from '../Utils/utils';
31
32 /**
@@ -120,12 +119,14 @@ class Env {
119 }
120 }
121
123 -export function validateNoRefAccessInRender(
124 - fn: HIRFunction,
125 -): Result<void, CompilerError> {
122 +export function validateNoRefAccessInRender(fn: HIRFunction): void {
123 const env = new Env();
124 collectTemporariesSidemap(fn, env);
128 - return validateNoRefAccessInRenderImpl(fn, env).map(_ => undefined);
125 + const errors = new CompilerError();
126 + validateNoRefAccessInRenderImpl(fn, env, errors);
127 + if (errors.hasAnyErrors()) {
128 + fn.env.recordErrors(errors);
129 + }
130 }
131
132 function collectTemporariesSidemap(fn: HIRFunction, env: Env): void {
@@ -305,7 +306,8 @@ function joinRefAccessTypes(...types: Array<RefAccessType>): RefAccessType {
306 function validateNoRefAccessInRenderImpl(
307 fn: HIRFunction,
308 env: Env,
308 -): Result<RefAccessType, CompilerError> {
309 + errors: CompilerError,
310 +): RefAccessType {
311 let returnValues: Array<undefined | RefAccessType> = [];
312 let place;
313 for (const param of fn.params) {
@@ -336,7 +338,6 @@ function validateNoRefAccessInRenderImpl(
338 env.resetChanged();
339 returnValues = [];
340 const safeBlocks: Array<{block: BlockId; ref: RefId}> = [];
339 - const errors = new CompilerError();
341 for (const [, block] of fn.body.blocks) {
342 retainWhere(safeBlocks, entry => entry.block !== block.id);
343 for (const phi of block.phis) {
@@ -432,13 +433,15 @@ function validateNoRefAccessInRenderImpl(
433 case 'FunctionExpression': {
434 let returnType: RefAccessType = {kind: 'None'};
435 let readRefEffect = false;
436 + const innerErrors = new CompilerError();
437 const result = validateNoRefAccessInRenderImpl(
438 instr.value.loweredFunc.func,
439 env,
440 + innerErrors,
441 );
439 - if (result.isOk()) {
440 - returnType = result.unwrap();
441 - } else if (result.isErr()) {
442 + if (!innerErrors.hasAnyErrors()) {
443 + returnType = result;
444 + } else {
445 readRefEffect = true;
446 }
447 env.set(instr.lvalue.identifier.id, {
@@ -729,7 +732,7 @@ function validateNoRefAccessInRenderImpl(
732 }
733
734 if (errors.hasAnyErrors()) {
732 - return Err(errors);
735 + return {kind: 'None'};
736 }
737 }
738
@@ -738,10 +741,8 @@ function validateNoRefAccessInRenderImpl(
741 loc: GeneratedSource,
742 });
743
741 - return Ok(
742 - joinRefAccessTypes(
743 - ...returnValues.filter((env): env is RefAccessType => env !== undefined),
744 - ),
744 + return joinRefAccessTypes(
745 + ...returnValues.filter((env): env is RefAccessType => env !== undefined),
746 );
747 }
748
compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoSetStateInRender.ts
+11 -8
@@ -13,7 +13,6 @@ import {
13 import {HIRFunction, IdentifierId, isSetStateType} from '../HIR';
14 import {computeUnconditionalBlocks} from '../HIR/ComputeUnconditionalBlocks';
15 import {eachInstructionValueOperand} from '../HIR/visitors';
16 -import {Result} from '../Utils/Result';
16
17 /**
18 * Validates that the given function does not have an infinite update loop
@@ -43,17 +42,21 @@ import {Result} from '../Utils/Result';
42 * y();
43 * ```
44 */
46 -export function validateNoSetStateInRender(
47 - fn: HIRFunction,
48 -): Result<void, CompilerError> {
45 +export function validateNoSetStateInRender(fn: HIRFunction): void {
46 const unconditionalSetStateFunctions: Set<IdentifierId> = new Set();
50 - return validateNoSetStateInRenderImpl(fn, unconditionalSetStateFunctions);
47 + const errors = validateNoSetStateInRenderImpl(
48 + fn,
49 + unconditionalSetStateFunctions,
50 + );
51 + if (errors.hasAnyErrors()) {
52 + fn.env.recordErrors(errors);
53 + }
54 }
55
56 function validateNoSetStateInRenderImpl(
57 fn: HIRFunction,
58 unconditionalSetStateFunctions: Set<IdentifierId>,
56 -): Result<void, CompilerError> {
59 +): CompilerError {
60 const unconditionalBlocks = computeUnconditionalBlocks(fn);
61 let activeManualMemoId: number | null = null;
62 const errors = new CompilerError();
@@ -92,7 +95,7 @@ function validateNoSetStateInRenderImpl(
95 validateNoSetStateInRenderImpl(
96 instr.value.loweredFunc.func,
97 unconditionalSetStateFunctions,
95 - ).isErr()
98 + ).hasAnyErrors()
99 ) {
100 // This function expression unconditionally calls a setState
101 unconditionalSetStateFunctions.add(instr.lvalue.identifier.id);
@@ -183,5 +186,5 @@ function validateNoSetStateInRenderImpl(
186 }
187 }
188
186 - return errors.asResult();
189 + return errors;
190 }
compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateUseMemo.ts
+4 -3
@@ -20,9 +20,8 @@ import {
20 eachInstructionValueOperand,
21 eachTerminalOperand,
22 } from '../HIR/visitors';
23 -import {Result} from '../Utils/Result';
23
25 -export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
24 +export function validateUseMemo(fn: HIRFunction): void {
25 const errors = new CompilerError();
26 const voidMemoErrors = new CompilerError();
27 const useMemos = new Set<IdentifierId>();
@@ -177,7 +176,9 @@ export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
176 }
177 }
178 fn.env.logErrors(voidMemoErrors.asResult());
180 - return errors.asResult();
179 + if (errors.hasAnyErrors()) {
180 + fn.env.recordErrors(errors);
181 + }
182 }
183
184 function validateNoContextVariableAssignment(
compiler/packages/eslint-plugin-react-compiler/__tests__/NoCapitalizedCallsRule-test.ts
+3
@@ -64,6 +64,9 @@ testRule(
64 makeTestCaseError(
65 'Capitalized functions are reserved for components',
66 ),
67 + makeTestCaseError(
68 + 'Capitalized functions are reserved for components',
69 + ),
70 ],
71 },
72 ],