use super::error::{MacroError, Result};
use crate::ts_syn::abi::{
GeneratedRegion, MappingSegment, Patch, PatchCode, SourceMapping, SpanIR,
};
use std::collections::HashSet;
use swc_core::{
common::{SourceMap, sync::Lrc},
ecma::codegen::{Config, Emitter, Node, text_writer::JsWriter},
};
#[derive(Clone, Debug)]
pub struct ApplyResult {
pub code: String,
pub mapping: SourceMapping,
}
pub struct PatchApplicator<'a> {
source: &'a str,
patches: Vec<Patch>,
}
impl<'a> PatchApplicator<'a> {
pub fn new(source: &'a str, patches: Vec<Patch>) -> Self {
Self { source, patches }
}
pub fn apply(mut self) -> Result<String> {
self.sort_patches();
self.validate_no_overlaps()?;
let mut result = self.source.to_string();
for patch in self.patches.iter().rev() {
match patch {
Patch::Insert { at, code, .. } => {
let rendered = render_patch_code(code)?;
let formatted =
self.format_insertion(&rendered, at.start.saturating_sub(1) as usize, code);
let idx = at.start.saturating_sub(1) as usize;
if idx <= result.len() {
result.insert_str(idx, &formatted);
}
}
Patch::InsertRaw { at, code, .. } => {
let idx = at.start.saturating_sub(1) as usize;
if idx <= result.len() {
result.insert_str(idx, code);
}
}
Patch::Replace { span, code, .. } => {
let rendered = render_patch_code(code)?;
let start = span.start.saturating_sub(1) as usize;
let end = span.end.saturating_sub(1) as usize;
if start <= end && end <= result.len() {
result.replace_range(start..end, &rendered);
}
}
Patch::ReplaceRaw { span, code, .. } => {
let start = span.start.saturating_sub(1) as usize;
let end = span.end.saturating_sub(1) as usize;
if start <= end && end <= result.len() {
result.replace_range(start..end, code);
}
}
Patch::Delete { span } => {
let start = span.start.saturating_sub(1) as usize;
let end = span.end.saturating_sub(1) as usize;
if start <= end && end <= result.len() {
result.replace_range(start..end, "");
}
}
}
}
Ok(result)
}
pub fn apply_with_mapping(mut self, fallback_macro_name: Option<&str>) -> Result<ApplyResult> {
self.sort_patches();
self.validate_no_overlaps()?;
if self.patches.is_empty() {
let source_len = self.source.len() as u32;
let mut mapping = SourceMapping::new();
if source_len > 0 {
mapping.add_segment(MappingSegment::new(0, source_len, 0, source_len));
}
return Ok(ApplyResult {
code: self.source.to_string(),
mapping,
});
}
let mut result = String::new();
let mut mapping = SourceMapping::with_capacity(self.patches.len() + 1, self.patches.len());
let mut original_pos: u32 = 1; let mut expanded_pos: u32 = 1; let source_len = self.source.len() as u32;
let source_end_pos = source_len + 1; let default_macro_name = fallback_macro_name.unwrap_or("macro");
for patch in &self.patches {
let mut copy_unchanged = |upto: u32| {
if upto > original_pos {
let len = upto - original_pos;
let start = original_pos.saturating_sub(1) as usize;
let end = upto.saturating_sub(1) as usize;
if end <= self.source.len() {
let unchanged = &self.source[start..end];
result.push_str(unchanged);
mapping.add_segment(MappingSegment::new(
original_pos - 1, upto - 1, expanded_pos - 1, expanded_pos + len - 1, ));
expanded_pos += len;
original_pos = upto;
}
}
};
let macro_attribution = patch.source_macro().unwrap_or(default_macro_name);
match patch {
Patch::Insert { at, code, .. } => {
copy_unchanged(at.start);
let rendered = render_patch_code(code)?;
let formatted =
self.format_insertion(&rendered, at.start.saturating_sub(1) as usize, code);
let gen_len = formatted.len() as u32;
result.push_str(&formatted);
mapping.add_generated(GeneratedRegion::new(
expanded_pos - 1,
expanded_pos - 1 + gen_len,
macro_attribution,
));
expanded_pos += gen_len;
}
Patch::InsertRaw { at, code, .. } => {
copy_unchanged(at.start);
let gen_len = code.len() as u32;
result.push_str(code);
mapping.add_generated(GeneratedRegion::new(
expanded_pos - 1,
expanded_pos - 1 + gen_len,
macro_attribution,
));
expanded_pos += gen_len;
}
Patch::Replace { span, code, .. } => {
copy_unchanged(span.start);
let rendered = render_patch_code(code)?;
let gen_len = rendered.len() as u32;
result.push_str(&rendered);
mapping.add_generated(GeneratedRegion::new(
expanded_pos - 1,
expanded_pos - 1 + gen_len,
macro_attribution,
));
expanded_pos += gen_len;
original_pos = span.end;
}
Patch::Delete { span } => {
copy_unchanged(span.start);
original_pos = span.end;
}
Patch::ReplaceRaw { span, code, .. } => {
copy_unchanged(span.start);
let gen_len = code.len() as u32;
result.push_str(code);
mapping.add_generated(GeneratedRegion::new(
expanded_pos - 1,
expanded_pos - 1 + gen_len,
macro_attribution,
));
expanded_pos += gen_len;
original_pos = span.end;
}
}
}
if original_pos < source_end_pos {
let len = source_end_pos - original_pos;
let start = original_pos.saturating_sub(1) as usize;
let remaining = &self.source[start..]; result.push_str(remaining);
mapping.add_segment(MappingSegment::new(
original_pos - 1,
source_end_pos - 1,
expanded_pos - 1,
expanded_pos - 1 + len,
));
}
Ok(ApplyResult {
code: result,
mapping,
})
}
fn format_insertion(&self, rendered: &str, position: usize, code: &PatchCode) -> String {
if !matches!(code, PatchCode::ClassMember(_)) {
return rendered.to_string();
}
let indent = self.detect_indentation(position);
format!("\n{}{}\n", indent, rendered.trim())
}
fn detect_indentation(&self, position: usize) -> String {
let bytes = self.source.as_bytes();
let mut search_pos = position.saturating_sub(1);
let mut found_indent: Option<String> = None;
let search_limit = position.saturating_sub(500);
while search_pos > search_limit && search_pos < bytes.len() {
let mut line_start = search_pos;
while line_start > 0 && bytes[line_start - 1] != b'\n' {
line_start -= 1;
}
let mut line_end = search_pos;
while line_end < bytes.len() && bytes[line_end] != b'\n' {
line_end += 1;
}
if line_start >= line_end {
if line_start == 0 {
break;
}
search_pos = line_start - 1;
continue;
}
let line = &self.source[line_start..line_end];
let trimmed = line.trim();
if !trimmed.is_empty()
&& !trimmed.starts_with('}')
&& !trimmed.starts_with('@')
&& (trimmed.contains(':')
|| trimmed.contains('(')
|| trimmed.starts_with("constructor"))
{
let indent_count = line.chars().take_while(|c| c.is_whitespace()).count();
if indent_count > 0 {
found_indent = Some(line.chars().take(indent_count).collect());
break;
}
}
if line_start == 0 {
break;
}
search_pos = line_start - 1;
}
found_indent.unwrap_or_else(|| " ".to_string())
}
fn sort_patches(&mut self) {
self.patches.sort_by_key(|patch| match patch {
Patch::Insert { at, .. } => at.start,
Patch::InsertRaw { at, .. } => at.start,
Patch::Replace { span, .. } => span.start,
Patch::ReplaceRaw { span, .. } => span.start,
Patch::Delete { span } => span.start,
});
}
fn validate_no_overlaps(&self) -> Result<()> {
for i in 0..self.patches.len() {
for j in i + 1..self.patches.len() {
if self.patches_overlap(&self.patches[i], &self.patches[j]) {
return Err(MacroError::Other(anyhow::anyhow!(
"Overlapping patches detected: patches cannot modify the same region"
)));
}
}
}
Ok(())
}
fn patches_overlap(&self, a: &Patch, b: &Patch) -> bool {
let a_span = self.get_patch_span(a);
let b_span = self.get_patch_span(b);
!(a_span.end <= b_span.start || b_span.end <= a_span.start)
}
fn get_patch_span(&self, patch: &Patch) -> SpanIR {
match patch {
Patch::Insert { at, .. } => *at,
Patch::InsertRaw { at, .. } => *at,
Patch::Replace { span, .. } => *span,
Patch::ReplaceRaw { span, .. } => *span,
Patch::Delete { span } => *span,
}
}
}
pub struct PatchCollector {
runtime_patches: Vec<Patch>,
type_patches: Vec<Patch>,
}
impl PatchCollector {
pub fn new() -> Self {
Self {
runtime_patches: Vec::new(),
type_patches: Vec::new(),
}
}
pub fn add_runtime_patches(&mut self, patches: Vec<Patch>) {
self.runtime_patches.extend(patches);
}
pub fn add_type_patches(&mut self, patches: Vec<Patch>) {
self.type_patches.extend(patches);
}
pub fn has_type_patches(&self) -> bool {
!self.type_patches.is_empty()
}
pub fn has_patches(&self) -> bool {
!self.runtime_patches.is_empty() || !self.type_patches.is_empty()
}
pub fn runtime_patches_count(&self) -> usize {
self.runtime_patches.len()
}
pub fn runtime_patches_slice(&self, start: usize) -> &[Patch] {
&self.runtime_patches[start..]
}
pub fn apply_runtime_patches(&self, source: &str) -> Result<String> {
if self.runtime_patches.is_empty() {
return Ok(source.to_string());
}
let mut patches = self.runtime_patches.clone();
dedupe_patches(&mut patches)?;
let applicator = PatchApplicator::new(source, patches);
applicator.apply()
}
pub fn apply_type_patches(&self, source: &str) -> Result<String> {
if self.type_patches.is_empty() {
return Ok(source.to_string());
}
let mut patches = self.type_patches.clone();
dedupe_patches(&mut patches)?;
let applicator = PatchApplicator::new(source, patches);
applicator.apply()
}
pub fn apply_runtime_patches_with_mapping(
&self,
source: &str,
macro_name: Option<&str>,
) -> Result<ApplyResult> {
if self.runtime_patches.is_empty() {
let source_len = source.len() as u32;
let mut mapping = SourceMapping::new();
if source_len > 0 {
mapping.add_segment(MappingSegment::new(0, source_len, 0, source_len));
}
return Ok(ApplyResult {
code: source.to_string(),
mapping,
});
}
let mut patches = self.runtime_patches.clone();
dedupe_patches(&mut patches)?;
let applicator = PatchApplicator::new(source, patches);
applicator.apply_with_mapping(macro_name)
}
pub fn apply_type_patches_with_mapping(
&self,
source: &str,
macro_name: Option<&str>,
) -> Result<ApplyResult> {
if self.type_patches.is_empty() {
let source_len = source.len() as u32;
let mut mapping = SourceMapping::new();
if source_len > 0 {
mapping.add_segment(MappingSegment::new(0, source_len, 0, source_len));
}
return Ok(ApplyResult {
code: source.to_string(),
mapping,
});
}
let mut patches = self.type_patches.clone();
dedupe_patches(&mut patches)?;
let applicator = PatchApplicator::new(source, patches);
applicator.apply_with_mapping(macro_name)
}
pub fn get_type_patches(&self) -> &Vec<Patch> {
&self.type_patches
}
}
impl Default for PatchCollector {
fn default() -> Self {
Self::new()
}
}
fn dedupe_patches(patches: &mut Vec<Patch>) -> Result<()> {
dedupe_imports(patches);
let mut seen: HashSet<(u8, u32, u32, Option<String>)> = HashSet::new();
let mut indices_to_keep = Vec::new();
for (i, patch) in patches.iter().enumerate() {
let key = match patch {
Patch::Insert { at, code, .. } => (0, at.start, at.end, Some(render_patch_code(code)?)),
Patch::InsertRaw { at, code, .. } => (3, at.start, at.end, Some(code.clone())),
Patch::Replace { span, code, .. } => {
(1, span.start, span.end, Some(render_patch_code(code)?))
}
Patch::ReplaceRaw { span, code, .. } => (4, span.start, span.end, Some(code.clone())),
Patch::Delete { span } => (2, span.start, span.end, None),
};
if seen.insert(key) {
indices_to_keep.push(i);
}
}
let old_patches = std::mem::take(patches);
*patches = indices_to_keep
.into_iter()
.map(|i| old_patches[i].clone())
.collect();
Ok(())
}
fn parse_import_patch(code: &str) -> Option<(String, String, bool)> {
let trimmed = code.trim();
let is_type = trimmed.starts_with("import type ");
let rest = if is_type {
trimmed.strip_prefix("import type ")?
} else {
trimmed.strip_prefix("import ")?
};
let brace_start = rest.find('{')?;
let brace_end = rest.find('}')?;
let specifier_raw = rest[brace_start + 1..brace_end].trim();
let base_specifier = if let Some(pos) = specifier_raw.find(" as ") {
specifier_raw[..pos].trim().to_string()
} else {
specifier_raw.to_string()
};
let after_brace = &rest[brace_end + 1..];
let quote_char = if after_brace.contains('"') { '"' } else { '\'' };
let first_quote = after_brace.find(quote_char)?;
let second_quote = after_brace[first_quote + 1..].find(quote_char)?;
let module = after_brace[first_quote + 1..first_quote + 1 + second_quote].to_string();
Some((base_specifier, module, is_type))
}
fn dedupe_imports(patches: &mut Vec<Patch>) {
let mut value_imports: HashSet<(String, String)> = HashSet::new();
for patch in patches.iter() {
if let Patch::InsertRaw {
context: Some(ctx),
code,
..
} = patch
&& ctx == "import"
&& let Some((specifier, module, is_type)) = parse_import_patch(code)
&& !is_type
{
value_imports.insert((specifier, module));
}
}
if value_imports.is_empty() {
return;
}
patches.retain(|patch| {
if let Patch::InsertRaw {
context: Some(ctx),
code,
..
} = patch
&& ctx == "import"
&& let Some((specifier, module, is_type)) = parse_import_patch(code)
&& is_type
&& value_imports.contains(&(specifier, module))
{
return false; }
true
});
}
fn render_patch_code(code: &PatchCode) -> Result<String> {
match code {
PatchCode::Text(s) => Ok(s.clone()),
PatchCode::ClassMember(member) => emit_node(member),
PatchCode::Stmt(stmt) => emit_node(stmt),
PatchCode::ModuleItem(item) => emit_node(item),
}
}
fn emit_node<N: Node>(node: &N) -> Result<String> {
let cm: Lrc<SourceMap> = Default::default();
let mut buf = Vec::new();
{
let writer = JsWriter::new(cm.clone(), "\n", &mut buf, None);
let mut emitter = Emitter {
cfg: Config::default(),
cm: cm.clone(),
comments: None,
wr: writer,
};
node.emit_with(&mut emitter)
.map_err(|err| anyhow::anyhow!(err))?;
}
let output = String::from_utf8(buf).map_err(|err| anyhow::anyhow!(err))?;
Ok(output.trim_end().to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_insert_patch() {
let source = "class Foo {}";
let patch = Patch::Insert {
at: SpanIR { start: 12, end: 12 },
code: " bar: string; ".to_string().into(),
source_macro: None,
};
let applicator = PatchApplicator::new(source, vec![patch]);
let result = applicator.apply().unwrap();
assert_eq!(result, "class Foo { bar: string; }");
}
#[test]
fn test_replace_patch() {
let source = "class Foo { old: number; }";
let patch = Patch::Replace {
span: SpanIR { start: 13, end: 26 },
code: "new: string;".to_string().into(),
source_macro: None,
};
let applicator = PatchApplicator::new(source, vec![patch]);
let result = applicator.apply().unwrap();
assert_eq!(result, "class Foo { new: string;}");
}
#[test]
fn test_delete_patch() {
let source = "class Foo { unnecessary: any; }";
let patch = Patch::Delete {
span: SpanIR { start: 13, end: 31 },
};
let applicator = PatchApplicator::new(source, vec![patch]);
let result = applicator.apply().unwrap();
assert_eq!(result, "class Foo { }");
}
#[test]
fn test_multiple_patches() {
let source = "class Foo {}";
let patches = vec![
Patch::Insert {
at: SpanIR { start: 12, end: 12 },
code: " bar: string;".to_string().into(),
source_macro: None,
},
Patch::Insert {
at: SpanIR { start: 12, end: 12 },
code: " baz: number;".to_string().into(),
source_macro: None,
},
];
let applicator = PatchApplicator::new(source, patches);
let result = applicator.apply().unwrap();
assert!(result.contains("bar: string"));
assert!(result.contains("baz: number"));
}
#[test]
fn test_replace_multiline_block_with_single_line() {
let source = "class C { constructor() { /* body */ } }";
let constructor_start = source.find("constructor").unwrap();
let constructor_end = source.find("} }").unwrap() + 1;
let patch = Patch::Replace {
span: SpanIR {
start: constructor_start as u32 + 1,
end: constructor_end as u32 + 1,
},
code: "constructor();".to_string().into(),
source_macro: None,
};
let applicator = PatchApplicator::new(source, vec![patch]);
let result = applicator.apply().unwrap();
let expected = "class C { constructor(); }";
assert_eq!(result, expected);
}
#[test]
fn test_detect_indentation_spaces() {
let source = r#"class User {
id: number;
name: string;
}"#;
let closing_brace_pos = source.rfind('}').unwrap();
let applicator = PatchApplicator::new(source, vec![]);
let indent = applicator.detect_indentation(closing_brace_pos);
assert_eq!(indent, " ");
}
#[test]
fn test_detect_indentation_tabs() {
let source = "class User {\n\tid: number;\n}";
let closing_brace_pos = source.rfind('}').unwrap();
let applicator = PatchApplicator::new(source, vec![]);
let indent = applicator.detect_indentation(closing_brace_pos);
assert_eq!(indent, "\t");
}
#[test]
fn test_format_insertion_adds_newline_and_indent() {
let source = r#"class User {
id: number;
}"#;
let closing_brace_pos = source.rfind('}').unwrap();
let applicator = PatchApplicator::new(source, vec![]);
use swc_core::ecma::ast::{ClassMember, EmptyStmt};
let code = PatchCode::ClassMember(ClassMember::Empty(EmptyStmt {
span: swc_core::common::DUMMY_SP,
}));
let formatted =
applicator.format_insertion("toString(): string;", closing_brace_pos, &code);
assert!(formatted.starts_with('\n'));
assert!(formatted.contains("toString(): string;"));
}
#[test]
fn test_insert_class_member_with_proper_formatting() {
let source = r#"class User {
id: number;
name: string;
}"#;
let closing_brace_pos = source.rfind('}').unwrap();
let patch = Patch::Insert {
at: SpanIR {
start: closing_brace_pos as u32 + 1,
end: closing_brace_pos as u32 + 1,
},
code: "toString(): string;".to_string().into(),
source_macro: None,
};
let applicator = PatchApplicator::new(source, vec![patch]);
let result = applicator.apply().unwrap();
assert!(result.contains("toString(): string;"));
}
#[test]
fn test_multiple_class_member_insertions() {
let source = r#"class User {
id: number;
}"#;
let closing_brace_pos = source.rfind('}').unwrap();
let patches = vec![
Patch::Insert {
at: SpanIR {
start: closing_brace_pos as u32 + 1,
end: closing_brace_pos as u32 + 1,
},
code: "toString(): string;".to_string().into(),
source_macro: None,
},
Patch::Insert {
at: SpanIR {
start: closing_brace_pos as u32 + 1,
end: closing_brace_pos as u32 + 1,
},
code: "toJSON(): Record<string, unknown>;".to_string().into(),
source_macro: None,
},
];
let applicator = PatchApplicator::new(source, patches);
let result = applicator.apply().unwrap();
assert!(result.contains("toString(): string;"));
assert!(result.contains("toJSON(): Record<string, unknown>;"));
}
#[test]
fn test_indentation_preserved_in_nested_class() {
let source = r#"export namespace Models {
class User {
id: number;
}
}"#;
let closing_brace_pos = source.find(" }").unwrap() + 2; let applicator = PatchApplicator::new(source, vec![]);
let indent = applicator.detect_indentation(closing_brace_pos);
assert_eq!(indent, " ");
}
#[test]
fn test_no_formatting_for_text_patches() {
let source = "class User {}";
let pos = 11; let applicator = PatchApplicator::new(source, vec![]);
let formatted =
applicator.format_insertion("test", pos, &PatchCode::Text("test".to_string()));
assert_eq!(formatted, "test");
}
#[test]
fn test_dedupe_patches_removes_identical_inserts() {
let mut patches = vec![
Patch::Insert {
at: SpanIR { start: 11, end: 11 },
code: "console.log('a');".to_string().into(),
source_macro: None,
},
Patch::Insert {
at: SpanIR { start: 11, end: 11 },
code: "console.log('a');".to_string().into(),
source_macro: None,
},
Patch::Insert {
at: SpanIR { start: 21, end: 21 },
code: "console.log('b');".to_string().into(),
source_macro: None,
},
];
dedupe_patches(&mut patches).expect("dedupe should succeed");
assert_eq!(
patches.len(),
2,
"duplicate inserts should collapse to a single patch"
);
assert!(
patches
.iter()
.any(|patch| matches!(patch, Patch::Insert { at, .. } if at.start == 21)),
"dedupe should retain distinct spans"
);
}
#[test]
fn test_apply_with_mapping_no_patches() {
let source = "class Foo {}";
let applicator = PatchApplicator::new(source, vec![]);
let result = applicator.apply_with_mapping(None).unwrap();
assert_eq!(result.code, source);
assert_eq!(result.mapping.segments.len(), 1);
assert!(result.mapping.generated_regions.is_empty());
assert_eq!(result.mapping.original_to_expanded(0), 0);
assert_eq!(result.mapping.original_to_expanded(5), 5);
assert_eq!(result.mapping.expanded_to_original(5), Some(5));
}
#[test]
fn test_apply_with_mapping_simple_insert() {
let source = "class Foo {}";
let patch = Patch::Insert {
at: SpanIR { start: 12, end: 12 },
code: " bar;".to_string().into(),
source_macro: Some("Test".to_string()),
};
let applicator = PatchApplicator::new(source, vec![patch]);
let result = applicator.apply_with_mapping(None).unwrap();
assert_eq!(result.code, "class Foo { bar;}");
assert_eq!(result.code.len(), 17);
assert_eq!(result.mapping.segments.len(), 2);
assert_eq!(result.mapping.generated_regions.len(), 1);
let seg1 = &result.mapping.segments[0];
assert_eq!(seg1.original_start, 0);
assert_eq!(seg1.original_end, 11);
assert_eq!(seg1.expanded_start, 0);
assert_eq!(seg1.expanded_end, 11);
let generated = &result.mapping.generated_regions[0];
assert_eq!(generated.start, 11);
assert_eq!(generated.end, 16);
assert_eq!(generated.source_macro, "Test");
let seg2 = &result.mapping.segments[1];
assert_eq!(seg2.original_start, 11);
assert_eq!(seg2.original_end, 12);
assert_eq!(seg2.expanded_start, 16);
assert_eq!(seg2.expanded_end, 17);
assert_eq!(result.mapping.original_to_expanded(0), 0);
assert_eq!(result.mapping.original_to_expanded(10), 10);
assert_eq!(result.mapping.original_to_expanded(11), 16);
assert_eq!(result.mapping.expanded_to_original(5), Some(5));
assert_eq!(result.mapping.expanded_to_original(12), None); assert_eq!(result.mapping.expanded_to_original(16), Some(11));
}
#[test]
fn test_apply_with_mapping_replace() {
let source = "let x = old;";
let patch = Patch::Replace {
span: SpanIR { start: 9, end: 12 },
code: "new".to_string().into(),
source_macro: None,
};
let applicator = PatchApplicator::new(source, vec![patch]);
let result = applicator.apply_with_mapping(None).unwrap();
assert_eq!(result.code, "let x = new;");
assert_eq!(result.mapping.segments.len(), 2);
assert_eq!(result.mapping.generated_regions.len(), 1);
let seg1 = &result.mapping.segments[0];
assert_eq!(seg1.original_start, 0);
assert_eq!(seg1.original_end, 8);
let generated = &result.mapping.generated_regions[0];
assert_eq!(generated.start, 8);
assert_eq!(generated.end, 11);
let seg2 = &result.mapping.segments[1];
assert_eq!(seg2.original_start, 11);
assert_eq!(seg2.original_end, 12);
assert_eq!(seg2.expanded_start, 11);
assert_eq!(seg2.expanded_end, 12);
assert_eq!(result.mapping.expanded_to_original(9), None);
}
#[test]
fn test_apply_with_mapping_delete() {
let source = "let x = 1; let y = 2;";
let patch = Patch::Delete {
span: SpanIR { start: 11, end: 21 },
};
let applicator = PatchApplicator::new(source, vec![patch]);
let result = applicator.apply_with_mapping(None).unwrap();
assert_eq!(result.code, "let x = 1;;");
assert_eq!(result.mapping.segments.len(), 2);
assert_eq!(result.mapping.generated_regions.len(), 0);
assert_eq!(result.mapping.original_to_expanded(20), 10);
assert_eq!(result.mapping.expanded_to_original(10), Some(20));
}
#[test]
fn test_apply_with_mapping_multiple_inserts() {
let source = "a;b;c;";
let patches = vec![
Patch::Insert {
at: SpanIR { start: 3, end: 3 },
code: "X".to_string().into(),
source_macro: Some("multi".to_string()),
},
Patch::Insert {
at: SpanIR { start: 5, end: 5 },
code: "Y".to_string().into(),
source_macro: Some("multi".to_string()),
},
];
let applicator = PatchApplicator::new(source, patches);
let result = applicator.apply_with_mapping(None).unwrap();
assert_eq!(result.code, "a;Xb;Yc;");
assert_eq!(result.mapping.segments.len(), 3);
assert_eq!(result.mapping.generated_regions.len(), 2);
assert_eq!(result.mapping.original_to_expanded(0), 0); assert_eq!(result.mapping.original_to_expanded(2), 3); assert_eq!(result.mapping.original_to_expanded(4), 6);
assert!(result.mapping.is_in_generated(2)); assert!(result.mapping.is_in_generated(5)); assert!(!result.mapping.is_in_generated(0)); assert!(!result.mapping.is_in_generated(3)); }
#[test]
fn test_apply_with_mapping_span_mapping() {
let source = "class Foo {}";
let patch = Patch::Insert {
at: SpanIR { start: 12, end: 12 },
code: " bar();".to_string().into(),
source_macro: None,
};
let applicator = PatchApplicator::new(source, vec![patch]);
let result = applicator.apply_with_mapping(None).unwrap();
let (exp_start, exp_len) = result.mapping.map_span_to_expanded(0, 5);
assert_eq!(exp_start, 0);
assert_eq!(exp_len, 5);
let orig = result.mapping.map_span_to_original(0, 5);
assert_eq!(orig, Some((0, 5)));
let gen_span = result.mapping.map_span_to_original(12, 3);
assert_eq!(gen_span, None);
}
#[test]
fn test_patch_collector_with_mapping() {
let source = "class Foo {}";
let mut collector = PatchCollector::new();
collector.add_runtime_patches(vec![Patch::Insert {
at: SpanIR { start: 12, end: 12 },
code: " toString() {}".to_string().into(),
source_macro: Some("Debug".to_string()),
}]);
let result = collector
.apply_runtime_patches_with_mapping(source, None)
.unwrap();
assert!(result.code.contains("toString()"));
assert_eq!(result.mapping.generated_regions.len(), 1);
assert_eq!(result.mapping.generated_regions[0].source_macro, "Debug");
}
#[test]
fn test_parse_import_patch_value() {
let code = "import { Option } from \"effect\";\n";
let result = parse_import_patch(code);
assert_eq!(
result,
Some(("Option".to_string(), "effect".to_string(), false))
);
}
#[test]
fn test_parse_import_patch_type_only() {
let code = "import type { Exit } from \"effect\";\n";
let result = parse_import_patch(code);
assert_eq!(
result,
Some(("Exit".to_string(), "effect".to_string(), true))
);
}
#[test]
fn test_parse_import_patch_aliased() {
let code = "import { Option as __gigaform_reexport_Option } from \"effect\";\n";
let result = parse_import_patch(code);
assert_eq!(
result,
Some(("Option".to_string(), "effect".to_string(), false))
);
}
#[test]
fn test_dedupe_imports_value_subsumes_type() {
let mut patches = vec![
Patch::InsertRaw {
at: SpanIR { start: 1, end: 1 },
code: "import type { Exit } from \"effect\";\n".to_string(),
context: Some("import".to_string()),
source_macro: None,
},
Patch::InsertRaw {
at: SpanIR { start: 1, end: 1 },
code: "import { Exit } from \"effect\";\n".to_string(),
context: Some("import".to_string()),
source_macro: None,
},
];
dedupe_imports(&mut patches);
assert_eq!(
patches.len(),
1,
"type-only import should be removed when value import exists"
);
assert!(patches[0].source_macro().is_none());
if let Patch::InsertRaw { code, .. } = &patches[0] {
assert!(
!code.contains("import type"),
"remaining import should be the value import"
);
assert!(code.contains("import { Exit }"));
} else {
panic!("expected InsertRaw");
}
}
#[test]
fn test_dedupe_imports_keeps_unrelated() {
let mut patches = vec![
Patch::InsertRaw {
at: SpanIR { start: 1, end: 1 },
code: "import type { FieldController } from \"@dealdraft/macros/gigaform\";\n"
.to_string(),
context: Some("import".to_string()),
source_macro: None,
},
Patch::InsertRaw {
at: SpanIR { start: 1, end: 1 },
code: "import { Option } from \"effect\";\n".to_string(),
context: Some("import".to_string()),
source_macro: None,
},
];
dedupe_imports(&mut patches);
assert_eq!(patches.len(), 2, "unrelated imports should both be kept");
}
#[test]
fn test_dedupe_imports_different_modules_kept() {
let mut patches = vec![
Patch::InsertRaw {
at: SpanIR { start: 1, end: 1 },
code: "import type { Option } from \"effect/Option\";\n".to_string(),
context: Some("import".to_string()),
source_macro: None,
},
Patch::InsertRaw {
at: SpanIR { start: 1, end: 1 },
code: "import { Option } from \"effect\";\n".to_string(),
context: Some("import".to_string()),
source_macro: None,
},
];
dedupe_imports(&mut patches);
assert_eq!(
patches.len(),
2,
"imports from different modules should both be kept"
);
}
}