compiler/docs/rust-port/rust-port-0005-babel-plugin.md MARKDOWN 705 lines View on github.com → Search inside
1# Rust Port Step 5: Babel Plugin (`babel-plugin-react-compiler-rust`)23## Goal45Create a new, minimal Babel plugin package (`babel-plugin-react-compiler-rust`) that serves as a thin JavaScript shim over the Rust compiler. The JS side does only three things:671. **Pre-filter**: Quick name-based scan for potential React functions (capitalized or hook-like names)82. **Invoke Rust**: Serialize the Babel AST, scope info, and resolved options to JSON; call the Rust compiler via NAPI93. **Apply result**: Replace the program AST with the Rust-returned AST and forward logger events1011All complex logic  function detection, compilation mode decisions, directives, suppressions, gating rewrites, import insertion, outlined functions  lives in Rust. This ensures the logic is implemented once and reused across future OXC and SWC integrations.1213**Current status**: Implementation complete. All entrypoint logic ported to Rust: compile_program orchestration, shouldSkipCompilation, findFunctionsToCompile, getReactFunctionType/getComponentOrHookLike (with all name heuristics, callsHooksOrCreatesJsx, returnsNonNode, isValidComponentParams), directive parsing, suppression detection/filtering, ProgramContext (uid generation, import tracking), gating rewrites, import insertion. The actual per-function compilation (compileFn) returns a skip event pending full pipeline implementation.1415**Prerequisites**: [rust-port-0001-babel-ast.md](rust-port-0001-babel-ast.md) (complete), [rust-port-0002-scope-types.md](rust-port-0002-scope-types.md) (complete), core compilation pipeline in Rust (in progress).1617---1819## Architecture Overview2021```22┌─────────────────────────────────────────────────────────┐23  Babel                                                  24                                                         25  1. Parse source  Babel AST                            26  2. babel-plugin-react-compiler-rust                    27     ┌─────────────────────────────────────────────┐     28       JS Shim (~50 lines)                             29                                                       30       a) Pre-filter: any capitalized/hook fns?        31       b) Pre-resolve: sources filter, reanimated,│     32          isDev  serializable options                  33       c) Extract scope tree (rust-port-0002)          34       d) JSON.stringify(ast, scope, options)          35       e) Call Rust via NAPI                           36       f) Parse result, forward logger events          37       g) Replace program AST if changed               38     └──────────────┬──────────────────────────────┘     39                     JSON                                40     ┌──────────────▼──────────────────────────────┐     41       Rust Compiler (via napi-rs)                     42                                                       43       - shouldSkipCompilation                         44       - findFunctionsToCompile                        45         (all compilation modes, directives,           46          forwardRef/memo, suppressions, etc.)         47       - compileFn (full pipeline)                     48       - gating rewrites                               49       - import insertion                              50       - outlined function insertion                   51       - panicThreshold handling                       52                                                       53       Returns: modified AST | null + events           54     └─────────────────────────────────────────────┘     55                                                         56  3. Babel continues with modified (or original) AST     57└─────────────────────────────────────────────────────────┘58```5960### Why This Split6162The guiding principle is **implement once in Rust, integrate thinly per tool**. The current TS plugin has ~1300 lines of complex entrypoint logic (`Program.ts`, `Imports.ts`, `Gating.ts`, `Suppression.ts`, `Reanimated.ts`, `Options.ts`). If this logic stayed in JS, it would need to be reimplemented for OXC and SWC integrations. By moving it all to Rust:6364- **Babel shim**: ~50 lines of JS65- **Future OXC integration**: ~50 lines of Rust (native `Traverse` trait, serialize to same JSON format)66- **Future SWC integration**: ~50 lines of Rust (native `VisitMut` trait, serialize to same JSON format)6768Each integration only needs to: (1) do a cheap pre-filter, (2) serialize AST + scope to the Babel JSON format, (3) call `compile()`, (4) apply the result.6970---7172## Rust Public API7374The Rust compiler exposes a single entry point. This extends the existing planned API from `rust-port-notes.md` with structured results:7576```rust77/// Main entry point for the React Compiler.78///79/// Receives a full program AST, scope information, and resolved options.80/// Returns a CompileResult containing either a modified AST or null,81/// along with structured logger events.82#[napi]83pub fn compile(84    ast_json: String,85    scope_json: String,86    options_json: String,87) -> napi::Result<String> {88    let ast: babel_ast::File = serde_json::from_str(&ast_json)?;89    let scope: ScopeInfo = serde_json::from_str(&scope_json)?;90    let opts: PluginOptions = serde_json::from_str(&options_json)?;9192    let result = react_compiler::compile_program(ast, scope, opts);9394    Ok(serde_json::to_string(&result)?)95}96```9798### Result Type99100```rust101#[derive(Serialize)]102#[serde(tag = "kind")]103pub enum CompileResult {104    /// Compilation succeeded (or no functions needed compilation).105    /// `ast` is None if no changes were made to the program.106    Success {107        ast: Option<babel_ast::File>,108        events: Vec<LoggerEvent>,109    },110    /// A fatal error occurred and panicThreshold dictates it should throw.111    /// The JS shim re-throws this as a CompilerError.112    Error {113        error: CompilerErrorInfo,114        events: Vec<LoggerEvent>,115    },116}117118#[derive(Serialize)]119pub struct CompilerErrorInfo {120    pub reason: String,121    pub description: Option<String>,122    pub details: Vec<CompilerErrorDetail>,123}124```125126### Logger Events127128Rust returns the same structured events as the current TS compiler. The JS shim forwards them to the user-provided logger:129130```rust131#[derive(Serialize)]132#[serde(tag = "kind")]133pub enum LoggerEvent {134    CompileSuccess {135        fn_loc: Option<SourceLocation>,136        fn_name: Option<String>,137        memo_slots: u32,138        memo_blocks: u32,139        memo_values: u32,140        pruned_memo_blocks: u32,141        pruned_memo_values: u32,142    },143    CompileError {144        fn_loc: Option<SourceLocation>,145        detail: CompilerErrorDetail,146    },147    CompileSkip {148        fn_loc: Option<SourceLocation>,149        reason: String,150        loc: Option<SourceLocation>,151    },152    CompileUnexpectedThrow {153        fn_loc: Option<SourceLocation>,154        data: String,155    },156    PipelineError {157        fn_loc: Option<SourceLocation>,158        data: String,159    },160    // Note: Timing events are handled on the JS side (performance.mark/measure)161}162```163164---165166## Resolved Options167168Options that involve JS functions or runtime checks (like `sources` filter, Reanimated detection) cannot cross the NAPI boundary. The JS shim pre-resolves these before calling Rust:169170### JS-Side Resolution171172| Option | JS Resolves | Rust Receives |173|--------|------------|---------------|174| `sources` | Calls `sources(filename)` or checks string array | `should_compile: bool` |175| `enableReanimatedCheck` | Calls `pipelineUsesReanimatedPlugin()` | `enable_reanimated: bool` |176| `isDev` (for `enableResetCacheOnSourceFileChanges`) | Checks `__DEV__` / `NODE_ENV` | `is_dev: bool` |177| `logger` | Kept on JS side | Not sent (events returned instead) |178179### Serializable Options (Passed Directly to Rust)180181```typescript182// Options that serialize directly to Rust183interface RustPluginOptions {184    // Pre-resolved by JS185    shouldCompile: boolean;186    enableReanimated: boolean;187    isDev: boolean;188    filename: string | null;189190    // Passed through as-is191    compilationMode: 'infer' | 'syntax' | 'annotation' | 'all';192    panicThreshold: 'all_errors' | 'critical_errors' | 'none';193    target: '17' | '18' | '19' | { kind: 'donotuse_meta_internal'; runtimeModule: string };194    gating: { source: string; importSpecifierName: string } | null;195    dynamicGating: { source: string } | null;196    noEmit: boolean;197    outputMode: 'ssr' | 'client' | 'lint' | null;198    eslintSuppressionRules: string[] | null;199    flowSuppressions: boolean;200    ignoreUseNoForget: boolean;201    customOptOutDirectives: string[] | null;202    environment: EnvironmentConfig;203}204```205206---207208## JS Shim: `babel-plugin-react-compiler-rust`209210### Package Structure211212```213compiler/packages/babel-plugin-react-compiler-rust/214    package.json215    tsconfig.json216    src/217        index.ts          # Babel plugin entry point (main export)218        BabelPlugin.ts    # Program visitor, pre-filter, bridge call219        prefilter.ts      # Name-based React function detection220        bridge.ts         # NAPI invocation, JSON serialization221        scope.ts          # Babel scope  ScopeInfo extraction (from rust-port-0002)222        options.ts        # Option resolution (pre-resolve JS-only options)223```224225### `BabelPlugin.ts`  Babel Plugin Entry Point226227```typescript228import type * as BabelCore from '@babel/core';229import {hasReactLikeFunctions} from './prefilter';230import {compileWithRust} from './bridge';231import {extractScopeInfo} from './scope';232import {resolveOptions, type PluginOptions} from './options';233234export default function BabelPluginReactCompilerRust(235    _babel: typeof BabelCore,236): BabelCore.PluginObj {237    return {238        name: 'react-compiler-rust',239        visitor: {240            Program: {241                enter(prog, pass): void {242                    const filename = pass.filename ?? null;243244                    // Step 1: Resolve options (pre-resolve JS-only values)245                    const opts = resolveOptions(pass.opts, pass.file, filename);246247                    // Step 2: Quick bail — should we compile this file at all?248                    if (!opts.shouldCompile) {249                        return;250                    }251252                    // Step 3: Pre-filter — any potential React functions?253                    if (!hasReactLikeFunctions(prog)) {254                        return;255                    }256257                    // Step 4: Extract scope info258                    const scopeInfo = extractScopeInfo(prog);259260                    // Step 5: Call Rust compiler261                    const result = compileWithRust(262                        prog.node,263                        scopeInfo,264                        opts,265                        pass.file.ast.comments ?? [],266                    );267268                    // Step 6: Forward logger events269                    if (pass.opts.logger && result.events) {270                        for (const event of result.events) {271                            pass.opts.logger.logEvent(filename, event);272                        }273                    }274275                    // Step 7: Handle result276                    if (result.kind === 'error') {277                        // panicThreshold triggered — throw278                        const err = new Error(result.error.reason);279                        // Attach details for CompilerError compatibility280                        (err as any).details = result.error.details;281                        throw err;282                    }283284                    if (result.ast != null) {285                        // Replace the entire program body with Rust's output286                        prog.replaceWith(result.ast);287                        prog.skip(); // Don't re-traverse288                    }289                },290            },291        },292    };293}294```295296### `prefilter.ts`  Name-Based Pre-Filter297298The pre-filter is intentionally loose. It checks only whether any function in the program has a name that *could* be a React component or hook. False positives (like `ParseURL` or `FormatDate`) are acceptable  Rust will quickly determine these aren't React functions and return `null`.299300```typescript301import {NodePath} from '@babel/core';302import * as t from '@babel/types';303304/**305 * Quick check: does this program contain any functions with names that306 * could be React components (capitalized) or hooks (useXxx)?307 *308 * This is intentionally loose  Rust handles the precise detection.309 * We just want to avoid serializing files that definitely have no310 * React functions (e.g., pure utility modules, CSS-in-JS, configs).311 */312export function hasReactLikeFunctions(313    program: NodePath<t.Program>,314): boolean {315    let found = false;316    program.traverse({317        // Skip classes — their methods are not compiled318        ClassDeclaration(path) { path.skip(); },319        ClassExpression(path) { path.skip(); },320321        FunctionDeclaration(path) {322            if (found) return;323            const name = path.node.id?.name;324            if (name && isReactLikeName(name)) {325                found = true;326                path.stop();327            }328        },329        FunctionExpression(path) {330            if (found) return;331            const name = inferFunctionName(path);332            if (name && isReactLikeName(name)) {333                found = true;334                path.stop();335            }336        },337        ArrowFunctionExpression(path) {338            if (found) return;339            const name = inferFunctionName(path);340            if (name && isReactLikeName(name)) {341                found = true;342                path.stop();343            }344        },345    });346    return found;347}348349function isReactLikeName(name: string): boolean {350    return /^[A-Z]/.test(name) || /^use[A-Z0-9]/.test(name);351}352353/**354 * Infer the name of an anonymous function expression from its parent355 * (e.g., `const Foo = () => {}`  'Foo').356 */357function inferFunctionName(358    path: NodePath<t.FunctionExpression | t.ArrowFunctionExpression>,359): string | null {360    const parent = path.parentPath;361    if (362        parent.isVariableDeclarator() &&363        parent.get('init').node === path.node &&364        parent.get('id').isIdentifier()365    ) {366        return (parent.get('id').node as t.Identifier).name;367    }368    if (369        parent.isAssignmentExpression() &&370        parent.get('right').node === path.node &&371        parent.get('left').isIdentifier()372    ) {373        return (parent.get('left').node as t.Identifier).name;374    }375    return null;376}377```378379### `bridge.ts`  NAPI Bridge380381```typescript382// The napi-rs generated binding383import {compile as rustCompile} from '../native';384385import type {ResolvedOptions} from './options';386import type {ScopeInfo} from './scope';387import type * as t from '@babel/types';388389export interface CompileSuccess {390    kind: 'success';391    ast: t.Program | null;392    events: Array<LoggerEvent>;393}394395export interface CompileError {396    kind: 'error';397    error: {398        reason: string;399        description?: string;400        details: Array<unknown>;401    };402    events: Array<LoggerEvent>;403}404405export type CompileResult = CompileSuccess | CompileError;406407export type LoggerEvent = {408    kind: string;409    [key: string]: unknown;410};411412export function compileWithRust(413    ast: t.Program,414    scopeInfo: ScopeInfo,415    options: ResolvedOptions,416    comments: Array<t.Comment>,417): CompileResult {418    // Attach comments to the AST for Rust (Babel stores them separately)419    const astWithComments = {...ast, comments};420421    const resultJson = rustCompile(422        JSON.stringify(astWithComments),423        JSON.stringify(scopeInfo),424        JSON.stringify(options),425    );426427    return JSON.parse(resultJson) as CompileResult;428}429```430431### `options.ts`  Option Resolution432433```typescript434import type * as BabelCore from '@babel/core';435import {436    pipelineUsesReanimatedPlugin,437    injectReanimatedFlag,438} from './reanimated'; // Thin copy or import from existing439440export interface ResolvedOptions {441    // Pre-resolved by JS442    shouldCompile: boolean;443    enableReanimated: boolean;444    isDev: boolean;445    filename: string | null;446447    // Pass-through448    compilationMode: string;449    panicThreshold: string;450    target: unknown;451    gating: unknown;452    dynamicGating: unknown;453    noEmit: boolean;454    outputMode: string | null;455    eslintSuppressionRules: string[] | null;456    flowSuppressions: boolean;457    ignoreUseNoForget: boolean;458    customOptOutDirectives: string[] | null;459    environment: Record<string, unknown>;460}461462export type PluginOptions = Partial<ResolvedOptions> & Record<string, unknown>;463464export function resolveOptions(465    rawOpts: PluginOptions,466    file: BabelCore.BabelFile,467    filename: string | null,468): ResolvedOptions {469    // Resolve sources filter (may be a function)470    let shouldCompile = true;471    if (rawOpts.sources != null && filename != null) {472        if (typeof rawOpts.sources === 'function') {473            shouldCompile = rawOpts.sources(filename);474        } else if (Array.isArray(rawOpts.sources)) {475            shouldCompile = rawOpts.sources.some(476                (prefix: string) => filename.indexOf(prefix) !== -1,477            );478        }479    } else if (rawOpts.sources != null && filename == null) {480        shouldCompile = false; // sources specified but no filename481    }482483    // Resolve reanimated check484    const enableReanimated =485        (rawOpts.enableReanimatedCheck !== false) &&486        pipelineUsesReanimatedPlugin(file.opts.plugins);487488    // Resolve isDev489    const isDev =490        (typeof __DEV__ !== 'undefined' && __DEV__ === true) ||491        process.env['NODE_ENV'] === 'development';492493    return {494        shouldCompile,495        enableReanimated,496        isDev,497        filename,498        compilationMode: rawOpts.compilationMode ?? 'infer',499        panicThreshold: rawOpts.panicThreshold ?? 'none',500        target: rawOpts.target ?? '19',501        gating: rawOpts.gating ?? null,502        dynamicGating: rawOpts.dynamicGating ?? null,503        noEmit: rawOpts.noEmit ?? false,504        outputMode: rawOpts.outputMode ?? null,505        eslintSuppressionRules: rawOpts.eslintSuppressionRules ?? null,506        flowSuppressions: rawOpts.flowSuppressions ?? true,507        ignoreUseNoForget: rawOpts.ignoreUseNoForget ?? false,508        customOptOutDirectives: rawOpts.customOptOutDirectives ?? null,509        environment: rawOpts.environment ?? {},510    };511}512```513514---515516## What Rust Implements (from `Program.ts` and friends)517518The following logic moves entirely from the TS entrypoint into Rust. Rust operates on the deserialized Babel AST and scope info, and returns a modified AST.519520### From `Program.ts`521522| Function | What It Does | Rust Module |523|----------|-------------|-------------|524| `shouldSkipCompilation` | Check sources filter (pre-resolved), check for existing `c` import from runtime module | `entrypoint/program.rs` |525| `findFunctionsToCompile` | Traverse program, skip classes, apply compilation mode, call `getReactFunctionType` | `entrypoint/program.rs` |526| `getReactFunctionType` | Determine if a function is Component/Hook/Other based on compilation mode, names, directives | `entrypoint/program.rs` |527| `getComponentOrHookLike` | Name-based heuristics + `callsHooksOrCreatesJsx` + `isValidComponentParams` + `returnsNonNode` + `isForwardRefCallback` + `isMemoCallback` | `entrypoint/program.rs` |528| `processFn` | Per-function: check directives (opt-in/opt-out), compile, check output mode | `entrypoint/program.rs` |529| `tryCompileFunction` | Check suppressions, call `compileFn`, handle errors | `entrypoint/program.rs` |530| `applyCompiledFunctions` | Replace original functions with compiled versions, handle gating, insert outlined functions | `entrypoint/program.rs` |531| `createNewFunctionNode` | Build replacement AST node matching original function type | `entrypoint/program.rs` |532| `handleError` / `logError` | Apply panicThreshold, log to events | `entrypoint/program.rs` |533534### From `Imports.ts`535536| Function | What It Does | Rust Module |537|----------|-------------|-------------|538| `ProgramContext` | Track compiled functions, generate unique names, manage imports | `entrypoint/imports.rs` |539| `addImportsToProgram` | Insert import declarations (or require calls) into program body | `entrypoint/imports.rs` |540| `validateRestrictedImports` | Check for blocklisted import modules | `entrypoint/imports.rs` |541542### From `Gating.ts`543544| Function | What It Does | Rust Module |545|----------|-------------|-------------|546| `insertGatedFunctionDeclaration` | Rewrite function with gating conditional (optimized vs unoptimized) | `entrypoint/gating.rs` |547| `insertAdditionalFunctionDeclaration` | Handle hoisted function declarations referenced before declaration | `entrypoint/gating.rs` |548549### From `Suppression.ts`550551| Function | What It Does | Rust Module |552|----------|-------------|-------------|553| `findProgramSuppressions` | Parse eslint-disable/enable and Flow suppression comments | `entrypoint/suppression.rs` |554| `filterSuppressionsThatAffectFunction` | Check if suppression ranges overlap a function | `entrypoint/suppression.rs` |555| `suppressionsToCompilerError` | Convert suppressions to compiler errors | `entrypoint/suppression.rs` |556557### From `Reanimated.ts`558559| Function | What It Does | Rust Module |560|----------|-------------|-------------|561| `injectReanimatedFlag` | Set `enableCustomTypeDefinitionForReanimated` in environment config | Pre-resolved by JS; Rust receives `enableReanimated: bool` |562| `pipelineUsesReanimatedPlugin` | Check if reanimated babel plugin is present | Pre-resolved by JS |563564### From `Options.ts`565566| Function | What It Does | Rust Module |567|----------|-------------|-------------|568| `parsePluginOptions` | Validate and parse plugin options | JS resolves, Rust re-validates serializable subset |569| Option types and schemas | Zod schemas for options | Rust serde types with validation |570| `LoggerEvent` types | Event type definitions | Rust enum (serialized back to JS) |571572---573574## NAPI Bridge Details575576### Technology: napi-rs577578The bridge uses [napi-rs](https://napi.rs/) to expose the Rust `compile` function to Node.js. This is the same approach used by SWC (`@swc/core`), Biome, and other Rust-based JS tools.579580### Serialization: JSON Strings581582The bridge passes JSON strings across the NAPI boundary. This is the simplest approach and provides several benefits:583584- **Debuggable**: JSON can be logged, inspected, and round-trip tested585- **Consistent with existing infrastructure**: The `react_compiler_ast` crate already handles JSON serde with all 1714 test fixtures passing586- **No schema coupling**: The JS side doesn't need generated bindings — just `JSON.stringify`/`JSON.parse`587- **Adequate performance**: For file-level granularity (one call per file), JSON serialization overhead is negligible compared to compilation time588589### Performance Considerations590591The JSON serialization adds overhead, but it is bounded:592593- **Serialization**: `JSON.stringify` of a typical program AST: ~1-5ms594- **Deserialization in Rust**: `serde_json::from_str`: ~1-5ms595- **Re-serialization in Rust**: `serde_json::to_string` of result: ~1-5ms596- **Parse in JS**: `JSON.parse` of result: ~1-5ms597- **Total overhead**: ~4-20ms per file598- **Compilation time**: Typically 50-500ms per file599600The serialization overhead is 2-10% of total time. If this becomes a bottleneck, a future optimization could use `Buffer` passing with a binary format, but JSON is the right starting point.601602### Native Module Structure603604```605compiler/packages/babel-plugin-react-compiler-rust/606    native/607        Cargo.toml          # napi-rs crate608        src/609            lib.rs          # #[napi] compile function610        build.rs            # napi-rs build script611    npm/                    # Platform-specific npm packages (generated by napi-rs)612        darwin-arm64/613        darwin-x64/614        linux-x64-gnu/615        win32-x64-msvc/616        ...617```618619---620621## What Stays in JS vs What Moves to Rust622623### JS Side (Thin Shim)624625| Responsibility | Reason it stays in JS |626|---------------|----------------------|627| Pre-filter (name-based scan) | Avoids serialization for files with no React functions |628| Resolve `sources` filter | May be a JS function (not serializable) |629| Resolve Reanimated check | Requires `require.resolve` and Babel plugin list inspection |630| Resolve `isDev` | Requires `process.env` / `__DEV__` access |631| Extract scope info | Requires Babel scope API |632| Serialize AST/scope/options | Bridge responsibility |633| Forward logger events | Logger is a JS callback |634| Throw on fatal errors | JS exception mechanism |635| Replace program AST | Babel `path.replaceWith` API |636| Performance timing | `performance.mark/measure` API |637638### Rust Side (Everything Else)639640| Responsibility | Current TS Location |641|---------------|-------------------|642| `shouldSkipCompilation` (non-sources checks) | `Program.ts:782-816` |643| `findFunctionsToCompile` | `Program.ts:495-559` |644| `getReactFunctionType` | `Program.ts:818-864` |645| `getComponentOrHookLike` | `Program.ts:1049-1078` |646| All name/param/return heuristics | `Program.ts:897-1164` |647| `forwardRef`/`memo` detection | `Program.ts:951-970` |648| Directive parsing (`use memo`, `use no memo`, `use memo if(...)`) | `Program.ts:47-144` |649| Suppression detection and filtering | `Suppression.ts` (all) |650| Per-function compilation (`compileFn`) | `Pipeline.ts` |651| Gating rewrites | `Gating.ts` (all) |652| Import generation and insertion | `Imports.ts:225-306` |653| Outlined function insertion | `Program.ts:283-329` |654| `ProgramContext` (uid gen, import tracking) | `Imports.ts:64-209` |655| Error handling / panicThreshold | `Program.ts:146-222` |656| Option validation | `Options.ts:324-403` |657658---659660## Cross-Tool Strategy (OXC, SWC)661662This architecture is designed to support future OXC and SWC integrations with minimal per-tool code.663664### Common Boundary: Babel JSON AST665666All integrations serialize to the same Babel JSON AST format that the `react_compiler_ast` crate expects. This means:667668- **OXC integration**: A Rust transform that converts OXC's native AST → Babel JSON AST → calls `compile()` → converts result back to OXC AST. Since both are Rust, this can use the struct types directly (no JSON step needed for the Rust→Rust path — just type conversion).669- **SWC integration**: A Rust transform (native or WASM plugin) that converts SWC's AST → Babel JSON AST → calls `compile()` → converts result back.670671### Scope Abstraction672673Each tool provides scope information differently:674- **Babel**: Scope tree object graph (extracted by JS, serialized to `ScopeInfo`)675- **OXC**: `ScopeTree` + `SymbolTable` from `oxc_semantic` (Rust-native, converted to `ScopeInfo`)676- **SWC**: Hygiene system (`SyntaxContext`/`Mark`)  requires building a scope tree equivalent677678The `ScopeInfo` type from `rust-port-0002` serves as the common abstraction. Each integration extracts its tool's scope model into this format.679680### Integration Size Comparison681682| Tool | Integration Code | Where Logic Lives |683|------|-----------------|-------------------|684| Babel (this doc) | ~50 lines JS + NAPI bridge | Rust |685| OXC (future) | ~100 lines Rust (AST conversion) | Rust |686| SWC (future) | ~100 lines Rust (AST conversion + scope extraction) | Rust |687688---689690## Differences from Current TS Plugin691692### Behavioral Equivalence693694The Rust plugin must produce identical output to the TS plugin for all inputs. The existing test infrastructure (`yarn snap`) can be used to verify this by running both plugins on the same fixtures and comparing output.695696### Known Differences6976981. **Timing events**: Handled on the JS side using `performance.mark/measure` (not sent to Rust). The JS shim wraps the Rust call with timing markers.6997002. **`CompilerError` class**: Rust returns a plain JSON error object. The JS shim constructs a `CompilerError`-compatible exception for Babel's error reporting.7017023. **`debugLogIRs` logger callback**: This optional callback receives intermediate compiler pipeline values. Rust would need to serialize these if supported. **Decision**: Defer to a follow-up; not needed for initial parity.7037044. **Comments handling**: Babel stores comments separately on `file.ast.comments`, not attached to AST nodes. The JS shim attaches comments to the program AST before serializing. Rust uses them for suppression detection.

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.