@@ -132,12 +132,13 @@ pub fn extract_component_metadata<'a>(
132132 metadata. imports = extract_identifier_array ( allocator, & prop. value ) ;
133133 // 2. The raw expression to pass to ɵɵgetComponentDepsFactory in RuntimeResolved mode
134134 metadata. raw_imports = convert_oxc_expression ( allocator, & prop. value ) ;
135- // 3. Determine if the imports array has any elements (directive dependencies ).
136- // Angular uses: meta.isStandalone && !meta.hasDirectiveDependencies → DomOnly
137- // Without type info, we conservatively assume any non-empty import
138- // could be a directive (not just a pipe) .
135+ // 3. Determine if the imports array has any non-pipe elements (directive deps ).
136+ // Angular's ngtsc (handler.ts:1326-1339) only counts MetaKind.Directive
137+ // and MetaKind.NgModule — NOT MetaKind.Pipe. Without type info, we use
138+ // a naming convention heuristic: identifiers ending in "Pipe" are pipes .
139139 // See: angular/packages/compiler/src/render3/view/compiler.ts:229-232
140- metadata. has_directive_dependencies = has_any_import_elements ( & prop. value ) ;
140+ metadata. has_directive_dependencies =
141+ has_any_non_pipe_import_elements ( & prop. value ) ;
141142 }
142143 "exportAs" => {
143144 // exportAs can be comma-separated: "foo, bar"
@@ -398,24 +399,45 @@ fn extract_string_array<'a>(
398399 Some ( result)
399400}
400401
401- /// Check if an imports expression has any elements that could be directive dependencies.
402+ /// Check if an imports expression has any non-pipe elements ( directive dependencies) .
402403///
403- /// Returns `true` if:
404- /// - The expression is a non-empty array literal (may contain directives)
405- /// - The expression is not an array literal (e.g., a variable reference that could contain anything)
404+ /// Angular's ngtsc (handler.ts:1326-1339) only counts `MetaKind.Directive` and
405+ /// `MetaKind.NgModule` as directive dependencies — NOT `MetaKind.Pipe`.
406406///
407- /// Returns `false` only for empty array literals (`imports: []`).
407+ /// Since OXC is a single-file compiler without type information, we use a naming
408+ /// convention heuristic: identifiers ending in "Pipe" are assumed to be pipes
409+ /// and do NOT count as directive dependencies. This matches the universal Angular
410+ /// convention where all pipe classes are named `*Pipe` (AsyncPipe, DatePipe, etc.).
408411///
409- /// This is a conservative check: without type info, any non-empty import
410- /// could be a directive. Angular's ngtsc has full type info to distinguish
411- /// directives from pipes, but Oxc uses this heuristic.
412- fn has_any_import_elements ( expr : & Expression < ' _ > ) -> bool {
413- match expr {
414- Expression :: ArrayExpression ( arr) => !arr. elements . is_empty ( ) ,
412+ /// Returns `true` (has directive dependencies) if:
413+ /// - The expression is not an array literal (e.g., variable reference — conservatively
414+ /// assumed to potentially contain directives)
415+ /// - The array contains any non-identifier element (spread, function call, etc.)
416+ /// - The array contains any identifier that does NOT end in "Pipe"
417+ ///
418+ /// Returns `false` if:
419+ /// - The expression is an empty array literal (`imports: []`)
420+ /// - ALL elements in the array are identifiers ending in "Pipe"
421+ fn has_any_non_pipe_import_elements ( expr : & Expression < ' _ > ) -> bool {
422+ let Expression :: ArrayExpression ( arr) = expr else {
415423 // Not an array literal (e.g., variable reference like `imports: MY_IMPORTS`)
416424 // Conservatively assume it may contain directives
417- _ => true ,
425+ return true ;
426+ } ;
427+ for element in & arr. elements {
428+ match element {
429+ ArrayExpressionElement :: Identifier ( id) => {
430+ if !id. name . ends_with ( "Pipe" ) {
431+ return true ;
432+ }
433+ }
434+ // Non-identifier elements (spread, call expressions, etc.)
435+ // conservatively treated as potential directives
436+ _ => return true ,
437+ }
418438 }
439+ // All elements are identifiers ending in "Pipe", or the array is empty
440+ false
419441}
420442
421443/// Extract an array of identifiers (for imports).
@@ -3261,4 +3283,146 @@ mod tests {
32613283 ) ;
32623284 } ) ;
32633285 }
3286+
3287+ // =========================================================================
3288+ // Directive dependency detection tests (pipe vs directive imports)
3289+ // =========================================================================
3290+ //
3291+ // Angular's ngtsc (handler.ts:1326-1339) only counts MetaKind.Directive
3292+ // and MetaKind.NgModule as directive dependencies — NOT MetaKind.Pipe.
3293+ // Since OXC is a single-file compiler, we use a naming convention heuristic:
3294+ // identifiers ending in "Pipe" are assumed to be pipes.
3295+
3296+ #[ test]
3297+ fn test_pipe_only_imports_no_directive_dependencies ( ) {
3298+ let code = r#"
3299+ @Component({
3300+ selector: 'app-test',
3301+ standalone: true,
3302+ imports: [AsyncPipe],
3303+ template: ''
3304+ })
3305+ class TestComponent {}
3306+ "# ;
3307+ assert_metadata ( code, |meta| {
3308+ assert ! (
3309+ !meta. has_directive_dependencies,
3310+ "Pipe-only imports should not set has_directive_dependencies"
3311+ ) ;
3312+ } ) ;
3313+ }
3314+
3315+ #[ test]
3316+ fn test_multiple_pipe_imports_no_directive_dependencies ( ) {
3317+ let code = r#"
3318+ @Component({
3319+ selector: 'app-test',
3320+ standalone: true,
3321+ imports: [AsyncPipe, DatePipe, SlicePipe, KeyValuePipe],
3322+ template: ''
3323+ })
3324+ class TestComponent {}
3325+ "# ;
3326+ assert_metadata ( code, |meta| {
3327+ assert ! (
3328+ !meta. has_directive_dependencies,
3329+ "Multiple pipe-only imports should not set has_directive_dependencies"
3330+ ) ;
3331+ } ) ;
3332+ }
3333+
3334+ #[ test]
3335+ fn test_mixed_pipe_and_directive_imports_has_directive_dependencies ( ) {
3336+ let code = r#"
3337+ @Component({
3338+ selector: 'app-test',
3339+ standalone: true,
3340+ imports: [AsyncPipe, HighlightDirective],
3341+ template: ''
3342+ })
3343+ class TestComponent {}
3344+ "# ;
3345+ assert_metadata ( code, |meta| {
3346+ assert ! (
3347+ meta. has_directive_dependencies,
3348+ "Mixed imports with non-pipe should set has_directive_dependencies"
3349+ ) ;
3350+ } ) ;
3351+ }
3352+
3353+ #[ test]
3354+ fn test_directive_only_imports_has_directive_dependencies ( ) {
3355+ let code = r#"
3356+ @Component({
3357+ selector: 'app-test',
3358+ standalone: true,
3359+ imports: [HighlightDirective, RouterModule],
3360+ template: ''
3361+ })
3362+ class TestComponent {}
3363+ "# ;
3364+ assert_metadata ( code, |meta| {
3365+ assert ! (
3366+ meta. has_directive_dependencies,
3367+ "Directive-only imports should set has_directive_dependencies"
3368+ ) ;
3369+ } ) ;
3370+ }
3371+
3372+ #[ test]
3373+ fn test_empty_imports_no_directive_dependencies ( ) {
3374+ let code = r#"
3375+ @Component({
3376+ selector: 'app-test',
3377+ standalone: true,
3378+ imports: [],
3379+ template: ''
3380+ })
3381+ class TestComponent {}
3382+ "# ;
3383+ assert_metadata ( code, |meta| {
3384+ assert ! (
3385+ !meta. has_directive_dependencies,
3386+ "Empty imports should not set has_directive_dependencies"
3387+ ) ;
3388+ } ) ;
3389+ }
3390+
3391+ #[ test]
3392+ fn test_variable_imports_has_directive_dependencies ( ) {
3393+ let code = r#"
3394+ @Component({
3395+ selector: 'app-test',
3396+ standalone: true,
3397+ imports: MY_IMPORTS,
3398+ template: ''
3399+ })
3400+ class TestComponent {}
3401+ "# ;
3402+ assert_metadata ( code, |meta| {
3403+ assert ! (
3404+ meta. has_directive_dependencies,
3405+ "Variable imports should conservatively set has_directive_dependencies"
3406+ ) ;
3407+ } ) ;
3408+ }
3409+
3410+ #[ test]
3411+ fn test_spread_in_imports_has_directive_dependencies ( ) {
3412+ let code = r#"
3413+ @Component({
3414+ selector: 'app-test',
3415+ standalone: true,
3416+ imports: [...SHARED_IMPORTS, AsyncPipe],
3417+ template: ''
3418+ })
3419+ class TestComponent {}
3420+ "# ;
3421+ assert_metadata ( code, |meta| {
3422+ assert ! (
3423+ meta. has_directive_dependencies,
3424+ "Spread in imports should conservatively set has_directive_dependencies"
3425+ ) ;
3426+ } ) ;
3427+ }
32643428}
0 commit comments