Skip to content

Commit ae3a105

Browse files
committed
add compiler source location validator
1 parent 1324e1b commit ae3a105

File tree

6 files changed

+467
-0
lines changed

6 files changed

+467
-0
lines changed

compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ import {inferMutationAliasingEffects} from '../Inference/InferMutationAliasingEf
104104
import {inferMutationAliasingRanges} from '../Inference/InferMutationAliasingRanges';
105105
import {validateNoDerivedComputationsInEffects} from '../Validation/ValidateNoDerivedComputationsInEffects';
106106
import {nameAnonymousFunctions} from '../Transform/NameAnonymousFunctions';
107+
import {validateSourceLocations} from '../Validation/ValidateSourceLocations';
107108

108109
export type CompilerPipelineValue =
109110
| {kind: 'ast'; name: string; value: CodegenFunction}
@@ -554,6 +555,10 @@ function runWithEnvironment(
554555
log({kind: 'ast', name: 'Codegen (outlined)', value: outlined.fn});
555556
}
556557

558+
if (env.config.validateSourceLocations) {
559+
validateSourceLocations(func, ast).unwrap();
560+
}
561+
557562
/**
558563
* This flag should be only set for unit / fixture tests to check
559564
* that Forget correctly handles unexpected errors (e.g. exceptions

compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,13 @@ export const EnvironmentConfigSchema = z.object({
368368
validateNoCapitalizedCalls: z.nullable(z.array(z.string())).default(null),
369369
validateBlocklistedImports: z.nullable(z.array(z.string())).default(null),
370370

371+
/**
372+
* Validates that AST nodes generated during codegen have proper source locations.
373+
* This is useful for debugging issues with source maps and Istanbul coverage.
374+
* When enabled, the compiler will error if important source locations are missing in the generated AST.
375+
*/
376+
validateSourceLocations: z.boolean().default(false),
377+
371378
/**
372379
* Validate against impure functions called during render
373380
*/
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
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 (t.isMemberExpression(callee) && t.isIdentifier(callee.property)) {
72+
return (
73+
callee.property.name === 'useMemo' ||
74+
callee.property.name === 'useCallback'
75+
);
76+
}
77+
}
78+
79+
return false;
80+
}
81+
82+
/**
83+
* Create a location key for comparison. We compare by line/column/source,
84+
* not by object identity.
85+
*/
86+
function locationKey(loc: t.SourceLocation): string {
87+
return `${loc.start.line}:${loc.start.column}-${loc.end.line}:${loc.end.column}`;
88+
}
89+
90+
/**
91+
* Validates that important source locations from the original code are preserved
92+
* in the generated AST. This ensures that Istanbul coverage instrumentation can
93+
* properly map back to the original source code.
94+
*
95+
* The validator:
96+
* 1. Collects locations from "important" nodes in the original AST (those that
97+
* Istanbul instruments for coverage tracking)
98+
* 2. Exempts known compiler optimizations (useMemo/useCallback removal)
99+
* 3. Verifies that all important locations appear somewhere in the generated AST
100+
*
101+
* Missing locations can cause Istanbul to fail to track coverage for certain
102+
* code paths, leading to inaccurate coverage reports.
103+
*/
104+
export function validateSourceLocations(
105+
func: NodePath<
106+
t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
107+
>,
108+
generatedAst: CodegenFunction,
109+
): Result<void, CompilerError> {
110+
const errors = new CompilerError();
111+
112+
// Step 1: Collect important locations from the original source
113+
const importantOriginalLocations = new Map<
114+
string,
115+
{loc: t.SourceLocation; nodeType: string}
116+
>();
117+
118+
func.traverse(
119+
{
120+
enter(path) {
121+
const node = path.node;
122+
123+
// Only track node types that Istanbul instruments
124+
if (!IMPORTANT_INSTRUMENTED_TYPES.has(node.type)) {
125+
return;
126+
}
127+
128+
// Skip manual memoization that the compiler intentionally removes
129+
if (isManualMemoization(node)) {
130+
return;
131+
}
132+
133+
// Collect the location if it exists
134+
if (node.loc) {
135+
const key = locationKey(node.loc);
136+
importantOriginalLocations.set(key, {
137+
loc: node.loc,
138+
nodeType: node.type,
139+
});
140+
}
141+
},
142+
},
143+
);
144+
145+
// Step 2: Collect all locations from the generated AST
146+
const generatedLocations = new Set<string>();
147+
148+
function collectGeneratedLocations(node: t.Node): void {
149+
if (node.loc) {
150+
generatedLocations.add(locationKey(node.loc));
151+
}
152+
153+
// Use Babel's VISITOR_KEYS to traverse only actual node properties
154+
const keys = t.VISITOR_KEYS[node.type as keyof typeof t.VISITOR_KEYS];
155+
if (!keys) return;
156+
157+
for (const key of keys) {
158+
const value = (node as any)[key];
159+
160+
if (Array.isArray(value)) {
161+
for (const item of value) {
162+
if (t.isNode(item)) {
163+
collectGeneratedLocations(item);
164+
}
165+
}
166+
} else if (t.isNode(value)) {
167+
collectGeneratedLocations(value);
168+
}
169+
}
170+
}
171+
172+
// Collect from main function body
173+
collectGeneratedLocations(generatedAst.body);
174+
175+
// Collect from outlined functions
176+
for (const outlined of generatedAst.outlined) {
177+
collectGeneratedLocations(outlined.fn.body);
178+
}
179+
180+
// Step 3: Validate that all important locations are preserved
181+
for (const [key, {loc, nodeType}] of importantOriginalLocations) {
182+
if (!generatedLocations.has(key)) {
183+
errors.pushDiagnostic(
184+
CompilerDiagnostic.create({
185+
category: ErrorCategory.Todo,
186+
reason:
187+
'Important source location missing in generated code',
188+
description:
189+
`Source location for ${nodeType} is missing in the generated output. This can cause coverage instrumentation ` +
190+
`to fail to track this code properly, resulting in inaccurate coverage reports.`,
191+
}).withDetails({
192+
kind: 'error',
193+
loc,
194+
message: null,
195+
})
196+
);
197+
}
198+
}
199+
200+
return errors.asResult();
201+
}

compiler/packages/babel-plugin-react-compiler/src/Validation/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,5 @@ export {validateNoCapitalizedCalls} from './ValidateNoCapitalizedCalls';
1212
export {validateNoRefAccessInRender} from './ValidateNoRefAccessInRender';
1313
export {validateNoSetStateInRender} from './ValidateNoSetStateInRender';
1414
export {validatePreservedManualMemoization} from './ValidatePreservedManualMemoization';
15+
export {validateSourceLocations} from './ValidateSourceLocations';
1516
export {validateUseMemo} from './ValidateUseMemo';

0 commit comments

Comments
 (0)