main
md 272 lines 10.1 KB
Rendered Raw
1 # Plan: `react_compiler_oxc` — OXC Frontend for React Compiler
2
3 ## Context
4
5 The Rust React Compiler (`compiler/crates/`) currently accepts Babel-format AST (`react_compiler_ast::File`) + scope info (`ScopeInfo`) and compiles via `compile_program()`. The only frontend is a Babel NAPI bridge (`compiler/packages/babel-plugin-react-compiler-rust/`). This plan adds an OXC frontend that enables both **build-time code transformation** and **linting** via the OXC ecosystem, all in pure Rust (no JS/NAPI boundary).
6
7 ## Crate Structure
8
9 ```
10 compiler/crates/react_compiler_oxc/
11 Cargo.toml
12 src/
13 lib.rs — Public API: transform(), lint(), ReactCompilerRule
14 prefilter.rs — Quick check for React-like function names in OXC AST
15 convert_ast.rs — OXC AST → react_compiler_ast::File
16 convert_ast_reverse.rs — react_compiler_ast → OXC AST (for applying results)
17 convert_scope.rs — OXC Semantic → ScopeInfo
18 diagnostics.rs — CompileResult → OxcDiagnostic conversion
19 ```
20
21 ### Dependencies (Cargo.toml)
22
23 ```toml
24 [dependencies]
25 react_compiler_ast = { path = "../react_compiler_ast" }
26 react_compiler = { path = "../react_compiler" }
27 react_compiler_diagnostics = { path = "../react_compiler_diagnostics" }
28 oxc_parser = "..."
29 oxc_ast = "..."
30 oxc_semantic = "..."
31 oxc_allocator = "..."
32 oxc_span = "..."
33 oxc_diagnostics = "..."
34 oxc_linter = "..." # for Rule trait
35 indexmap = "..."
36 ```
37
38 ## Module Details
39
40 ### 1. `prefilter.rs` — Quick React Function Check
41
42 Port of `babel-plugin-react-compiler-rust/src/prefilter.ts`.
43
44 ```rust
45 pub fn has_react_like_functions(program: &oxc_ast::ast::Program) -> bool
46 ```
47
48 - Use `oxc_ast::Visit` trait to walk the AST
49 - Check `FunctionDeclaration` names, `VariableDeclarator` inits that are arrow/function expressions
50 - Skip class bodies
51 - Name check: `starts_with(uppercase)` or matches `use[A-Z0-9]`
52 - Return `true` on first match (early exit)
53
54 ### 2. `convert_scope.rs` — OXC Semantic → ScopeInfo
55
56 ```rust
57 pub fn convert_scope_info(semantic: &oxc_semantic::Semantic) -> ScopeInfo
58 ```
59
60 This is the most natural conversion — both use arena-indexed flat tables with copyable u32 IDs.
61
62 **Scopes:** Iterate `semantic.scopes()`. For each scope:
63 - `ScopeId` — direct u32 remapping
64 - `parent` — from `scope_tree.get_parent_id()`
65 - `kind` — map `ScopeFlags``ScopeKind` (Top→Program, Function→Function, CatchClause→Catch, etc.; use parent AST node to distinguish For vs Block)
66 - `bindings` — from `scope_tree.get_bindings()`, map name→SymbolId to name→BindingId
67
68 **Bindings:** Iterate `semantic.symbols()`. For each symbol:
69 - `BindingId` — direct u32 remapping from SymbolId
70 - `name`, `scope` — direct from SymbolTable
71 - `kind` — inspect declaration AST node type: VariableDeclaration(var/let/const), FunctionDeclaration→Hoisted, param→Param, ImportDeclaration→Module
72 - `declaration_type` — string name of the declaring AST node type
73 - `declaration_start` — span.start of the binding's declaring identifier
74 - `import` — for Module bindings, extract source/kind/imported from the ImportDeclaration
75
76 **`node_to_scope`:** Walk AST nodes that create scopes; map `node.span().start → ScopeId`.
77
78 **`reference_to_binding`:** Iterate all references from SymbolTable. For each resolved reference: map `reference.span().start → BindingId`. Also add each symbol's declaration identifier span.
79
80 **`program_scope`:** `ScopeId(0)`.
81
82 Key files:
83 - Target types: `compiler/crates/react_compiler_ast/src/scope.rs`
84 - Reference impl: `compiler/packages/babel-plugin-react-compiler-rust/src/scope.ts`
85
86 ### 3. `convert_ast.rs` — OXC AST → react_compiler_ast::File
87
88 ```rust
89 pub fn convert_program(
90 program: &oxc_ast::ast::Program,
91 source_text: &str,
92 comments: &[oxc_ast::Comment],
93 ) -> react_compiler_ast::File
94 ```
95
96 **Approach:** Recursive conversion, one function per AST category (statement, expression, pattern, JSX, etc.). Data is copied out of OXC's arena into owned `react_compiler_ast` types.
97
98 **ConvertCtx:** Holds a line-offset table (built from source_text at init) for computing `Position { line, column, index }` from byte offsets.
99
100 **BaseNode construction:**
101 - `start = Some(span.start)`, `end = Some(span.end)` — critical for scope lookups
102 - `loc` — computed via line-offset table binary search
103
104 **Key mappings:**
105 | OXC | react_compiler_ast |
106 |-----|-------------------|
107 | `Statement` enum variants | `statements::Statement` variants |
108 | `Expression` enum variants | `expressions::Expression` variants |
109 | `Declaration` (separate in OXC) | Folded into `Statement` (Babel style) |
110 | `BindingPattern` | `patterns::PatternLike` |
111 | `JSXElement/Fragment/etc` | `jsx::*` types |
112 | TS type annotations | `Option<Box<serde_json::Value>>` (opaque passthrough) |
113
114 **Comments:** Map OXC `Comment { kind, span }``react_compiler_ast::common::Comment` (CommentBlock/CommentLine with start/end/value).
115
116 Key files:
117 - Target types: `compiler/crates/react_compiler_ast/src/` (all modules)
118
119 ### 4. `convert_ast_reverse.rs` — react_compiler_ast → OXC AST
120
121 Mirror of `convert_ast.rs`. Converts the compiled Babel-format AST back into OXC AST nodes.
122
123 ```rust
124 pub fn convert_program_to_oxc<'a>(
125 file: &react_compiler_ast::File,
126 allocator: &'a oxc_allocator::Allocator,
127 ) -> oxc_ast::ast::Program<'a>
128 ```
129
130 - Allocates new OXC AST nodes into the provided arena
131 - Maps each `react_compiler_ast` type back to its OXC equivalent
132 - The `CompileResult::Success { ast, .. }` returns `ast: Option<serde_json::Value>` — first deserialize to `react_compiler_ast::File`, then convert to OXC
133
134 This is the most labor-intensive module but avoids the perf cost of re-parsing.
135
136 ### 5. `diagnostics.rs` — Compiler Results → OXC Diagnostics
137
138 ```rust
139 pub fn compile_result_to_diagnostics(
140 result: &CompileResult,
141 source_text: &str,
142 ) -> Vec<oxc_diagnostics::OxcDiagnostic>
143 ```
144
145 Map compiler events/errors to OXC diagnostics:
146 - `LoggerEvent::CompileError { fn_loc, detail }``OxcDiagnostic::warn/error` with label at fn_loc span
147 - `CompileResult::Error { error, .. }``OxcDiagnostic::error`
148 - Preserve error messages and source locations
149
150 ### 6. `lib.rs` — Public API
151
152 #### Transform API (build pipeline)
153
154 ```rust
155 /// Result of compiling a program
156 pub struct TransformResult<'a> {
157 /// The compiled program (None if no changes needed)
158 pub program: Option<oxc_ast::ast::Program<'a>>,
159 pub diagnostics: Vec<oxc_diagnostics::OxcDiagnostic>,
160 pub events: Vec<LoggerEvent>,
161 }
162
163 /// Primary API — accepts pre-parsed AST + semantic
164 pub fn transform<'a>(
165 program: &oxc_ast::ast::Program,
166 semantic: &oxc_semantic::Semantic,
167 source_text: &str,
168 comments: &[oxc_ast::Comment],
169 options: PluginOptions,
170 output_allocator: &'a oxc_allocator::Allocator,
171 ) -> TransformResult<'a>
172
173 /// Convenience wrapper — parses from source text
174 pub fn transform_source<'a>(
175 source_text: &str,
176 source_type: oxc_span::SourceType,
177 options: PluginOptions,
178 output_allocator: &'a oxc_allocator::Allocator,
179 ) -> TransformResult<'a>
180 ```
181
182 Flow:
183 1. Prefilter (`has_react_like_functions`). Skip if `compilationMode == "all"`.
184 2. Convert AST (`convert_program`)
185 3. Convert scope (`convert_scope_info`)
186 4. Call `compile_program(file, scope_info, options)`
187 5. On success with modified AST: deserialize JSON → `File`, reverse-convert to OXC AST
188 6. Convert diagnostics
189
190 #### Lint API
191
192 ```rust
193 pub struct LintResult {
194 pub diagnostics: Vec<oxc_diagnostics::OxcDiagnostic>,
195 }
196
197 /// Lint — accepts pre-parsed AST + semantic
198 pub fn lint(
199 program: &oxc_ast::ast::Program,
200 semantic: &oxc_semantic::Semantic,
201 source_text: &str,
202 comments: &[oxc_ast::Comment],
203 options: PluginOptions,
204 ) -> LintResult
205
206 /// Convenience wrapper
207 pub fn lint_source(
208 source_text: &str,
209 source_type: oxc_span::SourceType,
210 options: PluginOptions,
211 ) -> LintResult
212 ```
213
214 Same as transform but with `no_emit = true` / lint output mode. Only collects diagnostics, no AST output.
215
216 #### oxc_linter::Rule Implementation
217
218 ```rust
219 pub struct ReactCompilerRule {
220 options: PluginOptions,
221 }
222
223 impl oxc_linter::Rule for ReactCompilerRule {
224 fn run_once(&self, ctx: &LintContext) {
225 // ctx already has parsed AST + semantic
226 let result = lint(
227 ctx.program(),
228 ctx.semantic(),
229 ctx.source_text(),
230 ctx.comments(),
231 self.options.clone(),
232 );
233 for diagnostic in result.diagnostics {
234 ctx.diagnostic(diagnostic);
235 }
236 }
237 }
238 ```
239
240 This avoids double-parsing since oxc_linter provides pre-parsed AST and semantic analysis.
241
242 ## Implementation Phases
243
244 ### Phase 1: Foundation (convert_scope + convert_ast + prefilter)
245 - `convert_scope.rs` with unit tests comparing against Babel scope extraction
246 - `convert_ast.rs` with unit tests comparing against Babel parser JSON output
247 - `prefilter.rs` with simple true/false tests
248 - These are independently testable without the full pipeline
249
250 ### Phase 2: Lint path (diagnostics + lint API + Rule)
251 - `diagnostics.rs`
252 - `lint()` function in `lib.rs`
253 - `ReactCompilerRule` impl
254 - Test against existing compiler fixtures — verify diagnostics match
255
256 ### Phase 3: Transform path (reverse converter + transform API)
257 - `convert_ast_reverse.rs`
258 - `transform()` function in `lib.rs`
259 - Integration tests: compile fixtures through OXC pipeline, compare output with Babel pipeline
260
261 ### Phase 4: Differential testing
262 - Cross-validate AST conversion: parse same source with both Babel and OXC, convert both to `react_compiler_ast::File`, diff
263 - Cross-validate scope conversion: compare `ScopeInfo` from both paths
264 - Run full fixture suite through both pipelines, compare compiled output
265
266 ## Verification
267
268 1. **Unit tests:** Each module has tests for its conversion logic
269 2. **Fixture tests:** Use existing fixtures at `compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/`
270 3. **Differential tests:** Compare OXC path output against Babel path output for same inputs
271 4. **`cargo test -p react_compiler_oxc`** — run all crate tests
272 5. **Scope correctness:** Most critical — incorrect scope info causes wrong compilation. Snapshot `ScopeInfo` JSON and compare against Babel extraction golden files