Skip to content

Commit 9724ae5

Browse files
Brooooooklynclaude
andauthored
fix(angular): preserve block-body functions in decorator providers (#205)
* fix(angular): preserve block-body functions in decorator providers Block-body arrow functions and function expressions in decorator properties (e.g., useFactory) were silently having unsupported statements dropped. Only return and expression statements survived, corrupting the function body and causing runtime errors. Add RawSource fallback: when convert_oxc_expression encounters a block-body arrow with unsupported statement types (const, if, for, try/catch, etc.) or a function expression, it preserves the complete source text verbatim via span slicing instead of silently dropping statements. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(angular): pass source_text through build_decorator_metadata_array Thread source_text into class metadata builder so decorator arguments containing block-body arrows/function expressions are preserved via RawSource fallback instead of being silently dropped. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(angular): thread source_text through remaining metadata builders - build_ctor_params_metadata: pass source_text so @Inject(...) args with complex expressions are preserved in ɵsetClassMetadata - build_prop_decorators_metadata: pass source_text so @input({...}) with complex transform functions are preserved - extract_provided_in: propagate source_text from parent extract_injectable_metadata into forwardRef extraction Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(angular): strip TS types from RawSource, add expression-body fallback, thread source_text through property decorators Three reviewer fixes: 1. P1 - RawSource now strips TypeScript type annotations via parse-transform-codegen pipeline, preventing invalid JS output like `(dep: Dep) => { ... }` in generated code. 2. Medium - Expression-body arrows with unsupported inner expressions (e.g., `() => someUnsupportedExpr`) now fall back to RawSource instead of returning None. 3. P2 - Thread source_text through property_decorators.rs so @input({transform}), @ViewChild, @ContentChild arguments with complex expressions are preserved via RawSource fallback. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(angular): use module source type and add fast path for TS stripping - Use SourceType::ts().with_module(true) instead of script-mode ts() so import.meta and ESM-only syntax parse correctly in RawSource fallback expressions. - Add fast path: try parsing as .mjs first. If the expression is already valid JavaScript (no type annotations), return it as-is without running the heavier semantic→transform→codegen pipeline. Only expressions with actual TypeScript syntax pay the full cost. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 7d4e9a0 commit 9724ae5

File tree

22 files changed

+780
-242
lines changed

22 files changed

+780
-242
lines changed

crates/oxc_angular_compiler/src/class_metadata/builders.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ use crate::output::oxc_converter::convert_oxc_expression;
2323
pub fn build_decorator_metadata_array<'a>(
2424
allocator: &'a Allocator,
2525
decorators: &[&Decorator<'a>],
26+
source_text: Option<&'a str>,
2627
) -> OutputExpression<'a> {
2728
let mut decorator_entries = AllocVec::new_in(allocator);
2829

@@ -38,7 +39,7 @@ pub fn build_decorator_metadata_array<'a>(
3839
))),
3940
Expression::StaticMemberExpression(member) => {
4041
// Handle namespaced decorators like ng.Component
41-
convert_oxc_expression(allocator, &member.object).map(|receiver| {
42+
convert_oxc_expression(allocator, &member.object, source_text).map(|receiver| {
4243
OutputExpression::ReadProp(Box::new_in(
4344
ReadPropExpr {
4445
receiver: Box::new_in(receiver, allocator),
@@ -77,7 +78,7 @@ pub fn build_decorator_metadata_array<'a>(
7778
let mut args = AllocVec::new_in(allocator);
7879
for arg in &call.arguments {
7980
let expr = arg.to_expression();
80-
if let Some(converted) = convert_oxc_expression(allocator, expr) {
81+
if let Some(converted) = convert_oxc_expression(allocator, expr, source_text) {
8182
args.push(converted);
8283
}
8384
}
@@ -122,6 +123,7 @@ pub fn build_ctor_params_metadata<'a>(
122123
constructor_deps: Option<&[R3DependencyMetadata<'a>]>,
123124
namespace_registry: &mut NamespaceRegistry<'a>,
124125
import_map: &ImportMap<'a>,
126+
source_text: Option<&'a str>,
125127
) -> Option<OutputExpression<'a>> {
126128
// Find constructor
127129
let constructor = class.body.body.iter().find_map(|element| {
@@ -163,7 +165,8 @@ pub fn build_ctor_params_metadata<'a>(
163165
// Extract decorators from the parameter
164166
let param_decorators = extract_angular_decorators_from_param(param);
165167
if !param_decorators.is_empty() {
166-
let decorators_array = build_decorator_metadata_array(allocator, &param_decorators);
168+
let decorators_array =
169+
build_decorator_metadata_array(allocator, &param_decorators, source_text);
167170
map_entries.push(LiteralMapEntry {
168171
key: Ident::from("decorators"),
169172
value: decorators_array,
@@ -205,6 +208,7 @@ pub fn build_ctor_params_metadata<'a>(
205208
pub fn build_prop_decorators_metadata<'a>(
206209
allocator: &'a Allocator,
207210
class: &Class<'a>,
211+
source_text: Option<&'a str>,
208212
) -> Option<OutputExpression<'a>> {
209213
const ANGULAR_PROP_DECORATORS: &[&str] = &[
210214
"Input",
@@ -251,7 +255,8 @@ pub fn build_prop_decorators_metadata<'a>(
251255
}
252256

253257
// Build decorators array for this property
254-
let decorators_array = build_decorator_metadata_array(allocator, &angular_decorators);
258+
let decorators_array =
259+
build_decorator_metadata_array(allocator, &angular_decorators, source_text);
255260

256261
prop_entries.push(LiteralMapEntry {
257262
key: prop_name,

crates/oxc_angular_compiler/src/component/decorator.rs

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ pub fn extract_component_metadata<'a>(
5050
class: &'a Class<'a>,
5151
implicit_standalone: bool,
5252
import_map: &ImportMap<'a>,
53+
source_text: Option<&'a str>,
5354
) -> Option<ComponentMetadata<'a>> {
5455
// Get the class name
5556
let class_name: Ident<'a> = class.id.as_ref()?.name.clone().into();
@@ -130,7 +131,8 @@ pub fn extract_component_metadata<'a>(
130131
// 1. The identifier list for local analysis
131132
metadata.imports = extract_identifier_array(allocator, &prop.value);
132133
// 2. The raw expression to pass to ɵɵgetComponentDepsFactory in RuntimeResolved mode
133-
metadata.raw_imports = convert_oxc_expression(allocator, &prop.value);
134+
metadata.raw_imports =
135+
convert_oxc_expression(allocator, &prop.value, source_text);
134136
}
135137
"exportAs" => {
136138
// exportAs can be comma-separated: "foo, bar"
@@ -150,7 +152,8 @@ pub fn extract_component_metadata<'a>(
150152
"animations" => {
151153
// Extract animations expression as full OutputExpression
152154
// Handles both identifier references and complex array expressions
153-
metadata.animations = convert_oxc_expression(allocator, &prop.value);
155+
metadata.animations =
156+
convert_oxc_expression(allocator, &prop.value, source_text);
154157
}
155158
"schemas" => {
156159
// Extract schemas identifiers (e.g., [CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA])
@@ -159,11 +162,13 @@ pub fn extract_component_metadata<'a>(
159162
"providers" => {
160163
// Extract providers as full OutputExpression
161164
// Handles complex expressions like [{provide: TOKEN, useFactory: Factory}]
162-
metadata.providers = convert_oxc_expression(allocator, &prop.value);
165+
metadata.providers =
166+
convert_oxc_expression(allocator, &prop.value, source_text);
163167
}
164168
"viewProviders" => {
165169
// Extract view providers as full OutputExpression
166-
metadata.view_providers = convert_oxc_expression(allocator, &prop.value);
170+
metadata.view_providers =
171+
convert_oxc_expression(allocator, &prop.value, source_text);
167172
}
168173
"hostDirectives" => {
169174
// Extract host directives array
@@ -236,7 +241,7 @@ pub fn extract_component_metadata<'a>(
236241
extract_constructor_deps(allocator, class, import_map, has_superclass);
237242

238243
// Extract inputs from @Input decorators on class members
239-
metadata.inputs = extract_input_metadata(allocator, class);
244+
metadata.inputs = extract_input_metadata(allocator, class, source_text);
240245

241246
// Extract outputs from @Output decorators on class members
242247
metadata.outputs = extract_output_metadata(allocator, class);
@@ -1134,9 +1139,13 @@ mod tests {
11341139
};
11351140

11361141
if let Some(class) = class {
1137-
if let Some(metadata) =
1138-
extract_component_metadata(&allocator, class, implicit_standalone, &import_map)
1139-
{
1142+
if let Some(metadata) = extract_component_metadata(
1143+
&allocator,
1144+
class,
1145+
implicit_standalone,
1146+
&import_map,
1147+
Some(code),
1148+
) {
11401149
found_metadata = Some(metadata);
11411150
break;
11421151
}

crates/oxc_angular_compiler/src/component/transform.rs

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1616,9 +1616,13 @@ pub fn transform_angular_file(
16161616
// Compute implicit_standalone based on Angular version
16171617
let implicit_standalone = options.implicit_standalone();
16181618

1619-
if let Some(mut metadata) =
1620-
extract_component_metadata(allocator, class, implicit_standalone, &import_map)
1621-
{
1619+
if let Some(mut metadata) = extract_component_metadata(
1620+
allocator,
1621+
class,
1622+
implicit_standalone,
1623+
&import_map,
1624+
Some(source),
1625+
) {
16221626
// 3. Resolve external styles and merge into metadata
16231627
resolve_styles(allocator, &mut metadata, resolved_resources);
16241628

@@ -1632,11 +1636,11 @@ pub fn transform_angular_file(
16321636
let template = allocator.alloc_str(&template_string);
16331637
// 4.5 Extract view queries from the class (for @ViewChild/@ViewChildren)
16341638
// These need to be passed to compile_component_full so predicates can be pooled
1635-
let view_queries = extract_view_queries(allocator, class);
1639+
let view_queries = extract_view_queries(allocator, class, Some(source));
16361640

16371641
// 4.6 Extract content queries from the class (for @ContentChild/@ContentChildren)
16381642
// Signal-based queries (contentChild(), contentChildren()) are also detected here
1639-
let content_queries = extract_content_queries(allocator, class);
1643+
let content_queries = extract_content_queries(allocator, class, Some(source));
16401644

16411645
// Collect content query property names for .d.ts generation
16421646
// (before content_queries is moved into compile_component_full)
@@ -1696,7 +1700,8 @@ pub fn transform_angular_file(
16961700

16971701
// Check if the class also has an @Injectable decorator.
16981702
// @Injectable is SHARED precedence and can coexist with @Component.
1699-
let has_injectable = extract_injectable_metadata(allocator, class);
1703+
let has_injectable =
1704+
extract_injectable_metadata(allocator, class, Some(source));
17001705
if let Some(injectable_metadata) = &has_injectable {
17011706
if let Some(span) = find_injectable_decorator_span(class) {
17021707
decorator_spans_to_remove.push(span);
@@ -1758,16 +1763,20 @@ pub fn transform_angular_file(
17581763
decorators: build_decorator_metadata_array(
17591764
allocator,
17601765
&[decorator],
1766+
Some(source),
17611767
),
17621768
ctor_parameters: build_ctor_params_metadata(
17631769
allocator,
17641770
class,
17651771
ctor_deps_slice,
17661772
&mut file_namespace_registry,
17671773
&import_map,
1774+
Some(source),
17681775
),
17691776
prop_decorators: build_prop_decorators_metadata(
1770-
allocator, class,
1777+
allocator,
1778+
class,
1779+
Some(source),
17711780
),
17721781
};
17731782

@@ -1848,7 +1857,7 @@ pub fn transform_angular_file(
18481857
// the directive and creating conflicting property definitions (like
18491858
// ɵfac getters) that interfere with the AOT-compiled assignments.
18501859
if let Some(mut directive_metadata) =
1851-
extract_directive_metadata(allocator, class, implicit_standalone)
1860+
extract_directive_metadata(allocator, class, implicit_standalone, Some(source))
18521861
{
18531862
// Track decorator span for removal
18541863
if let Some(span) = find_directive_decorator_span(class) {
@@ -1906,7 +1915,8 @@ pub fn transform_angular_file(
19061915

19071916
// Check if the class also has an @Injectable decorator.
19081917
// @Injectable is SHARED precedence and can coexist with @Directive.
1909-
let has_injectable = extract_injectable_metadata(allocator, class);
1918+
let has_injectable =
1919+
extract_injectable_metadata(allocator, class, Some(source));
19101920
if let Some(injectable_metadata) = &has_injectable {
19111921
if let Some(span) = find_injectable_decorator_span(class) {
19121922
decorator_spans_to_remove.push(span);
@@ -1939,7 +1949,7 @@ pub fn transform_angular_file(
19391949
class_definitions
19401950
.insert(class_name, (property_assignments, String::new(), String::new()));
19411951
} else if let Some(mut pipe_metadata) =
1942-
extract_pipe_metadata(allocator, class, implicit_standalone)
1952+
extract_pipe_metadata(allocator, class, implicit_standalone, Some(source))
19431953
{
19441954
// Not a @Component or @Directive - check if it's a @Pipe (PRIMARY)
19451955
// We need to compile @Pipe classes to generate ɵpipe and ɵfac definitions.
@@ -1980,7 +1990,8 @@ pub fn transform_angular_file(
19801990

19811991
// Check if the class also has an @Injectable decorator (issue #65).
19821992
// @Injectable is SHARED precedence and can coexist with @Pipe.
1983-
let has_injectable = extract_injectable_metadata(allocator, class);
1993+
let has_injectable =
1994+
extract_injectable_metadata(allocator, class, Some(source));
19841995
if let Some(injectable_metadata) = &has_injectable {
19851996
if let Some(span) = find_injectable_decorator_span(class) {
19861997
decorator_spans_to_remove.push(span);
@@ -2017,7 +2028,7 @@ pub fn transform_angular_file(
20172028
);
20182029
}
20192030
} else if let Some(mut ng_module_metadata) =
2020-
extract_ng_module_metadata(allocator, class)
2031+
extract_ng_module_metadata(allocator, class, Some(source))
20212032
{
20222033
// Not a @Component, @Directive, @Injectable, or @Pipe - check if it's an @NgModule
20232034
// We need to compile @NgModule classes to generate ɵmod, ɵfac, and ɵinj definitions.
@@ -2061,7 +2072,8 @@ pub fn transform_angular_file(
20612072

20622073
// Check if the class also has an @Injectable decorator.
20632074
// @Injectable is SHARED precedence and can coexist with @NgModule.
2064-
let has_injectable = extract_injectable_metadata(allocator, class);
2075+
let has_injectable =
2076+
extract_injectable_metadata(allocator, class, Some(source));
20652077
if let Some(injectable_metadata) = &has_injectable {
20662078
if let Some(span) = find_injectable_decorator_span(class) {
20672079
decorator_spans_to_remove.push(span);
@@ -2108,7 +2120,7 @@ pub fn transform_angular_file(
21082120
);
21092121
}
21102122
} else if let Some(mut injectable_metadata) =
2111-
extract_injectable_metadata(allocator, class)
2123+
extract_injectable_metadata(allocator, class, Some(source))
21122124
{
21132125
// Standalone @Injectable (no PRIMARY decorator on the class)
21142126
// We need to compile @Injectable classes to generate ɵprov and ɵfac definitions.

crates/oxc_angular_compiler/src/directive/decorator.rs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ pub fn extract_directive_metadata<'a>(
7777
allocator: &'a Allocator,
7878
class: &'a Class<'a>,
7979
implicit_standalone: bool,
80+
source_text: Option<&'a str>,
8081
) -> Option<R3DirectiveMetadata<'a>> {
8182
// Get the class name
8283
let class_name: Ident<'a> = class.id.as_ref()?.name.clone().into();
@@ -142,7 +143,9 @@ pub fn extract_directive_metadata<'a>(
142143
}
143144
}
144145
"providers" => {
145-
if let Some(providers) = convert_oxc_expression(allocator, &prop.value) {
146+
if let Some(providers) =
147+
convert_oxc_expression(allocator, &prop.value, source_text)
148+
{
146149
builder = builder.providers(providers);
147150
}
148151
}
@@ -164,7 +167,7 @@ pub fn extract_directive_metadata<'a>(
164167
}
165168

166169
// Extract @Input/@Output/@HostBinding/@HostListener from class members
167-
builder = builder.extract_from_class(allocator, class);
170+
builder = builder.extract_from_class(allocator, class, source_text);
168171

169172
// Detect if ngOnChanges lifecycle hook is implemented
170173
// Similar to Angular's: const usesOnChanges = members.some(member => ...)
@@ -180,7 +183,7 @@ pub fn extract_directive_metadata<'a>(
180183
// Extract constructor dependencies for factory generation
181184
// This enables proper DI for directive constructors
182185
// See: packages/compiler-cli/src/ngtsc/annotations/common/src/di.ts
183-
let constructor_deps = extract_constructor_deps(allocator, class, has_superclass);
186+
let constructor_deps = extract_constructor_deps(allocator, class, has_superclass, source_text);
184187
if let Some(deps) = constructor_deps {
185188
builder = builder.deps(deps);
186189
}
@@ -252,6 +255,7 @@ fn extract_constructor_deps<'a>(
252255
allocator: &'a Allocator,
253256
class: &'a Class<'a>,
254257
has_superclass: bool,
258+
source_text: Option<&'a str>,
255259
) -> Option<Vec<'a, R3DependencyMetadata<'a>>> {
256260
// Find the constructor method
257261
let constructor = class.body.body.iter().find_map(|element| {
@@ -270,7 +274,7 @@ fn extract_constructor_deps<'a>(
270274
let mut deps = Vec::with_capacity_in(params.items.len(), allocator);
271275

272276
for param in &params.items {
273-
let dep = extract_param_dependency(allocator, param);
277+
let dep = extract_param_dependency(allocator, param, source_text);
274278
deps.push(dep);
275279
}
276280

@@ -290,6 +294,7 @@ fn extract_constructor_deps<'a>(
290294
fn extract_param_dependency<'a>(
291295
allocator: &'a Allocator,
292296
param: &oxc_ast::ast::FormalParameter<'a>,
297+
source_text: Option<&'a str>,
293298
) -> R3DependencyMetadata<'a> {
294299
// Extract flags and @Inject token from decorators
295300
let mut optional = false;
@@ -306,7 +311,8 @@ fn extract_param_dependency<'a>(
306311
// @Inject(TOKEN) - extract the token
307312
if let Expression::CallExpression(call) = &decorator.expression {
308313
if let Some(arg) = call.arguments.first() {
309-
inject_token = convert_oxc_expression(allocator, arg.to_expression());
314+
inject_token =
315+
convert_oxc_expression(allocator, arg.to_expression(), source_text);
310316
}
311317
}
312318
}
@@ -885,7 +891,7 @@ mod tests {
885891

886892
if let Some(class) = class {
887893
if let Some(metadata) =
888-
extract_directive_metadata(&allocator, class, implicit_standalone)
894+
extract_directive_metadata(&allocator, class, implicit_standalone, Some(code))
889895
{
890896
found_metadata = Some(metadata);
891897
break;

0 commit comments

Comments
 (0)