Skip to content

Commit 187a62a

Browse files
committed
save
1 parent 1567c3d commit 187a62a

91 files changed

Lines changed: 1595 additions & 506 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/oxc_angular_compiler/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,11 @@ rustc-hash = { workspace = true }
2727
lazy-regex = { workspace = true }
2828
indexmap = { workspace = true }
2929
oxc_resolver = { version = "11", optional = true }
30+
pathdiff = { version = "0.2", optional = true }
3031

3132
[features]
3233
default = []
33-
cross_file_elision = ["oxc_resolver"]
34+
cross_file_elision = ["oxc_resolver", "pathdiff"]
3435

3536
[dev-dependencies]
3637
insta = { workspace = true, features = ["glob"] }

crates/oxc_angular_compiler/src/component/cross_file_elision.rs

Lines changed: 346 additions & 8 deletions
Large diffs are not rendered by default.

crates/oxc_angular_compiler/src/component/decorator.rs

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1088,6 +1088,48 @@ pub fn collect_member_decorator_spans(class: &Class<'_>, spans: &mut std::vec::V
10881088
}
10891089
}
10901090

1091+
/// Collect spans of uninitialized class field declarations for stripping.
1092+
///
1093+
/// TypeScript class field declarations like `name: string;` without initializers are
1094+
/// type-only annotations that have no runtime effect. They should be stripped from
1095+
/// the output to match TypeScript's emit behavior.
1096+
///
1097+
/// Only collects:
1098+
/// - Non-static property definitions without an initializer (e.g., `name: string;`)
1099+
///
1100+
/// Does NOT collect:
1101+
/// - Fields with initializers (e.g., `name = 'value';`) - these have runtime values
1102+
/// - Static fields (e.g., `static version: string;`) - handled separately
1103+
/// - Methods or accessors - they have runtime bodies
1104+
/// - Decorated fields - Angular may need the field structure
1105+
///
1106+
/// These spans are used by `transform.rs` to remove the declarations from the
1107+
/// source text during transformation.
1108+
pub fn collect_uninitialized_field_spans(class: &Class<'_>, spans: &mut std::vec::Vec<Span>) {
1109+
for element in &class.body.body {
1110+
if let ClassElement::PropertyDefinition(prop) = element {
1111+
// Skip static fields - they're handled differently
1112+
if prop.r#static {
1113+
continue;
1114+
}
1115+
1116+
// Skip fields with initializers - they have runtime values
1117+
if prop.value.is_some() {
1118+
continue;
1119+
}
1120+
1121+
// Skip decorated fields - Angular may need them
1122+
if !prop.decorators.is_empty() {
1123+
continue;
1124+
}
1125+
1126+
// This is an uninitialized, non-static, non-decorated field declaration
1127+
// (e.g., `name: string;`) - it should be stripped
1128+
spans.push(prop.span);
1129+
}
1130+
}
1131+
}
1132+
10911133
#[cfg(test)]
10921134
mod tests {
10931135
use super::*;
@@ -1109,7 +1151,7 @@ mod tests {
11091151
let parser_ret = Parser::new(&allocator, code, source_type).parse();
11101152

11111153
// Build import map from the program body
1112-
let import_map = build_import_map(&parser_ret.program.body);
1154+
let import_map = build_import_map(&allocator, &parser_ret.program.body, None);
11131155

11141156
// Find the first class declaration (handles plain, export default, and export named)
11151157
let mut found_metadata = None;

crates/oxc_angular_compiler/src/component/import_elision.rs

Lines changed: 126 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,25 @@ use rustc_hash::FxHashSet;
5050
/// Reference: packages/compiler-cli/src/ngtsc/annotations/common/src/di.ts
5151
const PARAM_DECORATORS: &[&str] = &["Inject", "Optional", "Self", "SkipSelf", "Host", "Attribute"];
5252

53+
/// Angular member decorators that are removed during compilation.
54+
/// These decorators are compiled into the component/directive definition and their
55+
/// imports can be elided. They are used on class properties and methods.
56+
///
57+
/// Reference: packages/compiler-cli/src/ngtsc/annotations/directive/src/decorator.ts
58+
const MEMBER_DECORATORS: &[&str] = &[
59+
// Property decorators
60+
"Input",
61+
"Output",
62+
"HostBinding",
63+
// Method decorators
64+
"HostListener",
65+
// Query decorators
66+
"ViewChild",
67+
"ViewChildren",
68+
"ContentChild",
69+
"ContentChildren",
70+
];
71+
5372
/// Analyzer for determining which imports are type-only and can be elided.
5473
pub struct ImportElisionAnalyzer<'a> {
5574
/// Set of import specifier local names that should be removed (type-only).
@@ -123,26 +142,27 @@ impl<'a> ImportElisionAnalyzer<'a> {
123142
Self { type_only_specifiers }
124143
}
125144

126-
/// Collect import names that are used ONLY in constructor parameter decorators.
145+
/// Collect import names that are used ONLY in Angular decorators that are removed during compilation.
127146
///
128-
/// Constructor parameter decorators like `@Inject`, `@Optional`, `@Self`, `@SkipSelf`,
129-
/// `@Host`, and `@Attribute` are removed by Angular's compiler and converted to
130-
/// factory metadata. The imports for these decorators and their arguments (like
131-
/// `@Inject(TOKEN)`) should be elided.
147+
/// This includes:
148+
/// - Constructor parameter decorators (`@Inject`, `@Optional`, `@Self`, `@SkipSelf`, `@Host`, `@Attribute`)
149+
/// - Member decorators (`@Input`, `@Output`, `@HostBinding`, `@HostListener`, `@ViewChild`, etc.)
132150
///
133-
/// This function returns a set of local import names that should be elided because:
134-
/// 1. The import is a known parameter decorator (Inject, Optional, etc.)
135-
/// 2. The import is ONLY used as an argument to @Inject()
151+
/// These decorators are compiled into the component/directive definition by Angular's compiler,
152+
/// so their imports can be elided.
136153
///
137154
/// Reference: packages/compiler-cli/src/ngtsc/transform/jit/src/downlevel_decorators_transform.ts
138155
fn collect_ctor_param_decorator_only_imports(program: &'a Program<'a>) -> FxHashSet<&'a str> {
139156
let mut result = FxHashSet::default();
140157

141158
// Track:
142-
// 1. Symbols used ONLY in ctor param decorator position (the decorator itself)
143-
// 2. Symbols used ONLY as @Inject() arguments
159+
// 1. Symbols used in ctor param decorator position (the decorator itself)
160+
// 2. Symbols used as @Inject() arguments
161+
// 3. Symbols used in member decorator position (property/method decorators)
162+
// 4. Symbols used in other value positions (can't be elided)
144163
let mut ctor_param_decorator_uses: FxHashSet<&'a str> = FxHashSet::default();
145164
let mut inject_arg_uses: FxHashSet<&'a str> = FxHashSet::default();
165+
let mut member_decorator_uses: FxHashSet<&'a str> = FxHashSet::default();
146166
let mut other_value_uses: FxHashSet<&'a str> = FxHashSet::default();
147167

148168
// Walk the AST to find constructor parameters and their decorators
@@ -151,13 +171,12 @@ impl<'a> ImportElisionAnalyzer<'a> {
151171
stmt,
152172
&mut ctor_param_decorator_uses,
153173
&mut inject_arg_uses,
174+
&mut member_decorator_uses,
154175
&mut other_value_uses,
155176
);
156177
}
157178

158-
// A symbol can be elided if:
159-
// 1. It's used in ctor param decorators AND NOT used elsewhere, OR
160-
// 2. It's used in @Inject() args AND NOT used elsewhere
179+
// A symbol can be elided if it's used only in decorator positions and NOT used elsewhere
161180
for name in ctor_param_decorator_uses {
162181
if !other_value_uses.contains(name) {
163182
result.insert(name);
@@ -168,6 +187,11 @@ impl<'a> ImportElisionAnalyzer<'a> {
168187
result.insert(name);
169188
}
170189
}
190+
for name in member_decorator_uses {
191+
if !other_value_uses.contains(name) {
192+
result.insert(name);
193+
}
194+
}
171195

172196
result
173197
}
@@ -177,6 +201,7 @@ impl<'a> ImportElisionAnalyzer<'a> {
177201
stmt: &'a Statement<'a>,
178202
ctor_param_decorator_uses: &mut FxHashSet<&'a str>,
179203
inject_arg_uses: &mut FxHashSet<&'a str>,
204+
member_decorator_uses: &mut FxHashSet<&'a str>,
180205
other_value_uses: &mut FxHashSet<&'a str>,
181206
) {
182207
match stmt {
@@ -185,6 +210,7 @@ impl<'a> ImportElisionAnalyzer<'a> {
185210
class,
186211
ctor_param_decorator_uses,
187212
inject_arg_uses,
213+
member_decorator_uses,
188214
other_value_uses,
189215
);
190216
}
@@ -196,6 +222,7 @@ impl<'a> ImportElisionAnalyzer<'a> {
196222
class,
197223
ctor_param_decorator_uses,
198224
inject_arg_uses,
225+
member_decorator_uses,
199226
other_value_uses,
200227
);
201228
}
@@ -207,6 +234,7 @@ impl<'a> ImportElisionAnalyzer<'a> {
207234
class,
208235
ctor_param_decorator_uses,
209236
inject_arg_uses,
237+
member_decorator_uses,
210238
other_value_uses,
211239
);
212240
}
@@ -232,6 +260,7 @@ impl<'a> ImportElisionAnalyzer<'a> {
232260
class: &'a oxc_ast::ast::Class<'a>,
233261
ctor_param_decorator_uses: &mut FxHashSet<&'a str>,
234262
inject_arg_uses: &mut FxHashSet<&'a str>,
263+
member_decorator_uses: &mut FxHashSet<&'a str>,
235264
other_value_uses: &mut FxHashSet<&'a str>,
236265
) {
237266
// Process class decorators - these are NOT elided (they run at runtime)
@@ -243,9 +272,14 @@ impl<'a> ImportElisionAnalyzer<'a> {
243272
for element in &class.body.body {
244273
match element {
245274
ClassElement::MethodDefinition(method) => {
246-
// Process method decorators (e.g., @HostListener) - NOT elided
275+
// Process method decorators (e.g., @HostListener)
276+
// Angular member decorators are elided; other decorators are kept
247277
for decorator in &method.decorators {
248-
Self::collect_value_uses_from_expr(&decorator.expression, other_value_uses);
278+
Self::collect_uses_from_member_decorator(
279+
&decorator.expression,
280+
member_decorator_uses,
281+
other_value_uses,
282+
);
249283
}
250284

251285
if method.kind == MethodDefinitionKind::Constructor {
@@ -258,9 +292,14 @@ impl<'a> ImportElisionAnalyzer<'a> {
258292
}
259293
}
260294
ClassElement::PropertyDefinition(prop) => {
261-
// Process property decorators (e.g., @Input, @ViewChild) - NOT elided
295+
// Process property decorators (e.g., @Input, @ViewChild, @HostBinding)
296+
// Angular member decorators are elided; other decorators are kept
262297
for decorator in &prop.decorators {
263-
Self::collect_value_uses_from_expr(&decorator.expression, other_value_uses);
298+
Self::collect_uses_from_member_decorator(
299+
&decorator.expression,
300+
member_decorator_uses,
301+
other_value_uses,
302+
);
264303
}
265304
// Process property initializers (e.g., doc = DOCUMENT)
266305
if let Some(init) = &prop.value {
@@ -269,14 +308,56 @@ impl<'a> ImportElisionAnalyzer<'a> {
269308
}
270309
ClassElement::AccessorProperty(prop) => {
271310
for decorator in &prop.decorators {
272-
Self::collect_value_uses_from_expr(&decorator.expression, other_value_uses);
311+
Self::collect_uses_from_member_decorator(
312+
&decorator.expression,
313+
member_decorator_uses,
314+
other_value_uses,
315+
);
273316
}
274317
}
275318
_ => {}
276319
}
277320
}
278321
}
279322

323+
/// Collect uses from a member decorator expression.
324+
///
325+
/// Angular member decorators like `@Input`, `@Output`, `@HostBinding`, `@HostListener`, etc.
326+
/// are compiled into the component/directive definition and can be elided.
327+
/// Non-Angular decorators are tracked as "other value uses" and are not elided.
328+
fn collect_uses_from_member_decorator(
329+
expr: &'a Expression<'a>,
330+
member_decorator_uses: &mut FxHashSet<&'a str>,
331+
other_value_uses: &mut FxHashSet<&'a str>,
332+
) {
333+
let decorator_name = match expr {
334+
// @HostBinding (without call)
335+
Expression::Identifier(id) => Some(id.name.as_str()),
336+
// @HostBinding('class.active') or @Input()
337+
Expression::CallExpression(call) => {
338+
if let Expression::Identifier(id) = &call.callee {
339+
Some(id.name.as_str())
340+
} else {
341+
None
342+
}
343+
}
344+
_ => None,
345+
};
346+
347+
if let Some(name) = decorator_name {
348+
// Check if this is a known Angular member decorator that can be elided
349+
if MEMBER_DECORATORS.contains(&name) {
350+
member_decorator_uses.insert(name);
351+
} else {
352+
// Non-Angular decorator - treat as value use (not elided)
353+
Self::collect_value_uses_from_expr(expr, other_value_uses);
354+
}
355+
} else {
356+
// Fallback: collect as value use
357+
Self::collect_value_uses_from_expr(expr, other_value_uses);
358+
}
359+
}
360+
280361
/// Collect uses from constructor parameters.
281362
///
282363
/// This is the key function that identifies parameter decorators and their arguments.
@@ -667,6 +748,15 @@ pub fn filter_imports<'a>(
667748

668749
let mut result = source.to_string();
669750
for (start, end, replacement) in all_operations {
751+
// Validate that positions are within bounds and on UTF-8 character boundaries
752+
if start > result.len()
753+
|| end > result.len()
754+
|| !result.is_char_boundary(start)
755+
|| !result.is_char_boundary(end)
756+
{
757+
// Skip this operation - it's invalid for the current source state
758+
continue;
759+
}
670760
result.replace_range(start..end, &replacement);
671761
}
672762

@@ -717,9 +807,10 @@ class MyComponent {
717807
}
718808
"#;
719809
let type_only = analyze_source(source);
720-
// Component and Input are used in decorators (value position)
810+
// Component is used as class decorator - preserved
721811
assert!(!type_only.contains("Component"));
722-
assert!(!type_only.contains("Input"));
812+
// Input is a member decorator - elided (compiled into definition)
813+
assert!(type_only.contains("Input"), "Input should be elided (member decorator)");
723814
}
724815

725816
#[test]
@@ -1001,9 +1092,11 @@ class MyComponent {
10011092
"Translation in type annotation should be elided"
10021093
);
10031094

1004-
// Component and Input are used as decorators - value references, preserved
1095+
// Component is the class decorator - preserved
10051096
assert!(!type_only.contains("Component"));
1006-
assert!(!type_only.contains("Input"));
1097+
1098+
// Input is a member decorator - elided (compiled into definition)
1099+
assert!(type_only.contains("Input"), "Input should be elided (member decorator)");
10071100
}
10081101

10091102
#[test]
@@ -1418,11 +1511,14 @@ export class BitLabelComponent {
14181511
"FormControlComponent should be elided (type annotation)"
14191512
);
14201513

1421-
// Component, HostBinding, Input, input should be preserved (runtime decorators/values)
1514+
// Component and input (function call) should be preserved (runtime decorators/values)
1515+
// Note: Component is used as class decorator, input() is a function call
14221516
assert!(!type_only.contains("Component"), "Component should be preserved");
1423-
assert!(!type_only.contains("HostBinding"), "HostBinding should be preserved");
1424-
assert!(!type_only.contains("Input"), "Input should be preserved");
14251517
assert!(!type_only.contains("input"), "input function should be preserved");
1518+
1519+
// HostBinding and Input should be elided (member decorators are compiled away)
1520+
assert!(type_only.contains("HostBinding"), "HostBinding should be elided (member decorator)");
1521+
assert!(type_only.contains("Input"), "Input should be elided (member decorator)");
14261522
}
14271523

14281524
#[test]
@@ -1459,12 +1555,14 @@ export class BitLabelComponent {
14591555
import_line
14601556
);
14611557

1462-
// Should still contain Component, HostBinding, Input, input
1558+
// Component and input (function call) should be in imports
14631559
assert!(import_line.contains("Component"), "Component should be in imports");
1464-
assert!(import_line.contains("HostBinding"), "HostBinding should be in imports");
1465-
assert!(import_line.contains("Input"), "Input should be in imports");
14661560
assert!(import_line.contains("input"), "input should be in imports");
14671561

1562+
// Member decorators should be removed (they're compiled into the definition)
1563+
assert!(!import_line.contains("HostBinding"), "HostBinding should be removed from imports");
1564+
assert!(!import_line.contains("Input"), "Input should be removed from imports");
1565+
14681566
// Should NOT contain ElementRef (type annotation)
14691567
assert!(
14701568
!import_line.contains("ElementRef"),

0 commit comments

Comments
 (0)