From 65b6f9a9346fe984f1699d5e96b6677605c802f2 Mon Sep 17 00:00:00 2001 From: Victor Hallberg Date: Wed, 27 May 2026 14:12:28 +0200 Subject: [PATCH 01/10] test: add smoke test testing if lingui-set directive attached to unbundled code still applies --- e2e/smoke.test.ts | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/e2e/smoke.test.ts b/e2e/smoke.test.ts index 9ef5d68..e8df050 100644 --- a/e2e/smoke.test.ts +++ b/e2e/smoke.test.ts @@ -1,5 +1,5 @@ import {describe, expect, it} from 'vitest' -import {transformFile} from '@swc/core' +import {transform, transformFile} from '@swc/core' import {resolve} from 'path' const wasmPath = resolve(import.meta.dirname, '../target/wasm32-wasip1/release/lingui_macro_plugin.wasm') @@ -34,4 +34,33 @@ describe('E2E Smoke Test', () => { const result = await transformWithSwc(fixturePath, 'production') expect(result.code).toMatchSnapshot() }) + + it('should keep lingui-set directives set on TypeScript export declarations', async () => { + const result = await transform( + ` + import { msg } from '@lingui/core/macro' + + // lingui-set context="navigation" + type SomeType = string + + const table = { + home: msg\`Home\`, + } + `, + { + filename: 'directive-after-export-type.ts', + jsc: { + parser: { + syntax: 'typescript', + tsx: false, + }, + experimental: { + plugins: [[wasmPath, { descriptorFields: 'message' }]], + }, + }, + }, + ) + + expect(result.code).toContain('context: "navigation"') + }) }) From fad320dde1bd5a06a92b2b9dd669f6c16159cf24 Mon Sep 17 00:00:00 2001 From: Victor Hallberg Date: Wed, 27 May 2026 14:14:55 +0200 Subject: [PATCH 02/10] fix: handle lingui-set directives attached to unbundled code --- src/comment_directive.rs | 92 ++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 38 +++++++++++++++-- src/macro_utils.rs | 46 +++++++++++++++++++- 3 files changed, 172 insertions(+), 4 deletions(-) diff --git a/src/comment_directive.rs b/src/comment_directive.rs index f1de874..3f9dfae 100644 --- a/src/comment_directive.rs +++ b/src/comment_directive.rs @@ -24,6 +24,12 @@ pub struct DirectiveEntry { pub values: DirectiveValues, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DirectiveLineEntry { + pub line: usize, + pub values: DirectiveValues, +} + #[derive(Debug, Clone, PartialEq, Eq)] enum DirectiveValueUpdate { Set(String), @@ -235,6 +241,92 @@ pub fn find_directive_for_pos( } } +pub fn collect_lingui_directives_from_source( + source: &str, + start_line: usize, +) -> Vec { + let mut directives: Vec<(usize, bool, DirectiveUpdate)> = source + .lines() + .enumerate() + .filter_map(|(index, line)| { + let comment = extract_directive_comment(line)?; + match parse_lingui_directive_raw(comment) { + Ok(Some(parsed)) => Some((index + start_line, parsed.reset, parsed.values)), + Ok(None) => None, + Err(message) => { + HANDLER.with(|handler| handler.struct_err(&message).emit()); + None + } + } + }) + .collect(); + + directives.sort_by_key(|directive| directive.0); + + let mut accumulated = DirectiveValues::default(); + + directives + .into_iter() + .map(|(line, reset, values)| { + if reset { + accumulated = DirectiveValues::default(); + } + + accumulated.apply_update(values); + + DirectiveLineEntry { + line, + values: accumulated.clone(), + } + }) + .collect() +} + +pub fn find_directive_for_line( + directives: &[DirectiveLineEntry], + line: usize, +) -> Option<&DirectiveValues> { + if directives.is_empty() { + return None; + } + + let mut lo = 0usize; + let mut hi = directives.len(); + + while lo < hi { + let mid = (lo + hi) / 2; + if directives[mid].line <= line { + lo = mid + 1; + } else { + hi = mid; + } + } + + if lo == 0 { + None + } else { + Some(&directives[lo - 1].values) + } +} + +fn extract_directive_comment(line: &str) -> Option<&str> { + let trimmed = line.trim(); + + if let Some(comment) = trimmed.strip_prefix("//") { + return Some(comment.trim()); + } + + if let Some(comment) = trimmed.strip_prefix("/*") { + return Some(comment.trim_end_matches("*/").trim()); + } + + if let Some(comment) = trimmed.strip_prefix('*') { + return Some(comment.trim_end_matches("*/").trim()); + } + + None +} + pub(crate) fn collect_lingui_directives( node: &N, comments: &Option, diff --git a/src/lib.rs b/src/lib.rs index 83c1de6..201bad1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,5 @@ use std::collections::HashSet; -use swc_core::common::{Span, Spanned, SyntaxContext, DUMMY_SP}; +use swc_core::common::{SourceMapper, Span, Spanned, SyntaxContext, DUMMY_SP}; use swc_core::common::comments::*; use swc_core::ecma::utils::private_ident; @@ -31,7 +31,7 @@ use crate::macro_utils::*; use crate::options::*; use ast_utils::*; use builder::*; -use comment_directive::collect_lingui_directives; +use comment_directive::{collect_lingui_directives, collect_lingui_directives_from_source}; use js_macro_folder::JsMacroFolder; use jsx_visitor::TransJSXVisitor; @@ -562,5 +562,37 @@ pub fn process_transform(program: Program, metadata: TransformPluginProgramMetad .unwrap_or_default(), ); - program.fold_with(&mut LinguiMacroFolder::new(config, metadata.comments)) + let mut folder = LinguiMacroFolder::new(config, metadata.comments); + let program_span = program.span(); + let file = metadata.source_map.lookup_char_pos(program_span.lo).file; + let file_span = Span::new(file.start_pos, program_span.hi); + + if let Ok(source) = metadata.source_map.span_to_snippet(file_span) { + let start_line = metadata.source_map.lookup_char_pos(file_span.lo).line; + let line_starts = collect_line_starts(file_span.lo, &source); + let directives = collect_lingui_directives_from_source(&source, start_line); + + folder + .ctx + .set_source_directives(directives, line_starts, start_line); + } + + program.fold_with(&mut folder) +} + +fn collect_line_starts( + start: swc_core::common::BytePos, + source: &str, +) -> Vec { + let mut line_starts = vec![start]; + let mut offset = start.0; + + for byte in source.bytes() { + offset += 1; + if byte == b'\n' { + line_starts.push(swc_core::common::BytePos(offset)); + } + } + + line_starts } diff --git a/src/macro_utils.rs b/src/macro_utils.rs index 5f6db8f..a0be0d3 100644 --- a/src/macro_utils.rs +++ b/src/macro_utils.rs @@ -1,5 +1,8 @@ use crate::ast_utils::*; -use crate::comment_directive::{find_directive_for_pos, DirectiveEntry, DirectiveValues}; +use crate::comment_directive::{ + find_directive_for_line, find_directive_for_pos, DirectiveEntry, DirectiveLineEntry, + DirectiveValues, +}; use crate::tokens::*; use crate::LinguiOptions; use std::collections::{HashMap, HashSet}; @@ -38,6 +41,9 @@ pub struct MacroCtx { pub options: LinguiOptions, pub comment_directives: Vec, + pub source_directives: Vec, + pub source_line_starts: Vec, + pub source_start_line: usize, pub runtime_idents: RuntimeIdents, } @@ -70,8 +76,46 @@ impl MacroCtx { self.comment_directives = directives; } + pub fn set_source_directives( + &mut self, + directives: Vec, + line_starts: Vec, + start_line: usize, + ) { + self.source_directives = directives; + self.source_line_starts = line_starts; + self.source_start_line = start_line; + } + pub fn get_comment_directive(&self, pos: BytePos) -> Option<&DirectiveValues> { find_directive_for_pos(&self.comment_directives, pos) + .or_else(|| self.get_source_directive(pos)) + } + + fn get_source_directive(&self, pos: BytePos) -> Option<&DirectiveValues> { + let line = self.line_for_pos(pos)?; + find_directive_for_line(&self.source_directives, line) + } + + fn line_for_pos(&self, pos: BytePos) -> Option { + let first = *self.source_line_starts.first()?; + if pos < first { + return None; + } + + let mut lo = 0usize; + let mut hi = self.source_line_starts.len(); + + while lo < hi { + let mid = (lo + hi) / 2; + if self.source_line_starts[mid] <= pos { + lo = mid + 1; + } else { + hi = mid; + } + } + + Some(self.source_start_line + lo.saturating_sub(1)) } /// is given ident exported from @lingui/macro? and one of choice functions? From 5678045ec47270c946731e76a177b51a73188473 Mon Sep 17 00:00:00 2001 From: Victor Hallberg Date: Fri, 29 May 2026 13:39:04 +0200 Subject: [PATCH 03/10] adress review suggestions --- src/comment_directive.rs | 461 +++++++++++++++++++++++---------------- src/lib.rs | 93 ++++---- src/macro_utils.rs | 54 +---- tests/common/mod.rs | 54 +++-- 4 files changed, 366 insertions(+), 296 deletions(-) diff --git a/src/comment_directive.rs b/src/comment_directive.rs index 3f9dfae..0ff8cdb 100644 --- a/src/comment_directive.rs +++ b/src/comment_directive.rs @@ -1,16 +1,18 @@ use once_cell::sync::Lazy; use regex::Regex; -use std::collections::HashSet; -use swc_core::common::comments::{Comment, Comments}; -use swc_core::common::{BytePos, Span, Spanned}; -use swc_core::ecma::ast::*; -use swc_core::ecma::visit::{Visit, VisitWith}; +#[cfg(test)] +use swc_core::common::comments::Comment; +use swc_core::common::{BytePos, Span}; use swc_core::plugin::errors::HANDLER; static DIRECTIVE_RE: Lazy = Lazy::new(|| Regex::new(r"^(lingui-(?:set|reset))(?:\s|$)(.*)").unwrap()); static TOKEN_RE: Lazy = Lazy::new(|| Regex::new(r#"\s+|(\w+)(?:="([^"]*)")?"#).unwrap()); +fn is_lingui_directive_prefix(comment: &str) -> bool { + comment.starts_with("lingui-set") || comment.starts_with("lingui-reset") +} + #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct DirectiveValues { pub context: Option, @@ -24,12 +26,6 @@ pub struct DirectiveEntry { pub values: DirectiveValues, } -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct DirectiveLineEntry { - pub line: usize, - pub values: DirectiveValues, -} - #[derive(Debug, Clone, PartialEq, Eq)] enum DirectiveValueUpdate { Set(String), @@ -49,13 +45,6 @@ struct ParsedDirective { values: DirectiveUpdate, } -#[derive(Debug, Clone, PartialEq, Eq)] -struct RawDirectiveEntry { - pos: BytePos, - reset: bool, - values: DirectiveUpdate, -} - impl DirectiveValues { fn apply_update(&mut self, update: DirectiveUpdate) { if let Some(value) = update.context { @@ -169,51 +158,6 @@ fn parse_lingui_directive_raw(comment_value: &str) -> Result Vec { - let mut directives: Vec = comments - .iter() - .filter_map( - |comment| match parse_lingui_directive_raw(comment.text.as_ref()) { - Ok(Some(parsed)) => Some(RawDirectiveEntry { - pos: comment.span.lo, - reset: parsed.reset, - values: parsed.values, - }), - Ok(None) => None, - Err(message) => { - HANDLER.with(|handler| { - handler.struct_span_err(comment.span, &message).emit(); - }); - None - } - }, - ) - .collect(); - - directives.sort_by_key(|directive| directive.pos); - - let mut accumulated = DirectiveValues::default(); - - directives - .into_iter() - .map(|directive| { - let mut values = if directive.reset { - DirectiveValues::default() - } else { - accumulated.clone() - }; - - values.apply_update(directive.values); - accumulated = values.clone(); - - DirectiveEntry { - pos: directive.pos, - values, - } - }) - .collect() -} - pub fn find_directive_for_pos( directives: &[DirectiveEntry], pos: BytePos, @@ -241,161 +185,243 @@ pub fn find_directive_for_pos( } } -pub fn collect_lingui_directives_from_source( - source: &str, - start_line: usize, -) -> Vec { - let mut directives: Vec<(usize, bool, DirectiveUpdate)> = source - .lines() - .enumerate() - .filter_map(|(index, line)| { - let comment = extract_directive_comment(line)?; - match parse_lingui_directive_raw(comment) { - Ok(Some(parsed)) => Some((index + start_line, parsed.reset, parsed.values)), +#[cfg(test)] +pub fn collect_lingui_directives_from_comments(comments: &[Comment]) -> Vec { + let mut accumulated = DirectiveValues::default(); + + comments + .iter() + .filter_map( + |comment| match parse_lingui_directive_raw(comment.text.as_ref()) { + Ok(Some(parsed)) => { + Some(apply_directive(parsed, comment.span.lo, &mut accumulated)) + } Ok(None) => None, Err(message) => { - HANDLER.with(|handler| handler.struct_err(&message).emit()); + HANDLER.with(|handler| { + handler.struct_span_err(comment.span, &message).emit(); + }); None } - } - }) - .collect(); - - directives.sort_by_key(|directive| directive.0); + }, + ) + .collect() +} +pub fn collect_lingui_directives_from_source( + source: &str, + start_pos: BytePos, +) -> Vec { + let mut directives = Vec::new(); let mut accumulated = DirectiveValues::default(); - - directives - .into_iter() - .map(|(line, reset, values)| { - if reset { - accumulated = DirectiveValues::default(); + let bytes = source.as_bytes(); + let mut index = 0usize; + let mut mode = SourceScanMode::Code; + let mut template_expr_depths: Vec = vec![]; + + while index < bytes.len() { + match mode { + SourceScanMode::Code => match bytes[index] { + b'\'' => { + mode = SourceScanMode::SingleQuoted; + index += 1; + } + b'"' => { + mode = SourceScanMode::DoubleQuoted; + index += 1; + } + b'`' => { + mode = SourceScanMode::TemplateText; + index += 1; + } + b'/' if bytes.get(index + 1) == Some(&b'/') => { + let comment_start = BytePos(start_pos.0 + index as u32); + let content_start = index + 2; + index = content_start; + + while index < bytes.len() && bytes[index] != b'\n' { + index += 1; + } + + parse_source_directive( + source[content_start..index].trim(), + Span::new(comment_start, BytePos(start_pos.0 + index as u32)), + &mut accumulated, + &mut directives, + ); + } + b'/' if bytes.get(index + 1) == Some(&b'*') => { + let comment_start = BytePos(start_pos.0 + index as u32); + let content_start = index + 2; + index = content_start; + + while index + 1 < bytes.len() + && !(bytes[index] == b'*' && bytes[index + 1] == b'/') + { + index += 1; + } + + let content_end = index; + if index + 1 < bytes.len() { + index += 2; + } else { + index = bytes.len(); + } + + parse_block_directives( + &source[content_start..content_end], + comment_start, + &mut accumulated, + &mut directives, + ); + } + b'{' => { + if let Some(depth) = template_expr_depths.last_mut() { + *depth += 1; + } + index += 1; + } + b'}' => { + if let Some(depth) = template_expr_depths.last_mut() { + if *depth == 0 { + template_expr_depths.pop(); + mode = SourceScanMode::TemplateText; + } else { + *depth -= 1; + } + } + index += 1; + } + _ => { + index += 1; + } + }, + SourceScanMode::SingleQuoted => { + if bytes[index] == b'\\' { + index = (index + 2).min(bytes.len()); + } else { + if bytes[index] == b'\'' { + mode = SourceScanMode::Code; + } + index += 1; + } } - - accumulated.apply_update(values); - - DirectiveLineEntry { - line, - values: accumulated.clone(), + SourceScanMode::DoubleQuoted => { + if bytes[index] == b'\\' { + index = (index + 2).min(bytes.len()); + } else { + if bytes[index] == b'"' { + mode = SourceScanMode::Code; + } + index += 1; + } } - }) - .collect() -} - -pub fn find_directive_for_line( - directives: &[DirectiveLineEntry], - line: usize, -) -> Option<&DirectiveValues> { - if directives.is_empty() { - return None; - } - - let mut lo = 0usize; - let mut hi = directives.len(); - - while lo < hi { - let mid = (lo + hi) / 2; - if directives[mid].line <= line { - lo = mid + 1; - } else { - hi = mid; + SourceScanMode::TemplateText => match bytes[index] { + b'\\' => { + index = (index + 2).min(bytes.len()); + } + b'`' => { + mode = SourceScanMode::Code; + index += 1; + } + b'$' if bytes.get(index + 1) == Some(&b'{') => { + template_expr_depths.push(0); + mode = SourceScanMode::Code; + index += 2; + } + _ => { + index += 1; + } + }, } } - if lo == 0 { - None - } else { - Some(&directives[lo - 1].values) - } + directives } -fn extract_directive_comment(line: &str) -> Option<&str> { - let trimmed = line.trim(); - - if let Some(comment) = trimmed.strip_prefix("//") { - return Some(comment.trim()); - } +enum SourceScanMode { + Code, + SingleQuoted, + DoubleQuoted, + TemplateText, +} - if let Some(comment) = trimmed.strip_prefix("/*") { - return Some(comment.trim_end_matches("*/").trim()); +fn parse_source_directive( + text: &str, + span: Span, + accumulated: &mut DirectiveValues, + directives: &mut Vec, +) { + if !is_lingui_directive_prefix(text) { + return; } - if let Some(comment) = trimmed.strip_prefix('*') { - return Some(comment.trim_end_matches("*/").trim()); + match parse_lingui_directive_raw(text) { + Ok(Some(parsed)) => directives.push(apply_directive(parsed, span.lo, accumulated)), + Ok(None) => {} + Err(message) => { + HANDLER.with(|handler| handler.struct_span_err(span, &message).emit()); + } } - - None } -pub(crate) fn collect_lingui_directives( - node: &N, - comments: &Option, -) -> Vec -where - for<'a> N: VisitWith>, -{ - let Some(comments) = comments.as_ref() else { - return vec![]; +fn apply_directive( + parsed: ParsedDirective, + pos: BytePos, + accumulated: &mut DirectiveValues, +) -> DirectiveEntry { + let mut values = if parsed.reset { + DirectiveValues::default() + } else { + accumulated.clone() }; - let mut collector = DirectiveCollector::new(comments); - node.visit_with(&mut collector); - collect_lingui_directives_from_comments(&collector.comments) -} + values.apply_update(parsed.values); + *accumulated = values.clone(); -pub(crate) struct DirectiveCollector<'a, C> -where - C: Comments, -{ - comments_api: &'a C, - seen_positions: HashSet, - comments: Vec, + DirectiveEntry { pos, values } } -impl<'a, C> DirectiveCollector<'a, C> -where - C: Comments, -{ - fn new(comments_api: &'a C) -> Self { - Self { - comments_api, - seen_positions: HashSet::new(), - comments: vec![], - } - } - - fn record_for_span(&mut self, span: Span) { - if span.is_dummy() { - return; - } - - if let Some(comments) = self.comments_api.get_leading(span.lo()) { - for comment in comments { - if self.seen_positions.insert(comment.span.lo) { - self.comments.push(comment); - } - } +fn parse_block_directives( + content: &str, + comment_start: BytePos, + accumulated: &mut DirectiveValues, + directives: &mut Vec, +) { + let mut line_offset = 0u32; + + for segment in content.split_inclusive('\n') { + let line_with_cr = segment.strip_suffix('\n').unwrap_or(segment); + let line = line_with_cr.trim_end_matches('\r'); + let trimmed = line.trim_start(); + let leading_ws = (line.len() - trimmed.len()) as u32; + + if line_offset == 0 { + parse_source_directive( + trimmed, + Span::new( + comment_start, + BytePos(comment_start.0 + 2 + line_with_cr.len() as u32), + ), + accumulated, + directives, + ); + } else if let Some(after_star) = trimmed.strip_prefix('*') { + let text = after_star.trim_start(); + let marker_pos = BytePos(comment_start.0 + 2 + line_offset + leading_ws); + + parse_source_directive( + text, + Span::new( + marker_pos, + BytePos(comment_start.0 + 2 + line_offset + line_with_cr.len() as u32), + ), + accumulated, + directives, + ); } - } -} -impl Visit for DirectiveCollector<'_, C> -where - C: Comments, -{ - fn visit_expr(&mut self, expr: &Expr) { - self.record_for_span(expr.span()); - expr.visit_children_with(self); - } - - fn visit_module_item(&mut self, module_item: &ModuleItem) { - self.record_for_span(module_item.span()); - module_item.visit_children_with(self); - } - - fn visit_stmt(&mut self, stmt: &Stmt) { - self.record_for_span(stmt.span()); - stmt.visit_children_with(self); + line_offset += segment.len() as u32; } } @@ -403,6 +429,7 @@ where mod tests { use super::*; use swc_core::common::comments::CommentKind; + use swc_core::common::Span; fn make_comment(text: &str, lo: u32) -> Comment { Comment { @@ -470,6 +497,56 @@ mod tests { ); } + #[test] + fn collect_from_source_should_handle_crlf_block_comments() { + let directives = collect_lingui_directives_from_source( + "/* lingui-set context=\"ctx\" */\r\nconst msg = t`Hello`;\r\n", + BytePos(10), + ); + + assert_eq!( + directives, + vec![DirectiveEntry { + pos: BytePos(10), + values: DirectiveValues { + context: Some("ctx".into()), + comment: None, + id_prefix: None, + }, + }] + ); + } + + #[test] + fn collect_from_source_should_ignore_template_text_that_looks_like_comment() { + let directives = collect_lingui_directives_from_source( + "const msg = `\n// lingui-set context=\"ctx\"\n`;\n", + BytePos(10), + ); + + assert_eq!(directives, vec![]); + } + + #[test] + fn collect_from_source_should_support_starred_block_comment_lines() { + let directives = collect_lingui_directives_from_source( + "/**\n * lingui-set context=\"ctx\"\n */\nconst msg = t`Hello`;\n", + BytePos(10), + ); + + assert_eq!( + directives, + vec![DirectiveEntry { + pos: BytePos(15), + values: DirectiveValues { + context: Some("ctx".into()), + comment: None, + id_prefix: None, + }, + }] + ); + } + #[test] fn collect_should_merge_and_reset_directives() { let directives = collect_lingui_directives_from_comments(&[ diff --git a/src/lib.rs b/src/lib.rs index 201bad1..5b42e48 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,5 @@ use std::collections::HashSet; -use swc_core::common::{SourceMapper, Span, Spanned, SyntaxContext, DUMMY_SP}; +use swc_core::common::{BytePos, SourceMapper, Span, Spanned, SyntaxContext, DUMMY_SP}; use swc_core::common::comments::*; use swc_core::ecma::utils::private_ident; @@ -11,8 +11,9 @@ use swc_core::{ visit::{Fold, FoldWith, VisitWith}, }, plugin::{ - metadata::TransformPluginMetadataContextKind, plugin_transform, - proxies::TransformPluginProgramMetadata, + metadata::TransformPluginMetadataContextKind, + plugin_transform, + proxies::{PluginSourceMapProxy, TransformPluginProgramMetadata}, }, }; @@ -31,7 +32,7 @@ use crate::macro_utils::*; use crate::options::*; use ast_utils::*; use builder::*; -use comment_directive::{collect_lingui_directives, collect_lingui_directives_from_source}; +use comment_directive::collect_lingui_directives_from_source; use js_macro_folder::JsMacroFolder; use jsx_visitor::TransJSXVisitor; @@ -52,6 +53,11 @@ impl Fold for IdentReplacer { } } +pub enum DirectiveSource { + Text { start_pos: BytePos, source: String }, + SourceMap(PluginSourceMapProxy), +} + pub struct LinguiMacroFolder where C: Comments + Clone, @@ -59,6 +65,7 @@ where has_lingui_macro_imports: bool, ctx: MacroCtx, comments: Option, + directive_source: Option, } impl LinguiMacroFolder @@ -70,6 +77,48 @@ where has_lingui_macro_imports: false, ctx: MacroCtx::new(options), comments, + directive_source: None, + } + } + + pub fn with_directive_source( + mut self, + directive_source: DirectiveSource, + ) -> LinguiMacroFolder { + self.directive_source = Some(directive_source); + self + } + + fn ensure_source_directives(&mut self, module_items: &[ModuleItem]) { + if !self.ctx.directives.is_empty() { + return; + } + + let Some(directive_source) = self.directive_source.take() else { + return; + }; + + match directive_source { + DirectiveSource::Text { start_pos, source } => { + self.ctx + .set_directives(collect_lingui_directives_from_source(&source, start_pos)); + } + DirectiveSource::SourceMap(source_map) => { + let Some(last_item) = module_items.last() else { + return; + }; + + let file = source_map.lookup_char_pos(module_items[0].span().lo).file; + let file_span = Span::new(file.start_pos, last_item.span().hi); + + if let Ok(source) = source_map.span_to_snippet(file_span) { + self.ctx + .set_directives(collect_lingui_directives_from_source( + &source, + file_span.lo, + )); + } + } } } @@ -388,8 +437,7 @@ where return n; } - self.ctx - .set_comment_directives(collect_lingui_directives(&n, &self.comments)); + self.ensure_source_directives(&n); let mut insert_index: usize = 0; let mut index = 0; @@ -562,37 +610,8 @@ pub fn process_transform(program: Program, metadata: TransformPluginProgramMetad .unwrap_or_default(), ); - let mut folder = LinguiMacroFolder::new(config, metadata.comments); - let program_span = program.span(); - let file = metadata.source_map.lookup_char_pos(program_span.lo).file; - let file_span = Span::new(file.start_pos, program_span.hi); - - if let Ok(source) = metadata.source_map.span_to_snippet(file_span) { - let start_line = metadata.source_map.lookup_char_pos(file_span.lo).line; - let line_starts = collect_line_starts(file_span.lo, &source); - let directives = collect_lingui_directives_from_source(&source, start_line); - - folder - .ctx - .set_source_directives(directives, line_starts, start_line); - } + let mut folder = LinguiMacroFolder::new(config, metadata.comments) + .with_directive_source(DirectiveSource::SourceMap(metadata.source_map)); program.fold_with(&mut folder) } - -fn collect_line_starts( - start: swc_core::common::BytePos, - source: &str, -) -> Vec { - let mut line_starts = vec![start]; - let mut offset = start.0; - - for byte in source.bytes() { - offset += 1; - if byte == b'\n' { - line_starts.push(swc_core::common::BytePos(offset)); - } - } - - line_starts -} diff --git a/src/macro_utils.rs b/src/macro_utils.rs index a0be0d3..502ef82 100644 --- a/src/macro_utils.rs +++ b/src/macro_utils.rs @@ -1,8 +1,5 @@ use crate::ast_utils::*; -use crate::comment_directive::{ - find_directive_for_line, find_directive_for_pos, DirectiveEntry, DirectiveLineEntry, - DirectiveValues, -}; +use crate::comment_directive::{find_directive_for_pos, DirectiveEntry, DirectiveValues}; use crate::tokens::*; use crate::LinguiOptions; use std::collections::{HashMap, HashSet}; @@ -40,10 +37,7 @@ pub struct MacroCtx { pub should_add_uselingui_import: bool, pub options: LinguiOptions, - pub comment_directives: Vec, - pub source_directives: Vec, - pub source_line_starts: Vec, - pub source_start_line: usize, + pub directives: Vec, pub runtime_idents: RuntimeIdents, } @@ -72,50 +66,12 @@ impl MacroCtx { } } - pub fn set_comment_directives(&mut self, directives: Vec) { - self.comment_directives = directives; - } - - pub fn set_source_directives( - &mut self, - directives: Vec, - line_starts: Vec, - start_line: usize, - ) { - self.source_directives = directives; - self.source_line_starts = line_starts; - self.source_start_line = start_line; + pub fn set_directives(&mut self, directives: Vec) { + self.directives = directives; } pub fn get_comment_directive(&self, pos: BytePos) -> Option<&DirectiveValues> { - find_directive_for_pos(&self.comment_directives, pos) - .or_else(|| self.get_source_directive(pos)) - } - - fn get_source_directive(&self, pos: BytePos) -> Option<&DirectiveValues> { - let line = self.line_for_pos(pos)?; - find_directive_for_line(&self.source_directives, line) - } - - fn line_for_pos(&self, pos: BytePos) -> Option { - let first = *self.source_line_starts.first()?; - if pos < first { - return None; - } - - let mut lo = 0usize; - let mut hi = self.source_line_starts.len(); - - while lo < hi { - let mid = (lo + hi) / 2; - if self.source_line_starts[mid] <= pos { - lo = mid + 1; - } else { - hi = mid; - } - } - - Some(self.source_start_line + lo.saturating_sub(1)) + find_directive_for_pos(&self.directives, pos) } /// is given ident exported from @lingui/macro? and one of choice functions? diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 9da2c26..f426737 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -4,7 +4,7 @@ use std::sync::{Arc, Mutex}; use swc_core::common::comments::SingleThreadedComments; use swc_core::common::errors::{EmitterWriter, Handler, HANDLER}; use swc_core::common::sync::Lrc; -use swc_core::common::{FileName, Globals, Mark, SourceMap, GLOBALS}; +use swc_core::common::{BytePos, FileName, Globals, Mark, SourceMap, GLOBALS}; use swc_core::ecma::ast::Pass; use swc_core::ecma::codegen::to_code_default; use swc_core::ecma::parser::{Parser, StringInput, Syntax, TsSyntax}; @@ -23,7 +23,7 @@ impl Write for SharedWriter { pub fn transform( input: &str, - transform_cb: impl FnOnce(&SingleThreadedComments) -> P, + transform_cb: impl FnOnce(&SingleThreadedComments, BytePos) -> P, ) -> Result { let error_buffer = Arc::new(Mutex::new(Vec::new())); let cm: Lrc = Default::default(); @@ -66,7 +66,7 @@ pub fn transform( let program = program .apply(resolver(Mark::new(), Mark::new(), true)) - .apply(transform_cb(&comments)) + .apply(transform_cb(&comments, fm.start_pos)) .apply(hygiene::hygiene()) .apply(fixer::fixer(Some(&comments))); @@ -129,11 +129,17 @@ macro_rules! to { #[test] fn $name() { let source = common::dedent($input); - let output = common::transform(source.as_str(), |comments| { - swc_core::ecma::visit::fold_pass(lingui_macro_plugin::LinguiMacroFolder::new( - Default::default(), - Some(comments.clone()), - )) + let output = common::transform(source.as_str(), |comments, start_pos| { + swc_core::ecma::visit::fold_pass( + lingui_macro_plugin::LinguiMacroFolder::new( + Default::default(), + Some(comments.clone()), + ) + .with_directive_source(lingui_macro_plugin::DirectiveSource::Text { + start_pos, + source: source.clone(), + }), + ) }) .expect("Transform produced unexpected errors"); insta::with_settings!({ @@ -149,11 +155,17 @@ macro_rules! to { let options: lingui_macro_plugin::LinguiOptions = $options; let source = common::dedent($input); - let output = common::transform(source.as_str(), |comments| { - swc_core::ecma::visit::fold_pass(lingui_macro_plugin::LinguiMacroFolder::new( - options.clone(), - Some(comments.clone()), - )) + let output = common::transform(source.as_str(), |comments, start_pos| { + swc_core::ecma::visit::fold_pass( + lingui_macro_plugin::LinguiMacroFolder::new( + options.clone(), + Some(comments.clone()), + ) + .with_directive_source(lingui_macro_plugin::DirectiveSource::Text { + start_pos, + source: source.clone(), + }), + ) }) .expect("Transform produced unexpected errors"); insta::with_settings!({ @@ -173,11 +185,17 @@ macro_rules! to_panic { fn $name() { let options: lingui_macro_plugin::LinguiOptions = $options; let source = common::dedent($input); - let err = common::transform(source.as_str(), |comments| { - swc_core::ecma::visit::fold_pass(lingui_macro_plugin::LinguiMacroFolder::new( - options.clone(), - Some(comments.clone()), - )) + let err = common::transform(source.as_str(), |comments, start_pos| { + swc_core::ecma::visit::fold_pass( + lingui_macro_plugin::LinguiMacroFolder::new( + options.clone(), + Some(comments.clone()), + ) + .with_directive_source(lingui_macro_plugin::DirectiveSource::Text { + start_pos, + source: source.clone(), + }), + ) }) .expect_err("Expected transform to produce an error, but it succeeded"); insta::with_settings!({ From 5816413967d5b31aa3f2be7010a93d68e09c00e2 Mon Sep 17 00:00:00 2001 From: iatsenko <1586852+timofei-iatsenko@users.noreply.github.com> Date: Mon, 8 Jun 2026 16:54:57 +0200 Subject: [PATCH 04/10] clean up --- src/comment_directive.rs | 99 +++++++++++----------------------------- src/lib.rs | 4 +- 2 files changed, 30 insertions(+), 73 deletions(-) diff --git a/src/comment_directive.rs b/src/comment_directive.rs index 0ff8cdb..c2b60af 100644 --- a/src/comment_directive.rs +++ b/src/comment_directive.rs @@ -1,7 +1,5 @@ use once_cell::sync::Lazy; use regex::Regex; -#[cfg(test)] -use swc_core::common::comments::Comment; use swc_core::common::{BytePos, Span}; use swc_core::plugin::errors::HANDLER; @@ -78,20 +76,7 @@ fn parse_value_update(value: &str) -> DirectiveValueUpdate { } } -#[cfg(test)] -pub(crate) fn parse_lingui_directive( - comment_value: &str, -) -> Result, String> { - parse_lingui_directive_raw(comment_value).map(|parsed| { - parsed.map(|parsed| { - let mut values = DirectiveValues::default(); - values.apply_update(parsed.values); - (parsed.reset, values) - }) - }) -} - -fn parse_lingui_directive_raw(comment_value: &str) -> Result, String> { +fn parse_lingui_directive(comment_value: &str) -> Result, String> { let trimmed = comment_value.trim(); let Some(directive_match) = DIRECTIVE_RE.captures(trimmed) else { @@ -185,29 +170,6 @@ pub fn find_directive_for_pos( } } -#[cfg(test)] -pub fn collect_lingui_directives_from_comments(comments: &[Comment]) -> Vec { - let mut accumulated = DirectiveValues::default(); - - comments - .iter() - .filter_map( - |comment| match parse_lingui_directive_raw(comment.text.as_ref()) { - Ok(Some(parsed)) => { - Some(apply_directive(parsed, comment.span.lo, &mut accumulated)) - } - Ok(None) => None, - Err(message) => { - HANDLER.with(|handler| { - handler.struct_span_err(comment.span, &message).emit(); - }); - None - } - }, - ) - .collect() -} - pub fn collect_lingui_directives_from_source( source: &str, start_pos: BytePos, @@ -356,7 +318,7 @@ fn parse_source_directive( return; } - match parse_lingui_directive_raw(text) { + match parse_lingui_directive(text) { Ok(Some(parsed)) => directives.push(apply_directive(parsed, span.lo, accumulated)), Ok(None) => {} Err(message) => { @@ -428,16 +390,6 @@ fn parse_block_directives( #[cfg(test)] mod tests { use super::*; - use swc_core::common::comments::CommentKind; - use swc_core::common::Span; - - fn make_comment(text: &str, lo: u32) -> Comment { - Comment { - kind: CommentKind::Block, - span: Span::new(BytePos(lo), BytePos(lo + text.len() as u32)), - text: text.into(), - } - } #[test] fn parse_should_parse_multiple_keys() { @@ -447,14 +399,14 @@ mod tests { assert_eq!( parsed, - Some(( - false, - DirectiveValues { - context: Some("ctx".into()), - comment: Some("cmt".into()), - id_prefix: Some("p.".into()), + Some(ParsedDirective { + reset: false, + values: DirectiveUpdate { + context: Some(DirectiveValueUpdate::Set("ctx".into())), + comment: Some(DirectiveValueUpdate::Set("cmt".into())), + id_prefix: Some(DirectiveValueUpdate::Set("p.".into())), } - )) + }) ); } @@ -486,14 +438,14 @@ mod tests { assert_eq!( parsed, - Some(( - false, - DirectiveValues { - context: None, - comment: Some("note".into()), + Some(ParsedDirective { + reset: false, + values: DirectiveUpdate { + context: Some(DirectiveValueUpdate::Unset), + comment: Some(DirectiveValueUpdate::Set("note".into())), id_prefix: None, } - )) + }) ); } @@ -549,18 +501,21 @@ mod tests { #[test] fn collect_should_merge_and_reset_directives() { - let directives = collect_lingui_directives_from_comments(&[ - make_comment(r#" lingui-set context="ctx1" "#, 10), - make_comment(" not a directive", 20), - make_comment(r#" lingui-set comment="cmt" "#, 30), - make_comment(r#" lingui-reset context="ctx2" "#, 40), - ]); + let directives = collect_lingui_directives_from_source( + r#" + // lingui-set context="ctx1" + // not a directive + // lingui-set comment="cmt" + // lingui-reset context="ctx2" + "#, + BytePos(10), + ); assert_eq!( directives, vec![ DirectiveEntry { - pos: BytePos(10), + pos: BytePos(17), values: DirectiveValues { context: Some("ctx1".into()), comment: None, @@ -568,7 +523,7 @@ mod tests { }, }, DirectiveEntry { - pos: BytePos(30), + pos: BytePos(77), values: DirectiveValues { context: Some("ctx1".into()), comment: Some("cmt".into()), @@ -576,7 +531,7 @@ mod tests { }, }, DirectiveEntry { - pos: BytePos(40), + pos: BytePos(111), values: DirectiveValues { context: Some("ctx2".into()), comment: None, diff --git a/src/lib.rs b/src/lib.rs index 78c2fda..4c978aa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -107,7 +107,9 @@ where return; }; - let file = source_map.lookup_char_pos(module_items[0].span().lo).file; + let file = source_map + .lookup_char_pos(module_items.first().span().lo) + .file; let file_span = Span::new(file.start_pos, last_item.span().hi); if let Ok(source) = source_map.span_to_snippet(file_span) { From c876d7c93e39347f2a4675665707199a79d68b4e Mon Sep 17 00:00:00 2001 From: iatsenko <1586852+timofei-iatsenko@users.noreply.github.com> Date: Mon, 8 Jun 2026 17:13:57 +0200 Subject: [PATCH 05/10] create a convinient struct --- src/comment_directive.rs | 37 ++++++++++++++++++++++++++----------- src/lib.rs | 7 ++++--- src/macro_utils.rs | 8 ++++---- 3 files changed, 34 insertions(+), 18 deletions(-) diff --git a/src/comment_directive.rs b/src/comment_directive.rs index c2b60af..a33d73c 100644 --- a/src/comment_directive.rs +++ b/src/comment_directive.rs @@ -18,10 +18,15 @@ pub struct DirectiveValues { pub id_prefix: Option, } +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct LinguiCommentDirectives { + directives: Vec, +} + #[derive(Debug, Clone, PartialEq, Eq)] -pub struct DirectiveEntry { - pub pos: BytePos, - pub values: DirectiveValues, +struct DirectiveEntry { + pos: BytePos, + values: DirectiveValues, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -43,6 +48,22 @@ struct ParsedDirective { values: DirectiveUpdate, } +impl LinguiCommentDirectives { + pub fn from_source_text(source: &str, start_pos: BytePos) -> Self { + Self { + directives: collect_lingui_directives_from_source(source, start_pos), + } + } + + pub fn find_for_pos(&self, pos: BytePos) -> Option<&DirectiveValues> { + find_directive_for_pos(&self.directives, pos) + } + + pub fn is_empty(&self) -> bool { + self.directives.is_empty() + } +} + impl DirectiveValues { fn apply_update(&mut self, update: DirectiveUpdate) { if let Some(value) = update.context { @@ -143,10 +164,7 @@ fn parse_lingui_directive(comment_value: &str) -> Result Ok(Some(ParsedDirective { reset, values })) } -pub fn find_directive_for_pos( - directives: &[DirectiveEntry], - pos: BytePos, -) -> Option<&DirectiveValues> { +fn find_directive_for_pos(directives: &[DirectiveEntry], pos: BytePos) -> Option<&DirectiveValues> { if directives.is_empty() { return None; } @@ -170,10 +188,7 @@ pub fn find_directive_for_pos( } } -pub fn collect_lingui_directives_from_source( - source: &str, - start_pos: BytePos, -) -> Vec { +fn collect_lingui_directives_from_source(source: &str, start_pos: BytePos) -> Vec { let mut directives = Vec::new(); let mut accumulated = DirectiveValues::default(); let bytes = source.as_bytes(); diff --git a/src/lib.rs b/src/lib.rs index 4c978aa..0f36b25 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,7 +31,7 @@ use crate::generate_id::*; use crate::macro_utils::*; use ast_utils::*; use builder::*; -use comment_directive::collect_lingui_directives_from_source; +use comment_directive::LinguiCommentDirectives; use js_macro_folder::JsMacroFolder; use jsx_visitor::TransJSXVisitor; @@ -93,6 +93,7 @@ where return; } + let Some(directive_source) = self.directive_source.take() else { return; }; @@ -100,7 +101,7 @@ where match directive_source { DirectiveSource::Text { start_pos, source } => { self.ctx - .set_directives(collect_lingui_directives_from_source(&source, start_pos)); + .set_directives(LinguiCommentDirectives::from_source_text(&source, start_pos)); } DirectiveSource::SourceMap(source_map) => { let Some(last_item) = module_items.last() else { @@ -114,7 +115,7 @@ where if let Ok(source) = source_map.span_to_snippet(file_span) { self.ctx - .set_directives(collect_lingui_directives_from_source( + .set_directives(LinguiCommentDirectives::from_source_text( &source, file_span.lo, )); diff --git a/src/macro_utils.rs b/src/macro_utils.rs index 502ef82..3bd8e12 100644 --- a/src/macro_utils.rs +++ b/src/macro_utils.rs @@ -1,5 +1,5 @@ use crate::ast_utils::*; -use crate::comment_directive::{find_directive_for_pos, DirectiveEntry, DirectiveValues}; +use crate::comment_directive::{DirectiveValues, LinguiCommentDirectives}; use crate::tokens::*; use crate::LinguiOptions; use std::collections::{HashMap, HashSet}; @@ -37,7 +37,7 @@ pub struct MacroCtx { pub should_add_uselingui_import: bool, pub options: LinguiOptions, - pub directives: Vec, + pub directives: LinguiCommentDirectives, pub runtime_idents: RuntimeIdents, } @@ -66,12 +66,12 @@ impl MacroCtx { } } - pub fn set_directives(&mut self, directives: Vec) { + pub fn set_directives(&mut self, directives: LinguiCommentDirectives) { self.directives = directives; } pub fn get_comment_directive(&self, pos: BytePos) -> Option<&DirectiveValues> { - find_directive_for_pos(&self.directives, pos) + self.directives.find_for_pos(pos) } /// is given ident exported from @lingui/macro? and one of choice functions? From 885b7d96aea36a7ed3e39f3faec45bce60d4f5f3 Mon Sep 17 00:00:00 2001 From: iatsenko <1586852+timofei-iatsenko@users.noreply.github.com> Date: Mon, 8 Jun 2026 18:14:27 +0200 Subject: [PATCH 06/10] separate mod --- .../mod.rs} | 159 ++------- src/comment_directive/source_scanner.rs | 335 ++++++++++++++++++ 2 files changed, 366 insertions(+), 128 deletions(-) rename src/{comment_directive.rs => comment_directive/mod.rs} (74%) create mode 100644 src/comment_directive/source_scanner.rs diff --git a/src/comment_directive.rs b/src/comment_directive/mod.rs similarity index 74% rename from src/comment_directive.rs rename to src/comment_directive/mod.rs index a33d73c..8056e2b 100644 --- a/src/comment_directive.rs +++ b/src/comment_directive/mod.rs @@ -1,5 +1,8 @@ +mod source_scanner; + use once_cell::sync::Lazy; use regex::Regex; +use source_scanner::{scan_source_comments, CommentKind}; use swc_core::common::{BytePos, Span}; use swc_core::plugin::errors::HANDLER; @@ -143,8 +146,8 @@ fn parse_lingui_directive(comment_value: &str) -> Result "idPrefix" => values.id_prefix = Some(update), _ => { return Err(format!( - "`{directive_name}` directive has unknown param \"{key}\". Valid params: context, comment, idPrefix" - )); + "`{directive_name}` directive has unknown param \"{key}\". Valid params: context, comment, idPrefix" + )); } } } @@ -157,8 +160,8 @@ fn parse_lingui_directive(comment_value: &str) -> Result if !has_params && !reset { return Err(format!( - "`{directive_name}` directive requires at least one param. Valid params: context, comment, idPrefix" - )); + "`{directive_name}` directive requires at least one param. Valid params: context, comment, idPrefix" + )); } Ok(Some(ParsedDirective { reset, values })) @@ -189,140 +192,40 @@ fn find_directive_for_pos(directives: &[DirectiveEntry], pos: BytePos) -> Option } fn collect_lingui_directives_from_source(source: &str, start_pos: BytePos) -> Vec { + if !source.contains("lingui-set") && !source.contains("lingui-reset") { + return vec![]; + } + let mut directives = Vec::new(); let mut accumulated = DirectiveValues::default(); - let bytes = source.as_bytes(); - let mut index = 0usize; - let mut mode = SourceScanMode::Code; - let mut template_expr_depths: Vec = vec![]; - - while index < bytes.len() { - match mode { - SourceScanMode::Code => match bytes[index] { - b'\'' => { - mode = SourceScanMode::SingleQuoted; - index += 1; - } - b'"' => { - mode = SourceScanMode::DoubleQuoted; - index += 1; - } - b'`' => { - mode = SourceScanMode::TemplateText; - index += 1; - } - b'/' if bytes.get(index + 1) == Some(&b'/') => { - let comment_start = BytePos(start_pos.0 + index as u32); - let content_start = index + 2; - index = content_start; - - while index < bytes.len() && bytes[index] != b'\n' { - index += 1; - } - - parse_source_directive( - source[content_start..index].trim(), - Span::new(comment_start, BytePos(start_pos.0 + index as u32)), - &mut accumulated, - &mut directives, - ); - } - b'/' if bytes.get(index + 1) == Some(&b'*') => { - let comment_start = BytePos(start_pos.0 + index as u32); - let content_start = index + 2; - index = content_start; - - while index + 1 < bytes.len() - && !(bytes[index] == b'*' && bytes[index + 1] == b'/') - { - index += 1; - } - - let content_end = index; - if index + 1 < bytes.len() { - index += 2; - } else { - index = bytes.len(); - } - - parse_block_directives( - &source[content_start..content_end], - comment_start, - &mut accumulated, - &mut directives, - ); - } - b'{' => { - if let Some(depth) = template_expr_depths.last_mut() { - *depth += 1; - } - index += 1; - } - b'}' => { - if let Some(depth) = template_expr_depths.last_mut() { - if *depth == 0 { - template_expr_depths.pop(); - mode = SourceScanMode::TemplateText; - } else { - *depth -= 1; - } - } - index += 1; - } - _ => { - index += 1; - } - }, - SourceScanMode::SingleQuoted => { - if bytes[index] == b'\\' { - index = (index + 2).min(bytes.len()); - } else { - if bytes[index] == b'\'' { - mode = SourceScanMode::Code; - } - index += 1; - } + + for comment in scan_source_comments(source) { + let comment_start = BytePos(start_pos.0 + comment.byte_offset as u32); + + match comment.kind { + CommentKind::Line => { + let content_end = BytePos(comment_start.0 + 2 + comment.content.len() as u32); + parse_source_directive( + comment.content.trim(), + Span::new(comment_start, content_end), + &mut accumulated, + &mut directives, + ); } - SourceScanMode::DoubleQuoted => { - if bytes[index] == b'\\' { - index = (index + 2).min(bytes.len()); - } else { - if bytes[index] == b'"' { - mode = SourceScanMode::Code; - } - index += 1; - } + CommentKind::Block => { + parse_block_directives( + comment.content, + comment_start, + &mut accumulated, + &mut directives, + ); } - SourceScanMode::TemplateText => match bytes[index] { - b'\\' => { - index = (index + 2).min(bytes.len()); - } - b'`' => { - mode = SourceScanMode::Code; - index += 1; - } - b'$' if bytes.get(index + 1) == Some(&b'{') => { - template_expr_depths.push(0); - mode = SourceScanMode::Code; - index += 2; - } - _ => { - index += 1; - } - }, } } directives } -enum SourceScanMode { - Code, - SingleQuoted, - DoubleQuoted, - TemplateText, -} - fn parse_source_directive( text: &str, span: Span, diff --git a/src/comment_directive/source_scanner.rs b/src/comment_directive/source_scanner.rs new file mode 100644 index 0000000..7f22dbc --- /dev/null +++ b/src/comment_directive/source_scanner.rs @@ -0,0 +1,335 @@ +pub struct SourceComment<'a> { + pub byte_offset: usize, + pub content: &'a str, + pub kind: CommentKind, +} + +pub enum CommentKind { + Line, + Block, +} + +pub fn scan_source_comments(source: &str) -> Vec> { + let bytes = source.as_bytes(); + let mut comments = Vec::new(); + let mut index = 0usize; + + while index < bytes.len() { + match bytes[index] { + b'\'' | b'"' => { + index = skip_string_literal(bytes, index); + } + b'`' => { + index = skip_template_literal(bytes, index); + } + b'/' if bytes.get(index + 1) == Some(&b'/') => { + let comment_start = index; + let content_start = index + 2; + index = content_start; + + while index < bytes.len() && bytes[index] != b'\n' { + index += 1; + } + + comments.push(SourceComment { + byte_offset: comment_start, + content: &source[content_start..index], + kind: CommentKind::Line, + }); + } + b'/' if bytes.get(index + 1) == Some(&b'*') => { + let comment_start = index; + let content_start = index + 2; + index = content_start; + + while index < bytes.len() { + if bytes[index] == b'*' && bytes.get(index + 1) == Some(&b'/') { + break; + } + index += 1; + } + + let content_end = index; + if index < bytes.len() { + index += 2; + } + + comments.push(SourceComment { + byte_offset: comment_start, + content: &source[content_start..content_end], + kind: CommentKind::Block, + }); + } + _ => { + index += 1; + } + } + } + + comments +} + +fn skip_string_literal(bytes: &[u8], start: usize) -> usize { + let delim = bytes[start]; + let mut i = start + 1; + while i < bytes.len() { + if bytes[i] == b'\\' { + i = (i + 2).min(bytes.len()); + } else if bytes[i] == delim { + return i + 1; + } else { + i += 1; + } + } + i +} + +fn skip_template_literal(bytes: &[u8], start: usize) -> usize { + let mut i = start + 1; + while i < bytes.len() { + if bytes[i] == b'\\' { + i = (i + 2).min(bytes.len()); + } else if bytes[i] == b'`' { + return i + 1; + } else { + i += 1; + } + } + i +} + +#[cfg(test)] +mod tests { + use super::*; + + fn line_comments(source: &str) -> Vec<(usize, &str)> { + scan_source_comments(source) + .into_iter() + .filter(|c| matches!(c.kind, CommentKind::Line)) + .map(|c| (c.byte_offset, c.content)) + .collect() + } + + fn block_comments(source: &str) -> Vec<(usize, &str)> { + scan_source_comments(source) + .into_iter() + .filter(|c| matches!(c.kind, CommentKind::Block)) + .map(|c| (c.byte_offset, c.content)) + .collect() + } + + fn all_comments(source: &str) -> Vec<(usize, &str)> { + scan_source_comments(source) + .into_iter() + .map(|c| (c.byte_offset, c.content)) + .collect() + } + + #[test] + fn empty_source() { + assert_eq!(all_comments(""), Vec::<(usize, &str)>::new()); + } + + #[test] + fn no_comments() { + assert_eq!(all_comments("const x = 1;\nlet y = 2;"), vec![]); + } + + #[test] + fn single_line_comment() { + assert_eq!( + line_comments("// hello world"), + vec![(0, " hello world")] + ); + } + + #[test] + fn line_comment_after_code() { + assert_eq!( + line_comments("const x = 1; // inline"), + vec![(13, " inline")] + ); + } + + #[test] + fn multiple_line_comments() { + let source = "// first\n// second\ncode\n// third"; + assert_eq!( + line_comments(source), + vec![(0, " first"), (9, " second"), (24, " third")] + ); + } + + #[test] + fn single_block_comment() { + assert_eq!( + block_comments("/* block */"), + vec![(0, " block ")] + ); + } + + #[test] + fn multiline_block_comment() { + let source = "/* line1\n line2 */"; + assert_eq!( + block_comments(source), + vec![(0, " line1\n line2 ")] + ); + } + + #[test] + fn block_comment_after_code() { + assert_eq!( + block_comments("x = 1; /* note */ y = 2;"), + vec![(7, " note ")] + ); + } + + #[test] + fn ignores_comment_syntax_in_single_quoted_string() { + assert_eq!(all_comments("const x = '// not a comment';"), vec![]); + assert_eq!(all_comments("const x = '/* not a comment */';"), vec![]); + } + + #[test] + fn ignores_comment_syntax_in_double_quoted_string() { + assert_eq!(all_comments(r#"const x = "// not a comment";"#), vec![]); + assert_eq!( + all_comments(r#"const x = "/* not a comment */";"#), + vec![] + ); + } + + #[test] + fn ignores_comment_syntax_in_template_literal() { + assert_eq!(all_comments("const x = `// not a comment`;"), vec![]); + assert_eq!(all_comments("const x = `/* not a comment */`;"), vec![]); + } + + #[test] + fn handles_escaped_quotes_in_single_quoted_string() { + assert_eq!(all_comments(r"const x = 'it\'s'; // after"), vec![(19, " after")]); + } + + #[test] + fn handles_escaped_quotes_in_double_quoted_string() { + assert_eq!( + all_comments(r#"const x = "say \"hi\""; // after"#), + vec![(24, " after")] + ); + } + + #[test] + fn handles_escaped_backtick_in_template_literal() { + assert_eq!( + all_comments(r"const x = `\`template\``; // after"), + vec![(26, " after")] + ); + } + + #[test] + fn handles_backslash_at_end_of_string() { + // String ending with escape at EOF (unterminated) + assert_eq!(all_comments(r"const x = '\"), vec![]); + } + + #[test] + fn handles_backslash_at_end_of_template() { + assert_eq!(all_comments("const x = `\\"), vec![]); + } + + #[test] + fn unterminated_single_quoted_string() { + // No closing quote — scanner shouldn't panic + assert_eq!(all_comments("const x = 'unterminated // nope"), vec![]); + } + + #[test] + fn unterminated_double_quoted_string() { + assert_eq!(all_comments(r#"const x = "unterminated // nope"#), vec![]); + } + + #[test] + fn unterminated_template_literal() { + assert_eq!(all_comments("const x = `unterminated // nope"), vec![]); + } + + #[test] + fn unterminated_block_comment() { + // Block comment that never closes — content runs to end + assert_eq!( + block_comments("/* never closed"), + vec![(0, " never closed")] + ); + } + + #[test] + fn mixed_comment_types() { + let source = "// line\n/* block */\ncode // inline"; + let comments = all_comments(source); + assert_eq!( + comments, + vec![(0, " line"), (8, " block "), (25, " inline")] + ); + } + + #[test] + fn slash_not_followed_by_slash_or_star() { + // Division operator should not be mistaken for comment + assert_eq!(all_comments("const x = 10 / 2;"), vec![]); + } + + #[test] + fn empty_line_comment() { + assert_eq!(line_comments("//\ncode"), vec![(0, "")]); + } + + #[test] + fn empty_block_comment() { + assert_eq!(block_comments("/**/"), vec![(0, "")]); + } + + #[test] + fn block_comment_with_star_inside() { + assert_eq!( + block_comments("/* a * b */"), + vec![(0, " a * b ")] + ); + } + + #[test] + fn consecutive_block_comments() { + // "/* a */" = 7 bytes, so second comment starts at offset 7 + assert_eq!( + block_comments("/* a *//* b */"), + vec![(0, " a "), (7, " b ")] + ); + } + + #[test] + fn line_comment_at_eof_without_newline() { + assert_eq!(line_comments("// eof"), vec![(0, " eof")]); + } + + #[test] + fn comment_after_template_literal_with_expressions() { + // Template with ${} — the simplified scanner treats it as text until closing backtick + let source = "const x = `hello ${world}`; // after"; + assert_eq!(line_comments(source), vec![(28, " after")]); + } + + #[test] + fn multiline_template_literal_with_comment_like_content() { + let source = "const x = `\n// fake\n/* also fake */\n`;\n// real"; + assert_eq!(line_comments(source), vec![(39, " real")]); + } + + #[test] + fn string_containing_backslash_n_is_not_newline() { + // The literal text \n in a string (escaped), not a real newline + assert_eq!( + all_comments("const x = '\\n'; // yes"), + vec![(16, " yes")] + ); + } +} From f88808d5b937dacb88c07a1c669f50051ea52bb1 Mon Sep 17 00:00:00 2001 From: iatsenko <1586852+timofei-iatsenko@users.noreply.github.com> Date: Mon, 8 Jun 2026 18:18:36 +0200 Subject: [PATCH 07/10] cargo fmt --- src/comment_directive/source_scanner.rs | 35 +++++++------------------ src/lib.rs | 5 ++-- 2 files changed, 13 insertions(+), 27 deletions(-) diff --git a/src/comment_directive/source_scanner.rs b/src/comment_directive/source_scanner.rs index 7f22dbc..94bd03f 100644 --- a/src/comment_directive/source_scanner.rs +++ b/src/comment_directive/source_scanner.rs @@ -137,10 +137,7 @@ mod tests { #[test] fn single_line_comment() { - assert_eq!( - line_comments("// hello world"), - vec![(0, " hello world")] - ); + assert_eq!(line_comments("// hello world"), vec![(0, " hello world")]); } #[test] @@ -162,19 +159,13 @@ mod tests { #[test] fn single_block_comment() { - assert_eq!( - block_comments("/* block */"), - vec![(0, " block ")] - ); + assert_eq!(block_comments("/* block */"), vec![(0, " block ")]); } #[test] fn multiline_block_comment() { let source = "/* line1\n line2 */"; - assert_eq!( - block_comments(source), - vec![(0, " line1\n line2 ")] - ); + assert_eq!(block_comments(source), vec![(0, " line1\n line2 ")]); } #[test] @@ -194,10 +185,7 @@ mod tests { #[test] fn ignores_comment_syntax_in_double_quoted_string() { assert_eq!(all_comments(r#"const x = "// not a comment";"#), vec![]); - assert_eq!( - all_comments(r#"const x = "/* not a comment */";"#), - vec![] - ); + assert_eq!(all_comments(r#"const x = "/* not a comment */";"#), vec![]); } #[test] @@ -208,7 +196,10 @@ mod tests { #[test] fn handles_escaped_quotes_in_single_quoted_string() { - assert_eq!(all_comments(r"const x = 'it\'s'; // after"), vec![(19, " after")]); + assert_eq!( + all_comments(r"const x = 'it\'s'; // after"), + vec![(19, " after")] + ); } #[test] @@ -291,10 +282,7 @@ mod tests { #[test] fn block_comment_with_star_inside() { - assert_eq!( - block_comments("/* a * b */"), - vec![(0, " a * b ")] - ); + assert_eq!(block_comments("/* a * b */"), vec![(0, " a * b ")]); } #[test] @@ -327,9 +315,6 @@ mod tests { #[test] fn string_containing_backslash_n_is_not_newline() { // The literal text \n in a string (escaped), not a real newline - assert_eq!( - all_comments("const x = '\\n'; // yes"), - vec![(16, " yes")] - ); + assert_eq!(all_comments("const x = '\\n'; // yes"), vec![(16, " yes")]); } } diff --git a/src/lib.rs b/src/lib.rs index 0f36b25..05641b5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -93,7 +93,6 @@ where return; } - let Some(directive_source) = self.directive_source.take() else { return; }; @@ -101,7 +100,9 @@ where match directive_source { DirectiveSource::Text { start_pos, source } => { self.ctx - .set_directives(LinguiCommentDirectives::from_source_text(&source, start_pos)); + .set_directives(LinguiCommentDirectives::from_source_text( + &source, start_pos, + )); } DirectiveSource::SourceMap(source_map) => { let Some(last_item) = module_items.last() else { From 51e3fe7b5d5c4935417dfa2007460a40e041e4d8 Mon Sep 17 00:00:00 2001 From: iatsenko <1586852+timofei-iatsenko@users.noreply.github.com> Date: Tue, 9 Jun 2026 09:15:03 +0200 Subject: [PATCH 08/10] simplify comments parsing --- src/comment_directive/mod.rs | 144 +++++++---------------------------- 1 file changed, 29 insertions(+), 115 deletions(-) diff --git a/src/comment_directive/mod.rs b/src/comment_directive/mod.rs index 8056e2b..8b45764 100644 --- a/src/comment_directive/mod.rs +++ b/src/comment_directive/mod.rs @@ -201,24 +201,37 @@ fn collect_lingui_directives_from_source(source: &str, start_pos: BytePos) -> Ve for comment in scan_source_comments(source) { let comment_start = BytePos(start_pos.0 + comment.byte_offset as u32); + let trimmed = comment.content.trim(); - match comment.kind { - CommentKind::Line => { - let content_end = BytePos(comment_start.0 + 2 + comment.content.len() as u32); - parse_source_directive( - comment.content.trim(), - Span::new(comment_start, content_end), - &mut accumulated, - &mut directives, - ); + if !is_lingui_directive_prefix(trimmed) { + continue; + } + + let content_end = match comment.kind { + CommentKind::Line => BytePos(comment_start.0 + 2 + comment.content.len() as u32), + CommentKind::Block => BytePos(comment_start.0 + 2 + comment.content.len() as u32 + 2), + }; + let span = Span::new(comment_start, content_end); + + match parse_lingui_directive(trimmed) { + Ok(Some(parsed)) => { + let mut values = if parsed.reset { + DirectiveValues::default() + } else { + accumulated.clone() + }; + + values.apply_update(parsed.values); + accumulated = values.clone(); + + directives.push(DirectiveEntry { + pos: comment_start, + values, + }) } - CommentKind::Block => { - parse_block_directives( - comment.content, - comment_start, - &mut accumulated, - &mut directives, - ); + Ok(None) => {} + Err(message) => { + HANDLER.with(|handler| handler.struct_span_err(span, &message).emit()); } } } @@ -226,85 +239,6 @@ fn collect_lingui_directives_from_source(source: &str, start_pos: BytePos) -> Ve directives } -fn parse_source_directive( - text: &str, - span: Span, - accumulated: &mut DirectiveValues, - directives: &mut Vec, -) { - if !is_lingui_directive_prefix(text) { - return; - } - - match parse_lingui_directive(text) { - Ok(Some(parsed)) => directives.push(apply_directive(parsed, span.lo, accumulated)), - Ok(None) => {} - Err(message) => { - HANDLER.with(|handler| handler.struct_span_err(span, &message).emit()); - } - } -} - -fn apply_directive( - parsed: ParsedDirective, - pos: BytePos, - accumulated: &mut DirectiveValues, -) -> DirectiveEntry { - let mut values = if parsed.reset { - DirectiveValues::default() - } else { - accumulated.clone() - }; - - values.apply_update(parsed.values); - *accumulated = values.clone(); - - DirectiveEntry { pos, values } -} - -fn parse_block_directives( - content: &str, - comment_start: BytePos, - accumulated: &mut DirectiveValues, - directives: &mut Vec, -) { - let mut line_offset = 0u32; - - for segment in content.split_inclusive('\n') { - let line_with_cr = segment.strip_suffix('\n').unwrap_or(segment); - let line = line_with_cr.trim_end_matches('\r'); - let trimmed = line.trim_start(); - let leading_ws = (line.len() - trimmed.len()) as u32; - - if line_offset == 0 { - parse_source_directive( - trimmed, - Span::new( - comment_start, - BytePos(comment_start.0 + 2 + line_with_cr.len() as u32), - ), - accumulated, - directives, - ); - } else if let Some(after_star) = trimmed.strip_prefix('*') { - let text = after_star.trim_start(); - let marker_pos = BytePos(comment_start.0 + 2 + line_offset + leading_ws); - - parse_source_directive( - text, - Span::new( - marker_pos, - BytePos(comment_start.0 + 2 + line_offset + line_with_cr.len() as u32), - ), - accumulated, - directives, - ); - } - - line_offset += segment.len() as u32; - } -} - #[cfg(test)] mod tests { use super::*; @@ -397,26 +331,6 @@ mod tests { assert_eq!(directives, vec![]); } - #[test] - fn collect_from_source_should_support_starred_block_comment_lines() { - let directives = collect_lingui_directives_from_source( - "/**\n * lingui-set context=\"ctx\"\n */\nconst msg = t`Hello`;\n", - BytePos(10), - ); - - assert_eq!( - directives, - vec![DirectiveEntry { - pos: BytePos(15), - values: DirectiveValues { - context: Some("ctx".into()), - comment: None, - id_prefix: None, - }, - }] - ); - } - #[test] fn collect_should_merge_and_reset_directives() { let directives = collect_lingui_directives_from_source( From 03f0fad2bcddc55c6c14dc712e4b7381e6d62563 Mon Sep 17 00:00:00 2001 From: iatsenko <1586852+timofei-iatsenko@users.noreply.github.com> Date: Tue, 9 Jun 2026 09:31:34 +0200 Subject: [PATCH 09/10] remove `with_directive_source` in favour of passing this to constructor --- src/lib.rs | 33 ++++++++++++++------------------- tests/common/mod.rs | 20 +++++++++++--------- 2 files changed, 25 insertions(+), 28 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 05641b5..caf41fc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -64,44 +64,36 @@ where has_lingui_macro_imports: bool, ctx: MacroCtx, comments: Option, - directive_source: Option, + directive_source: DirectiveSource, } impl LinguiMacroFolder where C: Comments + Clone, { - pub fn new(options: LinguiOptions, comments: Option) -> LinguiMacroFolder { + pub fn new( + options: LinguiOptions, + comments: Option, + directive_source: DirectiveSource, + ) -> LinguiMacroFolder { LinguiMacroFolder { has_lingui_macro_imports: false, ctx: MacroCtx::new(options), comments, - directive_source: None, + directive_source, } } - pub fn with_directive_source( - mut self, - directive_source: DirectiveSource, - ) -> LinguiMacroFolder { - self.directive_source = Some(directive_source); - self - } - fn ensure_source_directives(&mut self, module_items: &[ModuleItem]) { if !self.ctx.directives.is_empty() { return; } - let Some(directive_source) = self.directive_source.take() else { - return; - }; - - match directive_source { + match &self.directive_source { DirectiveSource::Text { start_pos, source } => { self.ctx .set_directives(LinguiCommentDirectives::from_source_text( - &source, start_pos, + source, *start_pos, )); } DirectiveSource::SourceMap(source_map) => { @@ -616,8 +608,11 @@ pub fn process_transform(program: Program, metadata: TransformPluginProgramMetad .unwrap_or_default(), ); - let mut folder = LinguiMacroFolder::new(config, metadata.comments) - .with_directive_source(DirectiveSource::SourceMap(metadata.source_map)); + let mut folder = LinguiMacroFolder::new( + config, + metadata.comments, + DirectiveSource::SourceMap(metadata.source_map), + ); program.fold_with(&mut folder) } diff --git a/tests/common/mod.rs b/tests/common/mod.rs index f426737..e048671 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -134,11 +134,12 @@ macro_rules! to { lingui_macro_plugin::LinguiMacroFolder::new( Default::default(), Some(comments.clone()), - ) - .with_directive_source(lingui_macro_plugin::DirectiveSource::Text { + lingui_macro_plugin::DirectiveSource::Text { start_pos, source: source.clone(), - }), + } + ) + ) }) .expect("Transform produced unexpected errors"); @@ -160,11 +161,12 @@ macro_rules! to { lingui_macro_plugin::LinguiMacroFolder::new( options.clone(), Some(comments.clone()), - ) - .with_directive_source(lingui_macro_plugin::DirectiveSource::Text { + lingui_macro_plugin::DirectiveSource::Text { start_pos, source: source.clone(), - }), + } + ) + ) }) .expect("Transform produced unexpected errors"); @@ -190,11 +192,11 @@ macro_rules! to_panic { lingui_macro_plugin::LinguiMacroFolder::new( options.clone(), Some(comments.clone()), - ) - .with_directive_source(lingui_macro_plugin::DirectiveSource::Text { + lingui_macro_plugin::DirectiveSource::Text { start_pos, source: source.clone(), - }), + }) + ) }) .expect_err("Expected transform to produce an error, but it succeeded"); From 62d5ecedc725e903663f2ad93c3dddb16c5b364c Mon Sep 17 00:00:00 2001 From: iatsenko <1586852+timofei-iatsenko@users.noreply.github.com> Date: Tue, 9 Jun 2026 10:16:22 +0200 Subject: [PATCH 10/10] get rid of differences DirectiveSource enum and differences between test/runtime --- src/lib.rs | 63 +++++++++++++++++---------------------------- tests/common/mod.rs | 31 +++++++--------------- 2 files changed, 33 insertions(+), 61 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index caf41fc..078974c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,6 @@ use std::collections::HashSet; -use swc_core::common::{BytePos, SourceMapper, Span, Spanned, SyntaxContext, DUMMY_SP}; +use swc_core::common::sync::Lrc; +use swc_core::common::{SourceMapper, Span, Spanned, SyntaxContext, DUMMY_SP}; use swc_core::common::comments::*; use swc_core::ecma::utils::private_ident; @@ -11,9 +12,8 @@ use swc_core::{ visit::{Fold, FoldWith, VisitWith}, }, plugin::{ - metadata::TransformPluginMetadataContextKind, - plugin_transform, - proxies::{PluginSourceMapProxy, TransformPluginProgramMetadata}, + metadata::TransformPluginMetadataContextKind, plugin_transform, + proxies::TransformPluginProgramMetadata, }, }; @@ -52,11 +52,6 @@ impl Fold for IdentReplacer { } } -pub enum DirectiveSource { - Text { start_pos: BytePos, source: String }, - SourceMap(PluginSourceMapProxy), -} - pub struct LinguiMacroFolder where C: Comments + Clone, @@ -64,7 +59,7 @@ where has_lingui_macro_imports: bool, ctx: MacroCtx, comments: Option, - directive_source: DirectiveSource, + source_map: Lrc, } impl LinguiMacroFolder @@ -74,46 +69,34 @@ where pub fn new( options: LinguiOptions, comments: Option, - directive_source: DirectiveSource, + source_map: Lrc, ) -> LinguiMacroFolder { LinguiMacroFolder { has_lingui_macro_imports: false, ctx: MacroCtx::new(options), comments, - directive_source, + source_map, } } - fn ensure_source_directives(&mut self, module_items: &[ModuleItem]) { + fn ensure_source_directives(&mut self, module_span: Span) { if !self.ctx.directives.is_empty() { return; } - match &self.directive_source { - DirectiveSource::Text { start_pos, source } => { - self.ctx - .set_directives(LinguiCommentDirectives::from_source_text( - source, *start_pos, - )); - } - DirectiveSource::SourceMap(source_map) => { - let Some(last_item) = module_items.last() else { - return; - }; - - let file = source_map - .lookup_char_pos(module_items.first().span().lo) - .file; - let file_span = Span::new(file.start_pos, last_item.span().hi); - - if let Ok(source) = source_map.span_to_snippet(file_span) { - self.ctx - .set_directives(LinguiCommentDirectives::from_source_text( - &source, - file_span.lo, - )); - } - } + if module_span.is_dummy() { + return; + } + + let file = self.source_map.lookup_char_pos(module_span.lo).file; + let file_span = Span::new(file.start_pos, module_span.hi); + + if let Ok(source) = self.source_map.span_to_snippet(file_span) { + self.ctx + .set_directives(LinguiCommentDirectives::from_source_text( + &source, + file_span.lo, + )); } } @@ -427,7 +410,7 @@ where return node; } - self.ensure_source_directives(&node.body); + self.ensure_source_directives(node.span); let mut insert_index: usize = 0; let mut index = 0; @@ -611,7 +594,7 @@ pub fn process_transform(program: Program, metadata: TransformPluginProgramMetad let mut folder = LinguiMacroFolder::new( config, metadata.comments, - DirectiveSource::SourceMap(metadata.source_map), + Lrc::new(metadata.source_map) as Lrc, ); program.fold_with(&mut folder) diff --git a/tests/common/mod.rs b/tests/common/mod.rs index e048671..fb326cd 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -4,7 +4,7 @@ use std::sync::{Arc, Mutex}; use swc_core::common::comments::SingleThreadedComments; use swc_core::common::errors::{EmitterWriter, Handler, HANDLER}; use swc_core::common::sync::Lrc; -use swc_core::common::{BytePos, FileName, Globals, Mark, SourceMap, GLOBALS}; +use swc_core::common::{FileName, Globals, Mark, SourceMap, GLOBALS}; use swc_core::ecma::ast::Pass; use swc_core::ecma::codegen::to_code_default; use swc_core::ecma::parser::{Parser, StringInput, Syntax, TsSyntax}; @@ -23,7 +23,7 @@ impl Write for SharedWriter { pub fn transform( input: &str, - transform_cb: impl FnOnce(&SingleThreadedComments, BytePos) -> P, + transform_cb: impl FnOnce(&SingleThreadedComments, Lrc) -> P, ) -> Result { let error_buffer = Arc::new(Mutex::new(Vec::new())); let cm: Lrc = Default::default(); @@ -66,7 +66,7 @@ pub fn transform( let program = program .apply(resolver(Mark::new(), Mark::new(), true)) - .apply(transform_cb(&comments, fm.start_pos)) + .apply(transform_cb(&comments, cm.clone())) .apply(hygiene::hygiene()) .apply(fixer::fixer(Some(&comments))); @@ -129,17 +129,13 @@ macro_rules! to { #[test] fn $name() { let source = common::dedent($input); - let output = common::transform(source.as_str(), |comments, start_pos| { + let output = common::transform(source.as_str(), |comments, cm| { swc_core::ecma::visit::fold_pass( lingui_macro_plugin::LinguiMacroFolder::new( Default::default(), Some(comments.clone()), - lingui_macro_plugin::DirectiveSource::Text { - start_pos, - source: source.clone(), - } + cm as swc_core::common::sync::Lrc, ) - ) }) .expect("Transform produced unexpected errors"); @@ -156,17 +152,13 @@ macro_rules! to { let options: lingui_macro_plugin::LinguiOptions = $options; let source = common::dedent($input); - let output = common::transform(source.as_str(), |comments, start_pos| { + let output = common::transform(source.as_str(), |comments, cm| { swc_core::ecma::visit::fold_pass( lingui_macro_plugin::LinguiMacroFolder::new( options.clone(), Some(comments.clone()), - lingui_macro_plugin::DirectiveSource::Text { - start_pos, - source: source.clone(), - } + cm as swc_core::common::sync::Lrc, ) - ) }) .expect("Transform produced unexpected errors"); @@ -187,16 +179,13 @@ macro_rules! to_panic { fn $name() { let options: lingui_macro_plugin::LinguiOptions = $options; let source = common::dedent($input); - let err = common::transform(source.as_str(), |comments, start_pos| { + let err = common::transform(source.as_str(), |comments, cm| { swc_core::ecma::visit::fold_pass( lingui_macro_plugin::LinguiMacroFolder::new( options.clone(), Some(comments.clone()), - lingui_macro_plugin::DirectiveSource::Text { - start_pos, - source: source.clone(), - }) - + cm as swc_core::common::sync::Lrc, + ) ) }) .expect_err("Expected transform to produce an error, but it succeeded");