compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateSourceLocations.ts TYPESCRIPT 311 lines View on github.com → Search inside
1/**2 * Copyright (c) Meta Platforms, Inc. and affiliates.3 *4 * This source code is licensed under the MIT license found in the5 * LICENSE file in the root directory of this source tree.6 */78import {NodePath} from '@babel/traverse';9import * as t from '@babel/types';10import {CompilerDiagnostic, ErrorCategory} from '..';11import {CodegenFunction} from '../ReactiveScopes';12import {Environment} from '../HIR/Environment';1314/**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 locations19 * for "important" original nodes.20 *21 * There's one big gotcha with this validation: it only works if the "important" original nodes22 * 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 no25 * corresponding node in the generated AST due to being completely removed during compilation.26 */2728/**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 validation33 * that requires both the source location AND node type to match in the generated AST. This ensures34 * that variable declarations maintain their structural integrity through compilation.35 */36const 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',6162  /**63   * Note: these aren't important for coverage tracking,64   * but we still want to track them to ensure we aren't regressing them when65   * we fix the source location tracking for other nodes.66   */67  'VariableDeclaration',68  'Identifier',69]);7071/**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 removed74 * by the DropManualMemoization pass.75 */76function isManualMemoization(node: t.Node): boolean {77  // Check if this is a useMemo/useCallback call expression78  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  }9596  return false;97}9899/**100 * Create a location key for comparison. We compare by line/column/source,101 * not by object identity.102 */103function locationKey(loc: t.SourceLocation): string {104  return `${loc.start.line}:${loc.start.column}-${loc.end.line}:${loc.end.column}`;105}106107/**108 * Validates that important source locations from the original code are preserved109 * in the generated AST. This ensures that Istanbul coverage instrumentation can110 * properly map back to the original source code.111 *112 * The validator:113 * 1. Collects locations from "important" nodes in the original AST (those that114 *    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 AST117 *118 * Missing locations can cause Istanbul to fail to track coverage for certain119 * code paths, leading to inaccurate coverage reports.120 */121export function validateSourceLocations(122  func: NodePath<123    t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression124  >,125  generatedAst: CodegenFunction,126  env: Environment,127): void {128  /*129   * Step 1: Collect important locations from the original source130   * 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  >();136137  func.traverse({138    enter(path) {139      const node = path.node;140141      // Only track node types that Istanbul instruments142      if (!IMPORTANT_INSTRUMENTED_TYPES.has(node.type)) {143        return;144      }145146      // Skip manual memoization that the compiler intentionally removes147      if (isManualMemoization(node)) {148        return;149      }150151      /*152       * Skip return statements inside arrow functions that will be simplified to expression body.153       * The compiler transforms `() => { return expr }` to `() => expr` in CodegenReactiveFunction154       */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 === 0163        ) {164          return;165        }166      }167168      // Collect the location if it exists169      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  });183184  // Step 2: Collect all locations from the generated AST with their node types185  const generatedLocations = new Map<string, Set<string>>();186187  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    }197198    // Use Babel's VISITOR_KEYS to traverse only actual node properties199    const keys = t.VISITOR_KEYS[node.type as keyof typeof t.VISITOR_KEYS];200201    if (!keys) {202      return;203    }204205    for (const key of keys) {206      const value = (node as any)[key];207208      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  }219220  // Collect from main function body221  collectGeneratedLocations(generatedAst.body);222223  // Collect from outlined functions224  for (const outlined of generatedAst.outlined) {225    collectGeneratedLocations(outlined.fn.body);226  }227228  /*229   * Step 3: Validate that all important locations are preserved230   * For certain node types, also validate that the node type matches231   */232  const strictNodeTypes = new Set([233    'VariableDeclaration',234    'VariableDeclarator',235    'Identifier',236  ]);237238  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  };256257  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  };277278  for (const [key, {loc, nodeTypes}] of importantOriginalLocations) {279    const generatedNodeTypes = generatedLocations.get(key);280281    if (!generatedNodeTypes) {282      // Location is completely missing283      reportMissingLocation(loc, Array.from(nodeTypes).join(', '));284    } else {285      // Location exists, check each node type286      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 present293           * Check if any generated node type is also an important original node type294           */295          const hasValidNodeType = Array.from(generatedNodeTypes).some(296            genType => nodeTypes.has(genType),297          );298299          if (hasValidNodeType) {300            // At least one generated node type is valid (also in original), so this is just missing301            reportMissingLocation(loc, nodeType);302          } else {303            // None of the generated node types are in original - this is wrong node type304            reportWrongNodeType(loc, nodeType, generatedNodeTypes);305          }306        }307      }308    }309  }310}

Findings

✓ No findings reported for this file.

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.