Skip to content

Commit 4bbbcf8

Browse files
authored
refactor: single-parse edit-based transform pipeline (#93)
1 parent a0787fd commit 4bbbcf8

File tree

2 files changed

+162
-274
lines changed

2 files changed

+162
-274
lines changed

crates/oxc_angular_compiler/src/component/import_elision.rs

Lines changed: 24 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ use oxc_semantic::{Semantic, SemanticBuilder, SymbolFlags};
4444
use oxc_span::Atom;
4545
use rustc_hash::FxHashSet;
4646

47+
use crate::optimizer::Edit;
48+
4749
/// Angular constructor parameter decorators that are removed during compilation.
4850
/// These decorators are downleveled to factory metadata and their imports can be elided.
4951
///
@@ -719,16 +721,16 @@ impl<'a> ImportElisionAnalyzer<'a> {
719721
}
720722
}
721723

722-
/// Filter import declarations to remove type-only specifiers.
724+
/// Compute import elision edits as `Vec<Edit>` objects.
723725
///
724-
/// Returns a new source string with type-only import specifiers removed.
726+
/// Returns edits that remove type-only import specifiers from the source.
725727
/// Entire import declarations are removed if all their specifiers are type-only,
726728
/// or if the import has no specifiers at all (`import {} from 'module'`).
727-
pub fn filter_imports<'a>(
729+
pub fn import_elision_edits<'a>(
728730
source: &str,
729731
program: &Program<'a>,
730732
analyzer: &ImportElisionAnalyzer<'a>,
731-
) -> String {
733+
) -> Vec<Edit> {
732734
// Check if there are empty imports that need removal (import {} from '...')
733735
let has_empty_imports = program.body.iter().any(|stmt| {
734736
if let Statement::ImportDeclaration(import_decl) = stmt {
@@ -740,13 +742,10 @@ pub fn filter_imports<'a>(
740742
});
741743

742744
if !analyzer.has_type_only_imports() && !has_empty_imports {
743-
return source.to_string();
745+
return Vec::new();
744746
}
745747

746-
// Collect spans to remove (in reverse order for safe removal)
747-
let mut removals: Vec<(usize, usize)> = Vec::new();
748-
// Collect partial replacements (start, end, replacement_string)
749-
let mut partial_replacements: Vec<(usize, usize, String)> = Vec::new();
748+
let mut edits: Vec<Edit> = Vec::new();
750749

751750
for stmt in &program.body {
752751
let oxc_ast::ast::Statement::ImportDeclaration(import_decl) = stmt else {
@@ -788,12 +787,10 @@ pub fn filter_imports<'a>(
788787
end += 1;
789788
}
790789

791-
removals.push((start, end));
790+
edits.push(Edit::delete(start as u32, end as u32));
792791
} else {
793792
// Partial removal - reconstruct import with only kept specifiers
794-
// We need to rebuild the import statement preserving the original structure
795793

796-
// Find default import and named specifiers among kept
797794
let mut default_import: Option<&str> = None;
798795
let mut named_specifiers: Vec<String> = Vec::new();
799796

@@ -812,7 +809,6 @@ pub fn filter_imports<'a>(
812809
}
813810
}
814811
ImportDeclarationSpecifier::ImportNamespaceSpecifier(s) => {
815-
// Namespace import: import * as foo
816812
named_specifiers.push(format!("* as {}", s.local.name));
817813
}
818814
}
@@ -830,7 +826,6 @@ pub fn filter_imports<'a>(
830826
}
831827

832828
if !named_specifiers.is_empty() {
833-
// Check if any is a namespace import
834829
if named_specifiers.len() == 1 && named_specifiers[0].starts_with("* as ") {
835830
new_import.push_str(&named_specifiers[0]);
836831
} else {
@@ -852,45 +847,38 @@ pub fn filter_imports<'a>(
852847
let bytes = source.as_bytes();
853848
while end < bytes.len() && (bytes[end] == b'\n' || bytes[end] == b'\r') {
854849
end += 1;
855-
// Add newline to replacement
856850
if !new_import.ends_with('\n') {
857851
new_import.push('\n');
858852
}
859853
}
860854

861-
// Store as replacement (start, end, replacement)
862-
// We'll handle this differently - store removals with replacement
863-
partial_replacements.push((start, end, new_import));
855+
edits.push(Edit::replace(start as u32, end as u32, new_import));
864856
}
865857
}
866858

867-
// Combine full removals (replacement = empty string) with partial replacements
868-
// and sort in reverse order for safe string manipulation
869-
let mut all_operations: Vec<(usize, usize, String)> = Vec::new();
870-
871-
for (start, end) in removals {
872-
all_operations.push((start, end, String::new()));
873-
}
874-
all_operations.extend(partial_replacements);
875-
876-
// Sort by start position in reverse order
877-
all_operations.sort_by(|a, b| b.0.cmp(&a.0));
878-
879-
let mut result = source.to_string();
880-
for (start, end, replacement) in all_operations {
881-
result.replace_range(start..end, &replacement);
882-
}
883-
884-
result
859+
edits
885860
}
886861

887862
#[cfg(test)]
888863
mod tests {
889864
use super::*;
865+
use crate::optimizer::apply_edits;
890866
use oxc_allocator::Allocator;
891867
use oxc_parser::Parser;
892868
use oxc_span::SourceType;
893869

870+
fn filter_imports<'a>(
871+
source: &str,
872+
program: &Program<'a>,
873+
analyzer: &ImportElisionAnalyzer<'a>,
874+
) -> String {
875+
let edits = import_elision_edits(source, program, analyzer);
876+
if edits.is_empty() {
877+
return source.to_string();
878+
}
879+
apply_edits(source, edits)
880+
}
881+
894882
fn analyze_source(source: &str) -> FxHashSet<String> {
895883
let allocator = Allocator::default();
896884
let source_type = SourceType::ts();

0 commit comments

Comments
 (0)