Skip to content

Commit 91078ec

Browse files
committed
validate source locations
1 parent 1324e1b commit 91078ec

File tree

6 files changed

+438
-0
lines changed

6 files changed

+438
-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
@@ -86,6 +86,7 @@ import {
8686
validateNoRefAccessInRender,
8787
validateNoSetStateInRender,
8888
validatePreservedManualMemoization,
89+
validateSourceLocations,
8990
validateUseMemo,
9091
} from '../Validation';
9192
import {validateLocalsNotReassignedAfterRender} from '../Validation/ValidateLocalsNotReassignedAfterRender';
@@ -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.node, 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
@@ -378,6 +378,13 @@ export const EnvironmentConfigSchema = z.object({
378378
*/
379379
validateNoFreezingKnownMutableFunctions: z.boolean().default(false),
380380

381+
/**
382+
* Validates that all AST nodes generated during codegen have proper source locations.
383+
* This is useful for debugging issues with source maps and Istanbul coverage.
384+
* When enabled, the compiler will error if any AST node is missing a location.
385+
*/
386+
validateSourceLocations: z.boolean().default(false),
387+
381388
/*
382389
* When enabled, the compiler assumes that hooks follow the Rules of React:
383390
* - Hooks may memoize computation based on any of their parameters, thus
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
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 traverse 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+
* Some common node types that are important for coverage tracking.
16+
* Based on istanbul-lib-instrument's visitor configuration.
17+
*/
18+
const IMPORTANT_INSTRUMENTED_TYPES = new Set([
19+
'ArrowFunctionExpression',
20+
'AssignmentPattern',
21+
// 'BlockStatement',
22+
// 'ExportDefaultDeclaration',
23+
// 'ExportNamedDeclaration',
24+
// 'ClassMethod',
25+
// 'ClassDeclaration',
26+
// 'ClassProperty',
27+
// 'ClassPrivateProperty',
28+
'ObjectMethod',
29+
'ExpressionStatement',
30+
'BreakStatement',
31+
'ContinueStatement',
32+
// 'DebuggerStatement',
33+
'ReturnStatement',
34+
'ThrowStatement',
35+
'TryStatement',
36+
// 'VariableDeclaration',
37+
'VariableDeclarator',
38+
'IfStatement',
39+
'ForStatement',
40+
'ForInStatement',
41+
'ForOfStatement',
42+
'WhileStatement',
43+
'DoWhileStatement',
44+
'SwitchStatement',
45+
'SwitchCase',
46+
'WithStatement',
47+
'FunctionDeclaration',
48+
'FunctionExpression',
49+
'LabeledStatement',
50+
'ConditionalExpression',
51+
'LogicalExpression',
52+
]);
53+
54+
/**
55+
* Check if a node is a manual memoization call that the compiler optimizes away.
56+
* These include useMemo and useCallback calls, which are intentionally removed
57+
* by the DropManualMemoization pass.
58+
*/
59+
function isManualMemoization(node: t.Node): boolean {
60+
// Check if this is a useMemo/useCallback call expression
61+
if (t.isCallExpression(node)) {
62+
const callee = node.callee;
63+
if (t.isIdentifier(callee)) {
64+
return callee.name === 'useMemo' || callee.name === 'useCallback';
65+
}
66+
if (t.isMemberExpression(callee) && t.isIdentifier(callee.property)) {
67+
return (
68+
callee.property.name === 'useMemo' ||
69+
callee.property.name === 'useCallback'
70+
);
71+
}
72+
}
73+
74+
return false;
75+
}
76+
77+
/**
78+
* Create a location key for comparison. We compare by line/column/source,
79+
* not by object identity.
80+
*/
81+
function locationKey(loc: t.SourceLocation): string {
82+
return `${loc.start.line}:${loc.start.column}-${loc.end.line}:${loc.end.column}`;
83+
}
84+
85+
/**
86+
* Validates that important source locations from the original code are preserved
87+
* in the generated AST. This ensures that Istanbul coverage instrumentation can
88+
* properly map back to the original source code.
89+
*
90+
* The validator:
91+
* 1. Collects locations from "important" nodes in the original AST (those that
92+
* Istanbul instruments for coverage tracking)
93+
* 2. Exempts known compiler optimizations (useMemo/useCallback removal)
94+
* 3. Verifies that all important locations appear somewhere in the generated AST
95+
*
96+
* Missing locations can cause Istanbul to fail to track coverage for certain
97+
* code paths, leading to inaccurate coverage reports.
98+
*/
99+
export function validateSourceLocations(
100+
originalAst: t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression,
101+
generatedAst: CodegenFunction,
102+
): Result<void, CompilerError> {
103+
const errors = new CompilerError();
104+
105+
// Step 1: Collect important locations from the original source
106+
const importantOriginalLocations = new Map<
107+
string,
108+
{loc: t.SourceLocation; nodeType: string}
109+
>();
110+
111+
traverse(
112+
t.file(t.program([t.isExpression(originalAst) ? t.expressionStatement(originalAst) : originalAst])),
113+
{
114+
enter(path) {
115+
const node = path.node;
116+
117+
// Only track node types that Istanbul instruments
118+
if (!IMPORTANT_INSTRUMENTED_TYPES.has(node.type)) {
119+
return;
120+
}
121+
122+
// Skip manual memoization that the compiler intentionally removes
123+
if (isManualMemoization(node)) {
124+
return;
125+
}
126+
127+
// Collect the location if it exists
128+
if (node.loc) {
129+
const key = locationKey(node.loc);
130+
importantOriginalLocations.set(key, {
131+
loc: node.loc,
132+
nodeType: node.type,
133+
});
134+
}
135+
},
136+
},
137+
);
138+
139+
// Step 2: Collect all locations from the generated AST
140+
const generatedLocations = new Set<string>();
141+
142+
function collectGeneratedLocations(node: t.Node): void {
143+
if (node.loc) {
144+
generatedLocations.add(locationKey(node.loc));
145+
}
146+
147+
// Use Babel's VISITOR_KEYS to traverse only actual node properties
148+
const keys = t.VISITOR_KEYS[node.type as keyof typeof t.VISITOR_KEYS];
149+
if (!keys) return;
150+
151+
for (const key of keys) {
152+
const value = (node as any)[key];
153+
154+
if (Array.isArray(value)) {
155+
for (const item of value) {
156+
if (t.isNode(item)) {
157+
collectGeneratedLocations(item);
158+
}
159+
}
160+
} else if (t.isNode(value)) {
161+
collectGeneratedLocations(value);
162+
}
163+
}
164+
}
165+
166+
// Collect from main function body
167+
collectGeneratedLocations(generatedAst.body);
168+
169+
// Collect from outlined functions
170+
for (const outlined of generatedAst.outlined) {
171+
collectGeneratedLocations(outlined.fn.body);
172+
}
173+
174+
// Step 3: Validate that all important locations are preserved
175+
for (const [key, {loc, nodeType}] of importantOriginalLocations) {
176+
if (!generatedLocations.has(key)) {
177+
errors.pushDiagnostic(
178+
CompilerDiagnostic.create({
179+
category: ErrorCategory.Todo,
180+
reason:
181+
'Important source location missing in generated code',
182+
description:
183+
`Source location for ${nodeType} is missing in the generated output. This can cause coverage instrumentation ` +
184+
`to fail to track this code properly, resulting in inaccurate coverage reports.`,
185+
}).withDetails({
186+
kind: 'error',
187+
loc,
188+
message: null,
189+
})
190+
);
191+
}
192+
}
193+
194+
return errors.asResult();
195+
}

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)