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"') + }) }) diff --git a/src/comment_directive.rs b/src/comment_directive/mod.rs similarity index 60% rename from src/comment_directive.rs rename to src/comment_directive/mod.rs index f1de874..8b45764 100644 --- a/src/comment_directive.rs +++ b/src/comment_directive/mod.rs @@ -1,16 +1,19 @@ +mod source_scanner; + 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}; +use source_scanner::{scan_source_comments, CommentKind}; +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, @@ -18,10 +21,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,11 +51,20 @@ struct ParsedDirective { values: DirectiveUpdate, } -#[derive(Debug, Clone, PartialEq, Eq)] -struct RawDirectiveEntry { - pos: BytePos, - reset: bool, - 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 { @@ -83,20 +100,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 { @@ -142,8 +146,8 @@ fn parse_lingui_directive_raw(comment_value: &str) -> Result 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" + )); } } } @@ -156,62 +160,14 @@ 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, -) -> Option<&DirectiveValues> { +fn find_directive_for_pos(directives: &[DirectiveEntry], pos: BytePos) -> Option<&DirectiveValues> { if directives.is_empty() { return None; } @@ -235,90 +191,57 @@ pub fn find_directive_for_pos( } } -pub(crate) fn collect_lingui_directives( - node: &N, - comments: &Option, -) -> Vec -where - for<'a> N: VisitWith>, -{ - let Some(comments) = comments.as_ref() else { +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 collector = DirectiveCollector::new(comments); - node.visit_with(&mut collector); - collect_lingui_directives_from_comments(&collector.comments) -} + } -pub(crate) struct DirectiveCollector<'a, C> -where - C: Comments, -{ - comments_api: &'a C, - seen_positions: HashSet, - comments: Vec, -} + let mut directives = Vec::new(); + let mut accumulated = DirectiveValues::default(); -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![], - } - } + for comment in scan_source_comments(source) { + let comment_start = BytePos(start_pos.0 + comment.byte_offset as u32); + let trimmed = comment.content.trim(); - fn record_for_span(&mut self, span: Span) { - if span.is_dummy() { - return; + if !is_lingui_directive_prefix(trimmed) { + continue; } - 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); - } + 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, + }) + } + Ok(None) => {} + Err(message) => { + HANDLER.with(|handler| handler.struct_span_err(span, &message).emit()); } } } -} -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); - } + directives } #[cfg(test)] mod tests { use super::*; - use swc_core::common::comments::CommentKind; - - 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() { @@ -328,14 +251,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())), } - )) + }) ); } @@ -367,31 +290,64 @@ 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, } - )) + }) + ); + } + + #[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_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, @@ -399,7 +355,7 @@ mod tests { }, }, DirectiveEntry { - pos: BytePos(30), + pos: BytePos(77), values: DirectiveValues { context: Some("ctx1".into()), comment: Some("cmt".into()), @@ -407,7 +363,7 @@ mod tests { }, }, DirectiveEntry { - pos: BytePos(40), + pos: BytePos(111), values: DirectiveValues { context: Some("ctx2".into()), comment: None, diff --git a/src/comment_directive/source_scanner.rs b/src/comment_directive/source_scanner.rs new file mode 100644 index 0000000..94bd03f --- /dev/null +++ b/src/comment_directive/source_scanner.rs @@ -0,0 +1,320 @@ +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")]); + } +} diff --git a/src/lib.rs b/src/lib.rs index ac46aaa..078974c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,6 @@ use std::collections::HashSet; -use swc_core::common::{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; @@ -30,7 +31,7 @@ use crate::generate_id::*; use crate::macro_utils::*; use ast_utils::*; use builder::*; -use comment_directive::collect_lingui_directives; +use comment_directive::LinguiCommentDirectives; use js_macro_folder::JsMacroFolder; use jsx_visitor::TransJSXVisitor; @@ -58,17 +59,44 @@ where has_lingui_macro_imports: bool, ctx: MacroCtx, comments: Option, + source_map: Lrc, } impl LinguiMacroFolder where C: Comments + Clone, { - pub fn new(options: LinguiOptions, comments: Option) -> LinguiMacroFolder { + pub fn new( + options: LinguiOptions, + comments: Option, + source_map: Lrc, + ) -> LinguiMacroFolder { LinguiMacroFolder { has_lingui_macro_imports: false, ctx: MacroCtx::new(options), comments, + source_map, + } + } + + fn ensure_source_directives(&mut self, module_span: Span) { + if !self.ctx.directives.is_empty() { + return; + } + + 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, + )); } } @@ -382,8 +410,7 @@ where return node; } - self.ctx - .set_comment_directives(collect_lingui_directives(&node, &self.comments)); + self.ensure_source_directives(node.span); let mut insert_index: usize = 0; let mut index = 0; @@ -564,5 +591,11 @@ 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, + Lrc::new(metadata.source_map) as Lrc, + ); + + program.fold_with(&mut folder) } diff --git a/src/macro_utils.rs b/src/macro_utils.rs index 5f6db8f..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 comment_directives: Vec, + pub directives: LinguiCommentDirectives, pub runtime_idents: RuntimeIdents, } @@ -66,12 +66,12 @@ impl MacroCtx { } } - pub fn set_comment_directives(&mut self, directives: Vec) { - self.comment_directives = directives; + 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.comment_directives, pos) + self.directives.find_for_pos(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..fb326cd 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -23,7 +23,7 @@ impl Write for SharedWriter { pub fn transform( input: &str, - transform_cb: impl FnOnce(&SingleThreadedComments) -> 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)) + .apply(transform_cb(&comments, cm.clone())) .apply(hygiene::hygiene()) .apply(fixer::fixer(Some(&comments))); @@ -129,11 +129,14 @@ 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, cm| { + swc_core::ecma::visit::fold_pass( + lingui_macro_plugin::LinguiMacroFolder::new( + Default::default(), + Some(comments.clone()), + cm as swc_core::common::sync::Lrc, + ) + ) }) .expect("Transform produced unexpected errors"); insta::with_settings!({ @@ -149,11 +152,14 @@ 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, cm| { + swc_core::ecma::visit::fold_pass( + lingui_macro_plugin::LinguiMacroFolder::new( + options.clone(), + Some(comments.clone()), + cm as swc_core::common::sync::Lrc, + ) + ) }) .expect("Transform produced unexpected errors"); insta::with_settings!({ @@ -173,11 +179,14 @@ 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, cm| { + swc_core::ecma::visit::fold_pass( + lingui_macro_plugin::LinguiMacroFolder::new( + options.clone(), + Some(comments.clone()), + cm as swc_core::common::sync::Lrc, + ) + ) }) .expect_err("Expected transform to produce an error, but it succeeded"); insta::with_settings!({