Skip to content

Commit 557ff9a

Browse files
Brooooooklynclaude
andcommitted
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>
1 parent 7d4e9a0 commit 557ff9a

File tree

20 files changed

+496
-134
lines changed

20 files changed

+496
-134
lines changed

crates/oxc_angular_compiler/src/class_metadata/builders.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ pub fn build_decorator_metadata_array<'a>(
3838
))),
3939
Expression::StaticMemberExpression(member) => {
4040
// Handle namespaced decorators like ng.Component
41-
convert_oxc_expression(allocator, &member.object).map(|receiver| {
41+
convert_oxc_expression(allocator, &member.object, None).map(|receiver| {
4242
OutputExpression::ReadProp(Box::new_in(
4343
ReadPropExpr {
4444
receiver: Box::new_in(receiver, allocator),
@@ -77,7 +77,7 @@ pub fn build_decorator_metadata_array<'a>(
7777
let mut args = AllocVec::new_in(allocator);
7878
for arg in &call.arguments {
7979
let expr = arg.to_expression();
80-
if let Some(converted) = convert_oxc_expression(allocator, expr) {
80+
if let Some(converted) = convert_oxc_expression(allocator, expr, None) {
8181
args.push(converted);
8282
}
8383
}

crates/oxc_angular_compiler/src/component/decorator.rs

Lines changed: 16 additions & 7 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
@@ -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: 19 additions & 11 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

@@ -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);
@@ -1848,7 +1853,7 @@ pub fn transform_angular_file(
18481853
// the directive and creating conflicting property definitions (like
18491854
// ɵfac getters) that interfere with the AOT-compiled assignments.
18501855
if let Some(mut directive_metadata) =
1851-
extract_directive_metadata(allocator, class, implicit_standalone)
1856+
extract_directive_metadata(allocator, class, implicit_standalone, Some(source))
18521857
{
18531858
// Track decorator span for removal
18541859
if let Some(span) = find_directive_decorator_span(class) {
@@ -1906,7 +1911,8 @@ pub fn transform_angular_file(
19061911

19071912
// Check if the class also has an @Injectable decorator.
19081913
// @Injectable is SHARED precedence and can coexist with @Directive.
1909-
let has_injectable = extract_injectable_metadata(allocator, class);
1914+
let has_injectable =
1915+
extract_injectable_metadata(allocator, class, Some(source));
19101916
if let Some(injectable_metadata) = &has_injectable {
19111917
if let Some(span) = find_injectable_decorator_span(class) {
19121918
decorator_spans_to_remove.push(span);
@@ -1939,7 +1945,7 @@ pub fn transform_angular_file(
19391945
class_definitions
19401946
.insert(class_name, (property_assignments, String::new(), String::new()));
19411947
} else if let Some(mut pipe_metadata) =
1942-
extract_pipe_metadata(allocator, class, implicit_standalone)
1948+
extract_pipe_metadata(allocator, class, implicit_standalone, Some(source))
19431949
{
19441950
// Not a @Component or @Directive - check if it's a @Pipe (PRIMARY)
19451951
// We need to compile @Pipe classes to generate ɵpipe and ɵfac definitions.
@@ -1980,7 +1986,8 @@ pub fn transform_angular_file(
19801986

19811987
// Check if the class also has an @Injectable decorator (issue #65).
19821988
// @Injectable is SHARED precedence and can coexist with @Pipe.
1983-
let has_injectable = extract_injectable_metadata(allocator, class);
1989+
let has_injectable =
1990+
extract_injectable_metadata(allocator, class, Some(source));
19841991
if let Some(injectable_metadata) = &has_injectable {
19851992
if let Some(span) = find_injectable_decorator_span(class) {
19861993
decorator_spans_to_remove.push(span);
@@ -2017,7 +2024,7 @@ pub fn transform_angular_file(
20172024
);
20182025
}
20192026
} else if let Some(mut ng_module_metadata) =
2020-
extract_ng_module_metadata(allocator, class)
2027+
extract_ng_module_metadata(allocator, class, Some(source))
20212028
{
20222029
// Not a @Component, @Directive, @Injectable, or @Pipe - check if it's an @NgModule
20232030
// We need to compile @NgModule classes to generate ɵmod, ɵfac, and ɵinj definitions.
@@ -2061,7 +2068,8 @@ pub fn transform_angular_file(
20612068

20622069
// Check if the class also has an @Injectable decorator.
20632070
// @Injectable is SHARED precedence and can coexist with @NgModule.
2064-
let has_injectable = extract_injectable_metadata(allocator, class);
2071+
let has_injectable =
2072+
extract_injectable_metadata(allocator, class, Some(source));
20652073
if let Some(injectable_metadata) = &has_injectable {
20662074
if let Some(span) = find_injectable_decorator_span(class) {
20672075
decorator_spans_to_remove.push(span);
@@ -2108,7 +2116,7 @@ pub fn transform_angular_file(
21082116
);
21092117
}
21102118
} else if let Some(mut injectable_metadata) =
2111-
extract_injectable_metadata(allocator, class)
2119+
extract_injectable_metadata(allocator, class, Some(source))
21122120
{
21132121
// Standalone @Injectable (no PRIMARY decorator on the class)
21142122
// We need to compile @Injectable classes to generate ɵprov and ɵfac definitions.

crates/oxc_angular_compiler/src/directive/decorator.rs

Lines changed: 11 additions & 5 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
}
@@ -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;

crates/oxc_angular_compiler/src/directive/property_decorators.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ fn parse_input_config<'a>(
183183
config.required = extract_boolean_value(&prop.value).unwrap_or(false);
184184
}
185185
"transform" => {
186-
config.transform = convert_oxc_expression(allocator, &prop.value);
186+
config.transform = convert_oxc_expression(allocator, &prop.value, None);
187187
}
188188
_ => {}
189189
}
@@ -779,7 +779,7 @@ fn parse_query_config<'a>(
779779
let expr = first_arg.to_expression();
780780
// Unwrap forwardRef if present - Angular doesn't include forwardRef in compiled output
781781
let unwrapped_expr = try_unwrap_forward_ref(expr).unwrap_or(expr);
782-
if let Some(output_expr) = convert_oxc_expression(allocator, unwrapped_expr) {
782+
if let Some(output_expr) = convert_oxc_expression(allocator, unwrapped_expr, None) {
783783
config.predicate = Some(QueryPredicate::Type(output_expr));
784784
}
785785
}
@@ -799,7 +799,7 @@ fn parse_query_config<'a>(
799799
config.is_static = extract_boolean_value(&prop.value).unwrap_or(false);
800800
}
801801
"read" => {
802-
config.read = convert_oxc_expression(allocator, &prop.value);
802+
config.read = convert_oxc_expression(allocator, &prop.value, None);
803803
}
804804
"descendants" => {
805805
// Use the decorator-specific default if not explicitly set
@@ -941,7 +941,7 @@ fn try_parse_signal_query<'a>(
941941
let expr = predicate_arg.to_expression();
942942
// Unwrap forwardRef if present - Angular doesn't include forwardRef in compiled output
943943
let unwrapped_expr = try_unwrap_forward_ref(expr).unwrap_or(expr);
944-
let output_expr = convert_oxc_expression(allocator, unwrapped_expr)?;
944+
let output_expr = convert_oxc_expression(allocator, unwrapped_expr, None)?;
945945
QueryPredicate::Type(output_expr)
946946
}
947947
};
@@ -960,7 +960,7 @@ fn try_parse_signal_query<'a>(
960960

961961
match key_name.as_str() {
962962
"read" => {
963-
read = convert_oxc_expression(allocator, &prop.value);
963+
read = convert_oxc_expression(allocator, &prop.value, None);
964964
}
965965
"descendants" => {
966966
descendants = extract_boolean_value(&prop.value).unwrap_or(descendants);

0 commit comments

Comments
 (0)