main
ts 310 lines 9.71 KB
Raw
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, ErrorCategory} from '..';
11 import {CodegenFunction} from '../ReactiveScopes';
12 import {Environment} from '../HIR/Environment';
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 + some other common nodes we expect to be present in the generated AST.
31 *
32 * Note: For VariableDeclaration, VariableDeclarator, and Identifier, we enforce stricter validation
33 * that requires both the source location AND node type to match in the generated AST. This ensures
34 * that variable declarations maintain their structural integrity through compilation.
35 */
36 const IMPORTANT_INSTRUMENTED_TYPES = new Set([
37 'ArrowFunctionExpression',
38 'AssignmentPattern',
39 'ObjectMethod',
40 'ExpressionStatement',
41 'BreakStatement',
42 'ContinueStatement',
43 'ReturnStatement',
44 'ThrowStatement',
45 'TryStatement',
46 'VariableDeclarator',
47 'IfStatement',
48 'ForStatement',
49 'ForInStatement',
50 'ForOfStatement',
51 'WhileStatement',
52 'DoWhileStatement',
53 'SwitchStatement',
54 'SwitchCase',
55 'WithStatement',
56 'FunctionDeclaration',
57 'FunctionExpression',
58 'LabeledStatement',
59 'ConditionalExpression',
60 'LogicalExpression',
61
62 /**
63 * Note: these aren't important for coverage tracking,
64 * but we still want to track them to ensure we aren't regressing them when
65 * we fix the source location tracking for other nodes.
66 */
67 'VariableDeclaration',
68 'Identifier',
69 ]);
70
71 /**
72 * Check if a node is a manual memoization call that the compiler optimizes away.
73 * These include useMemo and useCallback calls, which are intentionally removed
74 * by the DropManualMemoization pass.
75 */
76 function isManualMemoization(node: t.Node): boolean {
77 // Check if this is a useMemo/useCallback call expression
78 if (t.isCallExpression(node)) {
79 const callee = node.callee;
80 if (t.isIdentifier(callee)) {
81 return callee.name === 'useMemo' || callee.name === 'useCallback';
82 }
83 if (
84 t.isMemberExpression(callee) &&
85 t.isIdentifier(callee.property) &&
86 t.isIdentifier(callee.object)
87 ) {
88 return (
89 callee.object.name === 'React' &&
90 (callee.property.name === 'useMemo' ||
91 callee.property.name === 'useCallback')
92 );
93 }
94 }
95
96 return false;
97 }
98
99 /**
100 * Create a location key for comparison. We compare by line/column/source,
101 * not by object identity.
102 */
103 function locationKey(loc: t.SourceLocation): string {
104 return `${loc.start.line}:${loc.start.column}-${loc.end.line}:${loc.end.column}`;
105 }
106
107 /**
108 * Validates that important source locations from the original code are preserved
109 * in the generated AST. This ensures that Istanbul coverage instrumentation can
110 * properly map back to the original source code.
111 *
112 * The validator:
113 * 1. Collects locations from "important" nodes in the original AST (those that
114 * Istanbul instruments for coverage tracking)
115 * 2. Exempts known compiler optimizations (useMemo/useCallback removal)
116 * 3. Verifies that all important locations appear somewhere in the generated AST
117 *
118 * Missing locations can cause Istanbul to fail to track coverage for certain
119 * code paths, leading to inaccurate coverage reports.
120 */
121 export function validateSourceLocations(
122 func: NodePath<
123 t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
124 >,
125 generatedAst: CodegenFunction,
126 env: Environment,
127 ): void {
128 /*
129 * Step 1: Collect important locations from the original source
130 * Note: Multiple node types can share the same location (e.g. VariableDeclarator and Identifier)
131 */
132 const importantOriginalLocations = new Map<
133 string,
134 {loc: t.SourceLocation; nodeTypes: Set<string>}
135 >();
136
137 func.traverse({
138 enter(path) {
139 const node = path.node;
140
141 // Only track node types that Istanbul instruments
142 if (!IMPORTANT_INSTRUMENTED_TYPES.has(node.type)) {
143 return;
144 }
145
146 // Skip manual memoization that the compiler intentionally removes
147 if (isManualMemoization(node)) {
148 return;
149 }
150
151 /*
152 * Skip return statements inside arrow functions that will be simplified to expression body.
153 * The compiler transforms `() => { return expr }` to `() => expr` in CodegenReactiveFunction
154 */
155 if (t.isReturnStatement(node) && node.argument != null) {
156 const parentBody = path.parentPath;
157 const parentFunc = parentBody?.parentPath;
158 if (
159 parentBody?.isBlockStatement() &&
160 parentFunc?.isArrowFunctionExpression() &&
161 parentBody.node.body.length === 1 &&
162 parentBody.node.directives.length === 0
163 ) {
164 return;
165 }
166 }
167
168 // Collect the location if it exists
169 if (node.loc) {
170 const key = locationKey(node.loc);
171 const existing = importantOriginalLocations.get(key);
172 if (existing) {
173 existing.nodeTypes.add(node.type);
174 } else {
175 importantOriginalLocations.set(key, {
176 loc: node.loc,
177 nodeTypes: new Set([node.type]),
178 });
179 }
180 }
181 },
182 });
183
184 // Step 2: Collect all locations from the generated AST with their node types
185 const generatedLocations = new Map<string, Set<string>>();
186
187 function collectGeneratedLocations(node: t.Node): void {
188 if (node.loc) {
189 const key = locationKey(node.loc);
190 const nodeTypes = generatedLocations.get(key);
191 if (nodeTypes) {
192 nodeTypes.add(node.type);
193 } else {
194 generatedLocations.set(key, new Set([node.type]));
195 }
196 }
197
198 // Use Babel's VISITOR_KEYS to traverse only actual node properties
199 const keys = t.VISITOR_KEYS[node.type as keyof typeof t.VISITOR_KEYS];
200
201 if (!keys) {
202 return;
203 }
204
205 for (const key of keys) {
206 const value = (node as any)[key];
207
208 if (Array.isArray(value)) {
209 for (const item of value) {
210 if (t.isNode(item)) {
211 collectGeneratedLocations(item);
212 }
213 }
214 } else if (t.isNode(value)) {
215 collectGeneratedLocations(value);
216 }
217 }
218 }
219
220 // Collect from main function body
221 collectGeneratedLocations(generatedAst.body);
222
223 // Collect from outlined functions
224 for (const outlined of generatedAst.outlined) {
225 collectGeneratedLocations(outlined.fn.body);
226 }
227
228 /*
229 * Step 3: Validate that all important locations are preserved
230 * For certain node types, also validate that the node type matches
231 */
232 const strictNodeTypes = new Set([
233 'VariableDeclaration',
234 'VariableDeclarator',
235 'Identifier',
236 ]);
237
238 const reportMissingLocation = (
239 loc: t.SourceLocation,
240 nodeType: string,
241 ): void => {
242 env.recordError(
243 CompilerDiagnostic.create({
244 category: ErrorCategory.Todo,
245 reason: 'Important source location missing in generated code',
246 description:
247 `Source location for ${nodeType} is missing in the generated output. This can cause coverage instrumentation ` +
248 `to fail to track this code properly, resulting in inaccurate coverage reports.`,
249 }).withDetails({
250 kind: 'error',
251 loc,
252 message: null,
253 }),
254 );
255 };
256
257 const reportWrongNodeType = (
258 loc: t.SourceLocation,
259 expectedType: string,
260 actualTypes: Set<string>,
261 ): void => {
262 env.recordError(
263 CompilerDiagnostic.create({
264 category: ErrorCategory.Todo,
265 reason:
266 'Important source location has wrong node type in generated code',
267 description:
268 `Source location for ${expectedType} exists in the generated output but with wrong node type(s): ${Array.from(actualTypes).join(', ')}. ` +
269 `This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports.`,
270 }).withDetails({
271 kind: 'error',
272 loc,
273 message: null,
274 }),
275 );
276 };
277
278 for (const [key, {loc, nodeTypes}] of importantOriginalLocations) {
279 const generatedNodeTypes = generatedLocations.get(key);
280
281 if (!generatedNodeTypes) {
282 // Location is completely missing
283 reportMissingLocation(loc, Array.from(nodeTypes).join(', '));
284 } else {
285 // Location exists, check each node type
286 for (const nodeType of nodeTypes) {
287 if (
288 strictNodeTypes.has(nodeType) &&
289 !generatedNodeTypes.has(nodeType)
290 ) {
291 /*
292 * For strict node types, the specific node type must be present
293 * Check if any generated node type is also an important original node type
294 */
295 const hasValidNodeType = Array.from(generatedNodeTypes).some(
296 genType => nodeTypes.has(genType),
297 );
298
299 if (hasValidNodeType) {
300 // At least one generated node type is valid (also in original), so this is just missing
301 reportMissingLocation(loc, nodeType);
302 } else {
303 // None of the generated node types are in original - this is wrong node type
304 reportWrongNodeType(loc, nodeType, generatedNodeTypes);
305 }
306 }
307 }
308 }
309 }
310 }