Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/swc/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,7 @@ impl Options {
&program,
source_type,
&fm.src,
fm.start_pos,
comments,
options,
);
Expand Down
10 changes: 6 additions & 4 deletions crates/swc_ecma_react_compiler/examples/react_compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

use std::{env, path::Path};

use swc_common::{comments::SingleThreadedComments, sync::Lrc, FileName, SourceMap};
use swc_common::{comments::SingleThreadedComments, sync::Lrc, BytePos, FileName, SourceMap};
use swc_ecma_ast::{EsVersion, Program};
use swc_ecma_codegen::{text_writer::JsWriter, Emitter, Node};
use swc_ecma_parser::{parse_file_as_program, EsSyntax, Syntax, TsSyntax};
Expand All @@ -33,14 +33,16 @@ fn run() -> Result<(), String> {
let source_text = std::fs::read_to_string(path)
.map_err(|err| format!("failed to read {}: {err}", path.display()))?;

let (program, comments, source_type) = parse_program(path, &source_text)?;
let (program, comments, source_type, source_file_start_pos) =
parse_program(path, &source_text)?;
let mut options = default_plugin_options();
options.filename = Some(path.display().to_string());

let result = transform(
&program,
source_type,
&source_text,
source_file_start_pos,
Some(&comments),
options,
);
Expand All @@ -55,7 +57,7 @@ fn run() -> Result<(), String> {
fn parse_program(
path: &Path,
source_text: &str,
) -> Result<(Program, SingleThreadedComments, SourceType), String> {
) -> Result<(Program, SingleThreadedComments, SourceType, BytePos), String> {
let cm = Lrc::new(SourceMap::default());
let fm = cm.new_source_file(
Lrc::new(FileName::Real(path.to_path_buf())),
Expand Down Expand Up @@ -85,7 +87,7 @@ fn parse_program(
let program =
program.map_err(|error| format!("failed to parse input: {}", error.kind().msg()))?;
let source_type = SourceType::from_program(&program).with_typescript(is_typescript);
Ok((program, comments, source_type))
Ok((program, comments, source_type, fm.start_pos))
}

fn syntax_for_path(path: &Path) -> Syntax {
Expand Down
31 changes: 21 additions & 10 deletions crates/swc_ecma_react_compiler/src/convert_ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use react_compiler_ast::{
use swc_common::{
comments::{Comment as SwcComment, CommentKind, SingleThreadedComments},
util::take::Take,
Span, Spanned,
BytePos, Span, Spanned,
};
use swc_ecma_ast as swc;

Expand All @@ -28,36 +28,38 @@ use crate::preserved_ast::PreservedAst;
pub struct ConvertResult {
pub file: File,
pub preserved_ast: PreservedAst,
pub source_file_start_pos: swc_common::BytePos,
pub source_file_start_pos: BytePos,
}

/// Converts an SWC AST to the React compiler's Babel-compatible AST.
pub fn convert_program(
program: &swc::Program,
source_text: &str,
source_file_start_pos: BytePos,
comments: Option<&SingleThreadedComments>,
) -> ConvertResult {
let ctx = ConvertCtx::new(source_text);
let ctx = ConvertCtx::new(source_text, source_file_start_pos);
let comments = convert_swc_comments(&ctx, comments);
let mut ctx = ctx.with_comments(comments);
let file = ctx.convert_program(program);

ConvertResult {
file,
preserved_ast: ctx.preserved_ast.into_inner(),
source_file_start_pos: program.span().lo,
source_file_start_pos,
}
}

struct ConvertCtx<'a> {
source_text: &'a str,
source_file_start_pos: BytePos,
line_offsets: Vec<u32>,
comments: Vec<Comment>,
preserved_ast: RefCell<PreservedAst>,
}

impl<'a> ConvertCtx<'a> {
fn new(source_text: &'a str) -> Self {
fn new(source_text: &'a str, source_file_start_pos: BytePos) -> Self {
let mut line_offsets = vec![0u32];
for (i, ch) in source_text.char_indices() {
let next = i + ch.len_utf8();
Expand All @@ -67,6 +69,7 @@ impl<'a> ConvertCtx<'a> {
}
Self {
source_text,
source_file_start_pos,
line_offsets,
comments: Default::default(),
preserved_ast: Default::default(),
Expand All @@ -78,6 +81,9 @@ impl<'a> ConvertCtx<'a> {
self
}

/// `start`/`end`/`node_id` keep absolute SWC `BytePos` values so reverse
/// conversion and scope-key/rename logic can continue to use them directly.
/// `loc` remains Babel-style and file-relative.
fn make_base_node(&self, span: Span) -> BaseNode {
BaseNode {
node_type: None,
Expand All @@ -95,15 +101,17 @@ impl<'a> ConvertCtx<'a> {

/// Converts a SWC span to a Babel-compatible position.
///
/// SWC `BytePos` values are byte offsets and 1-based. Base node offsets
/// stay 1-based so scope keys can use `span.lo` directly, while `loc`
/// follows Babel's 1-based lines and 0-based columns/indices.
/// SWC `BytePos` values are absolute and 1-based. `start`/`end`/`node_id`
/// keep these absolute offsets, while `loc` follows Babel's file-relative
/// 1-based lines and 0-based columns/indices.
///
/// Assumption: the React Compiler does not receive the original source text
/// from this bridge, so UTF-8 byte offsets are enough for `column` and
/// `index`. If that changes, switch these offsets to UTF-16 code units.
fn position(&self, offset: u32) -> Position {
let offset = offset.saturating_sub(1).min(self.source_text.len() as u32);
let offset = offset
.saturating_sub(self.source_file_start_pos.0)
.min(self.source_text.len() as u32);
let line_idx = match self.line_offsets.binary_search(&offset) {
Ok(idx) => idx,
Err(idx) => idx.saturating_sub(1),
Expand Down Expand Up @@ -153,7 +161,10 @@ impl<'a> ConvertCtx<'a> {
.source_text
.find('\n')
.unwrap_or(self.source_text.len()) as u32;
let span = Span::new(swc_common::BytePos(1), swc_common::BytePos(1 + end));
let span = Span::new(
self.source_file_start_pos,
self.source_file_start_pos + swc_common::BytePos(end),
);
InterpreterDirective {
base: self.make_base_node(span),
value: shebang.to_string(),
Expand Down
7 changes: 5 additions & 2 deletions crates/swc_ecma_react_compiler/src/convert_ast_reverse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,11 @@ impl ReverseCtx {
}

fn span_from_base(&self, base: &BaseNode) -> Span {
// `start` and `end` preserve absolute SWC `BytePos` values from the
// forward conversion, while `loc` indices are file-relative.
// Invariant with the forward converter:
// - `start`/`end` keep absolute SWC `BytePos` values.
// - `loc.*.index` values are Babel-style file-relative offsets.
// So the fallback path must rebase `loc.index` with
// `source_file_start_pos` to recover absolute SWC spans.
match (base.start, base.end) {
(Some(start), Some(end)) => Span::new(BytePos(start), BytePos(end)),
(Some(start), None) => Span::new(BytePos(start), BytePos(start)),
Expand Down
41 changes: 30 additions & 11 deletions crates/swc_ecma_react_compiler/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ pub use react_compiler::entrypoint::plugin_options::{
};
use react_compiler_hir::environment_config::EnvironmentConfig;
pub use source_type::SourceType;
use swc_common::{comments::SingleThreadedComments, sync::Lrc, FileName};
use swc_common::{comments::SingleThreadedComments, sync::Lrc, BytePos, FileName};
use swc_ecma_ast::Program;

use crate::{convert_ast::ConvertResult, convert_scope::SemanticBuilder};
Expand Down Expand Up @@ -81,6 +81,7 @@ pub fn transform(
program: &Program,
source_type: SourceType,
source_text: &str,
source_file_start_pos: BytePos,
comments: Option<&SingleThreadedComments>,
options: PluginOptions,
) -> TransformResult {
Expand All @@ -98,7 +99,7 @@ pub fn transform(
file,
preserved_ast,
source_file_start_pos,
} = convert_program(program, source_text, comments);
} = convert_program(program, source_text, source_file_start_pos, comments);
let emit_success_error_diagnostics = options.no_emit;
let result =
react_compiler::entrypoint::program::compile_program(file, scope_info.clone(), options);
Expand Down Expand Up @@ -138,9 +139,14 @@ pub(crate) fn transform_source(
options: PluginOptions,
) -> TransformResult {
match parse_source(source_text, syntax) {
Ok((program, comments, source_type)) => {
transform(&program, source_type, source_text, Some(&comments), options)
}
Ok((program, comments, source_type, source_file_start_pos)) => transform(
&program,
source_type,
source_text,
source_file_start_pos,
Some(&comments),
options,
),
Err(diagnostic) => TransformResult {
program: None,
diagnostics: vec![*diagnostic],
Expand All @@ -159,12 +165,20 @@ pub fn lint(
program: &Program,
source_type: SourceType,
source_text: &str,
source_file_start_pos: BytePos,
comments: Option<&SingleThreadedComments>,
options: PluginOptions,
) -> LintResult {
let mut opts = options;
opts.no_emit = true;
let result = transform(program, source_type, source_text, comments, opts);
let result = transform(
program,
source_type,
source_text,
source_file_start_pos,
comments,
opts,
);
LintResult {
diagnostics: result.diagnostics,
}
Expand All @@ -179,9 +193,14 @@ pub fn lint_source(
options: PluginOptions,
) -> LintResult {
match parse_source(source_text, syntax) {
Ok((program, comments, source_type)) => {
lint(&program, source_type, source_text, Some(&comments), options)
}
Ok((program, comments, source_type, source_file_start_pos)) => lint(
&program,
source_type,
source_text,
source_file_start_pos,
Some(&comments),
options,
),
Err(diagnostic) => LintResult {
diagnostics: vec![*diagnostic],
},
Expand All @@ -191,7 +210,7 @@ pub fn lint_source(
fn parse_source(
source_text: &str,
syntax: swc_ecma_parser::Syntax,
) -> Result<(Program, SingleThreadedComments, SourceType), Box<DiagnosticMessage>> {
) -> Result<(Program, SingleThreadedComments, SourceType, BytePos), Box<DiagnosticMessage>> {
let cm = Lrc::new(swc_common::SourceMap::default());
let fm = cm.new_source_file(Lrc::new(FileName::Anon), source_text.to_string());
let comments = SingleThreadedComments::default();
Expand All @@ -206,7 +225,7 @@ fn parse_source(
) {
Ok(program) => {
let source_type = SourceType::from_program(&program).with_typescript(is_typescript);
Ok((program, comments, source_type))
Ok((program, comments, source_type, fm.start_pos))
}
Err(error) => Err(Box::new(DiagnosticMessage {
severity: diagnostics::Severity::Error,
Expand Down
Loading