@samitouri / QOS-React-1 / commits / 3a495ae722

[compiler] source location validator (#35109)

@josephsavona this was briefly discussed in an old thread, lmk your thoughts on the approach. I have some fixes ready as well but wanted to get this test case in first... there's some things I don't _love_ about this approach, but end of the day it's just a tool for the test suite rather than something for end user folks so even if it does a 70% good enough job that's fine. ### refresher on the problem when we generate coverage reports with jest (istanbul), our coverage ends up completely out of whack due to the AST missing a ton of (let's call them "important") source locations after the compiler pipeline has run. At the moment to get around this, we've been doing something a bit unorthodox and also running our test suite with istanbul running before the compiler -- which results in its own set of issues (for eg, things being memoized differently, or the compiler completely bailing out on the instrumented code, etc). before getting in fixes, I wanted to set up a test case to start chipping away on as you had recommended. ### how it works The validator basically: 1. Traverses the original AST and collects the source locations for some "important" node types - (excludes useMemo/useCallback calls, as those are stripped out by the compiler) 3. Traverses the generated AST and looks for nodes with matching source locations. 4. Generates errors for source locations missing nodes in the generated AST ### caveats/drawbacks There are some things that don't work super well with this approach. A more natural test fit I think would be just having some explicit assertions made against an AST in a test file, as you can just bake all of the assumptions/nuance in there that are difficult to handle in a generic manner. However, this is maybe "good enough" for now. 1. Have to be careful what you put into the test fixture. If you put in some code that the compiler just removes (for eg, a variable assignment that is unused), you're creating a failure case that's impossible to fix. I added a skip for useMemo/useCallback. 2. "Important" locations must exactly match for validation to pass. - Might get tricky making sure things are mapped correctly when a node type is completely changed, for eg, when a block statement arrow function body gets turned into an implicit return via the body just being an expression/identifier. - This can/could result in scenarios where more changes are needed to shuttle the locations through due to HIR not having a 1:1 mapping all the babel nuances, even if some combination of other data might be good enough even if not 10000% accurate. This might be the _right_ thing anyways so we don't end up with edge cases having incorrect source locations.

Nathan committed Nov 12, 2025 at 22:02 UTC 3a495ae72264c46b4a4355904c6b4958b0a2f9b2
6 files changed +472
compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts
+5
@@ -105,6 +105,7 @@ import {inferMutationAliasingRanges} from '../Inference/InferMutationAliasingRan
105 import {validateNoDerivedComputationsInEffects} from '../Validation/ValidateNoDerivedComputationsInEffects';
106 import {validateNoDerivedComputationsInEffects_exp} from '../Validation/ValidateNoDerivedComputationsInEffects_exp';
107 import {nameAnonymousFunctions} from '../Transform/NameAnonymousFunctions';
108 +import {validateSourceLocations} from '../Validation/ValidateSourceLocations';
109
110 export type CompilerPipelineValue =
111 | {kind: 'ast'; name: string; value: CodegenFunction}
@@ -557,6 +558,10 @@ function runWithEnvironment(
558 log({kind: 'ast', name: 'Codegen (outlined)', value: outlined.fn});
559 }
560
561 + if (env.config.validateSourceLocations) {
562 + validateSourceLocations(func, ast).unwrap();
563 + }
564 +
565 /**
566 * This flag should be only set for unit / fixture tests to check
567 * that Forget correctly handles unexpected errors (e.g. exceptions
compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts
+7
@@ -364,6 +364,13 @@ export const EnvironmentConfigSchema = z.object({
364 validateNoCapitalizedCalls: z.nullable(z.array(z.string())).default(null),
365 validateBlocklistedImports: z.nullable(z.array(z.string())).default(null),
366
367 + /**
368 + * Validates that AST nodes generated during codegen have proper source locations.
369 + * This is useful for debugging issues with source maps and Istanbul coverage.
370 + * When enabled, the compiler will error if important source locations are missing in the generated AST.
371 + */
372 + validateSourceLocations: z.boolean().default(false),
373 +
374 /**
375 * Validate against impure functions called during render
376 */
compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateSourceLocations.ts new
+206
@@ -0,0 +1,206 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + */
7 +
8 +import {NodePath} from '@babel/traverse';
9 +import * as t from '@babel/types';
10 +import {CompilerDiagnostic, CompilerError, ErrorCategory} from '..';
11 +import {CodegenFunction} from '../ReactiveScopes';
12 +import {Result} from '../Utils/Result';
13 +
14 +/**
15 + * IMPORTANT: This validation is only intended for use in unit tests.
16 + * It is not intended for use in production.
17 + *
18 + * This validation is used to ensure that the generated AST has proper source locations
19 + * for "important" original nodes.
20 + *
21 + * There's one big gotcha with this validation: it only works if the "important" original nodes
22 + * are not optimized away by the compiler.
23 + *
24 + * When that scenario happens, we should just update the fixture to not include a node that has no
25 + * corresponding node in the generated AST due to being completely removed during compilation.
26 + */
27 +
28 +/**
29 + * Some common node types that are important for coverage tracking.
30 + * Based on istanbul-lib-instrument
31 + */
32 +const IMPORTANT_INSTRUMENTED_TYPES = new Set([
33 + 'ArrowFunctionExpression',
34 + 'AssignmentPattern',
35 + 'ObjectMethod',
36 + 'ExpressionStatement',
37 + 'BreakStatement',
38 + 'ContinueStatement',
39 + 'ReturnStatement',
40 + 'ThrowStatement',
41 + 'TryStatement',
42 + 'VariableDeclarator',
43 + 'IfStatement',
44 + 'ForStatement',
45 + 'ForInStatement',
46 + 'ForOfStatement',
47 + 'WhileStatement',
48 + 'DoWhileStatement',
49 + 'SwitchStatement',
50 + 'SwitchCase',
51 + 'WithStatement',
52 + 'FunctionDeclaration',
53 + 'FunctionExpression',
54 + 'LabeledStatement',
55 + 'ConditionalExpression',
56 + 'LogicalExpression',
57 +]);
58 +
59 +/**
60 + * Check if a node is a manual memoization call that the compiler optimizes away.
61 + * These include useMemo and useCallback calls, which are intentionally removed
62 + * by the DropManualMemoization pass.
63 + */
64 +function isManualMemoization(node: t.Node): boolean {
65 + // Check if this is a useMemo/useCallback call expression
66 + if (t.isCallExpression(node)) {
67 + const callee = node.callee;
68 + if (t.isIdentifier(callee)) {
69 + return callee.name === 'useMemo' || callee.name === 'useCallback';
70 + }
71 + if (
72 + t.isMemberExpression(callee) &&
73 + t.isIdentifier(callee.property) &&
74 + t.isIdentifier(callee.object)
75 + ) {
76 + return (
77 + callee.object.name === 'React' &&
78 + (callee.property.name === 'useMemo' ||
79 + callee.property.name === 'useCallback')
80 + );
81 + }
82 + }
83 +
84 + return false;
85 +}
86 +
87 +/**
88 + * Create a location key for comparison. We compare by line/column/source,
89 + * not by object identity.
90 + */
91 +function locationKey(loc: t.SourceLocation): string {
92 + return `${loc.start.line}:${loc.start.column}-${loc.end.line}:${loc.end.column}`;
93 +}
94 +
95 +/**
96 + * Validates that important source locations from the original code are preserved
97 + * in the generated AST. This ensures that Istanbul coverage instrumentation can
98 + * properly map back to the original source code.
99 + *
100 + * The validator:
101 + * 1. Collects locations from "important" nodes in the original AST (those that
102 + * Istanbul instruments for coverage tracking)
103 + * 2. Exempts known compiler optimizations (useMemo/useCallback removal)
104 + * 3. Verifies that all important locations appear somewhere in the generated AST
105 + *
106 + * Missing locations can cause Istanbul to fail to track coverage for certain
107 + * code paths, leading to inaccurate coverage reports.
108 + */
109 +export function validateSourceLocations(
110 + func: NodePath<
111 + t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
112 + >,
113 + generatedAst: CodegenFunction,
114 +): Result<void, CompilerError> {
115 + const errors = new CompilerError();
116 +
117 + // Step 1: Collect important locations from the original source
118 + const importantOriginalLocations = new Map<
119 + string,
120 + {loc: t.SourceLocation; nodeType: string}
121 + >();
122 +
123 + func.traverse({
124 + enter(path) {
125 + const node = path.node;
126 +
127 + // Only track node types that Istanbul instruments
128 + if (!IMPORTANT_INSTRUMENTED_TYPES.has(node.type)) {
129 + return;
130 + }
131 +
132 + // Skip manual memoization that the compiler intentionally removes
133 + if (isManualMemoization(node)) {
134 + return;
135 + }
136 +
137 + // Collect the location if it exists
138 + if (node.loc) {
139 + const key = locationKey(node.loc);
140 + importantOriginalLocations.set(key, {
141 + loc: node.loc,
142 + nodeType: node.type,
143 + });
144 + }
145 + },
146 + });
147 +
148 + // Step 2: Collect all locations from the generated AST
149 + const generatedLocations = new Set<string>();
150 +
151 + function collectGeneratedLocations(node: t.Node): void {
152 + if (node.loc) {
153 + generatedLocations.add(locationKey(node.loc));
154 + }
155 +
156 + // Use Babel's VISITOR_KEYS to traverse only actual node properties
157 + const keys = t.VISITOR_KEYS[node.type as keyof typeof t.VISITOR_KEYS];
158 +
159 + if (!keys) {
160 + return;
161 + }
162 +
163 + for (const key of keys) {
164 + const value = (node as any)[key];
165 +
166 + if (Array.isArray(value)) {
167 + for (const item of value) {
168 + if (t.isNode(item)) {
169 + collectGeneratedLocations(item);
170 + }
171 + }
172 + } else if (t.isNode(value)) {
173 + collectGeneratedLocations(value);
174 + }
175 + }
176 + }
177 +
178 + // Collect from main function body
179 + collectGeneratedLocations(generatedAst.body);
180 +
181 + // Collect from outlined functions
182 + for (const outlined of generatedAst.outlined) {
183 + collectGeneratedLocations(outlined.fn.body);
184 + }
185 +
186 + // Step 3: Validate that all important locations are preserved
187 + for (const [key, {loc, nodeType}] of importantOriginalLocations) {
188 + if (!generatedLocations.has(key)) {
189 + errors.pushDiagnostic(
190 + CompilerDiagnostic.create({
191 + category: ErrorCategory.Todo,
192 + reason: 'Important source location missing in generated code',
193 + description:
194 + `Source location for ${nodeType} is missing in the generated output. This can cause coverage instrumentation ` +
195 + `to fail to track this code properly, resulting in inaccurate coverage reports.`,
196 + }).withDetails({
197 + kind: 'error',
198 + loc,
199 + message: null,
200 + }),
201 + );
202 + }
203 + }
204 +
205 + return errors.asResult();
206 +}
compiler/packages/babel-plugin-react-compiler/src/Validation/index.ts
+1
@@ -12,4 +12,5 @@ export {validateNoCapitalizedCalls} from './ValidateNoCapitalizedCalls';
12 export {validateNoRefAccessInRender} from './ValidateNoRefAccessInRender';
13 export {validateNoSetStateInRender} from './ValidateNoSetStateInRender';
14 export {validatePreservedManualMemoization} from './ValidatePreservedManualMemoization';
15 +export {validateSourceLocations} from './ValidateSourceLocations';
16 export {validateUseMemo} from './ValidateUseMemo';
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-missing-source-locations.expect.md new
+224
@@ -0,0 +1,224 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validateSourceLocations
6 +import {useEffect, useCallback} from 'react';
7 +
8 +function Component({prop1, prop2}) {
9 + const x = prop1 + prop2;
10 + const y = x * 2;
11 + const arr = [x, y];
12 + const obj = {x, y};
13 + const [a, b] = arr;
14 + const {x: c, y: d} = obj;
15 +
16 + useEffect(() => {
17 + if (a > 10) {
18 + console.log(a);
19 + }
20 + }, [a]);
21 +
22 + const foo = useCallback(() => {
23 + return a + b;
24 + }, [a, b]);
25 +
26 + function bar() {
27 + return (c + d) * 2;
28 + }
29 +
30 + console.log('Hello, world!');
31 +
32 + return [y, foo, bar];
33 +}
34 +
35 +```
36 +
37 +
38 +## Error
39 +
40 +```
41 +Found 13 errors:
42 +
43 +Todo: Important source location missing in generated code
44 +
45 +Source location for VariableDeclarator is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
46 +
47 +error.todo-missing-source-locations.ts:5:8
48 + 3 |
49 + 4 | function Component({prop1, prop2}) {
50 +> 5 | const x = prop1 + prop2;
51 + | ^^^^^^^^^^^^^^^^^
52 + 6 | const y = x * 2;
53 + 7 | const arr = [x, y];
54 + 8 | const obj = {x, y};
55 +
56 +Todo: Important source location missing in generated code
57 +
58 +Source location for VariableDeclarator is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
59 +
60 +error.todo-missing-source-locations.ts:6:8
61 + 4 | function Component({prop1, prop2}) {
62 + 5 | const x = prop1 + prop2;
63 +> 6 | const y = x * 2;
64 + | ^^^^^^^^^
65 + 7 | const arr = [x, y];
66 + 8 | const obj = {x, y};
67 + 9 | const [a, b] = arr;
68 +
69 +Todo: Important source location missing in generated code
70 +
71 +Source location for VariableDeclarator is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
72 +
73 +error.todo-missing-source-locations.ts:7:8
74 + 5 | const x = prop1 + prop2;
75 + 6 | const y = x * 2;
76 +> 7 | const arr = [x, y];
77 + | ^^^^^^^^^^^^
78 + 8 | const obj = {x, y};
79 + 9 | const [a, b] = arr;
80 + 10 | const {x: c, y: d} = obj;
81 +
82 +Todo: Important source location missing in generated code
83 +
84 +Source location for VariableDeclarator is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
85 +
86 +error.todo-missing-source-locations.ts:8:8
87 + 6 | const y = x * 2;
88 + 7 | const arr = [x, y];
89 +> 8 | const obj = {x, y};
90 + | ^^^^^^^^^^^^
91 + 9 | const [a, b] = arr;
92 + 10 | const {x: c, y: d} = obj;
93 + 11 |
94 +
95 +Todo: Important source location missing in generated code
96 +
97 +Source location for VariableDeclarator is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
98 +
99 +error.todo-missing-source-locations.ts:9:8
100 + 7 | const arr = [x, y];
101 + 8 | const obj = {x, y};
102 +> 9 | const [a, b] = arr;
103 + | ^^^^^^^^^^^^
104 + 10 | const {x: c, y: d} = obj;
105 + 11 |
106 + 12 | useEffect(() => {
107 +
108 +Todo: Important source location missing in generated code
109 +
110 +Source location for VariableDeclarator is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
111 +
112 +error.todo-missing-source-locations.ts:10:8
113 + 8 | const obj = {x, y};
114 + 9 | const [a, b] = arr;
115 +> 10 | const {x: c, y: d} = obj;
116 + | ^^^^^^^^^^^^^^^^^^
117 + 11 |
118 + 12 | useEffect(() => {
119 + 13 | if (a > 10) {
120 +
121 +Todo: Important source location missing in generated code
122 +
123 +Source location for ExpressionStatement is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
124 +
125 +error.todo-missing-source-locations.ts:12:2
126 + 10 | const {x: c, y: d} = obj;
127 + 11 |
128 +> 12 | useEffect(() => {
129 + | ^^^^^^^^^^^^^^^^^
130 +> 13 | if (a > 10) {
131 + | ^^^^^^^^^^^^^^^^^
132 +> 14 | console.log(a);
133 + | ^^^^^^^^^^^^^^^^^
134 +> 15 | }
135 + | ^^^^^^^^^^^^^^^^^
136 +> 16 | }, [a]);
137 + | ^^^^^^^^^^^
138 + 17 |
139 + 18 | const foo = useCallback(() => {
140 + 19 | return a + b;
141 +
142 +Todo: Important source location missing in generated code
143 +
144 +Source location for ExpressionStatement is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
145 +
146 +error.todo-missing-source-locations.ts:14:6
147 + 12 | useEffect(() => {
148 + 13 | if (a > 10) {
149 +> 14 | console.log(a);
150 + | ^^^^^^^^^^^^^^^
151 + 15 | }
152 + 16 | }, [a]);
153 + 17 |
154 +
155 +Todo: Important source location missing in generated code
156 +
157 +Source location for VariableDeclarator is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
158 +
159 +error.todo-missing-source-locations.ts:18:8
160 + 16 | }, [a]);
161 + 17 |
162 +> 18 | const foo = useCallback(() => {
163 + | ^^^^^^^^^^^^^^^^^^^^^^^^^
164 +> 19 | return a + b;
165 + | ^^^^^^^^^^^^^^^^^
166 +> 20 | }, [a, b]);
167 + | ^^^^^^^^^^^^^
168 + 21 |
169 + 22 | function bar() {
170 + 23 | return (c + d) * 2;
171 +
172 +Todo: Important source location missing in generated code
173 +
174 +Source location for ReturnStatement is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
175 +
176 +error.todo-missing-source-locations.ts:19:4
177 + 17 |
178 + 18 | const foo = useCallback(() => {
179 +> 19 | return a + b;
180 + | ^^^^^^^^^^^^^
181 + 20 | }, [a, b]);
182 + 21 |
183 + 22 | function bar() {
184 +
185 +Todo: Important source location missing in generated code
186 +
187 +Source location for ReturnStatement is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
188 +
189 +error.todo-missing-source-locations.ts:23:4
190 + 21 |
191 + 22 | function bar() {
192 +> 23 | return (c + d) * 2;
193 + | ^^^^^^^^^^^^^^^^^^^
194 + 24 | }
195 + 25 |
196 + 26 | console.log('Hello, world!');
197 +
198 +Todo: Important source location missing in generated code
199 +
200 +Source location for ExpressionStatement is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
201 +
202 +error.todo-missing-source-locations.ts:26:2
203 + 24 | }
204 + 25 |
205 +> 26 | console.log('Hello, world!');
206 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
207 + 27 |
208 + 28 | return [y, foo, bar];
209 + 29 | }
210 +
211 +Todo: Important source location missing in generated code
212 +
213 +Source location for ReturnStatement is missing in the generated output. This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports..
214 +
215 +error.todo-missing-source-locations.ts:28:2
216 + 26 | console.log('Hello, world!');
217 + 27 |
218 +> 28 | return [y, foo, bar];
219 + | ^^^^^^^^^^^^^^^^^^^^^
220 + 29 | }
221 + 30 |
222 +```
223 +
224 +
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-missing-source-locations.js new
+29
@@ -0,0 +1,29 @@
1 +// @validateSourceLocations
2 +import {useEffect, useCallback} from 'react';
3 +
4 +function Component({prop1, prop2}) {
5 + const x = prop1 + prop2;
6 + const y = x * 2;
7 + const arr = [x, y];
8 + const obj = {x, y};
9 + const [a, b] = arr;
10 + const {x: c, y: d} = obj;
11 +
12 + useEffect(() => {
13 + if (a > 10) {
14 + console.log(a);
15 + }
16 + }, [a]);
17 +
18 + const foo = useCallback(() => {
19 + return a + b;
20 + }, [a, b]);
21 +
22 + function bar() {
23 + return (c + d) * 2;
24 + }
25 +
26 + console.log('Hello, world!');
27 +
28 + return [y, foo, bar];
29 +}