Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 110 additions & 1 deletion crates/oxc_angular_compiler/src/component/import_elision.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,8 +235,35 @@ impl<'a> ImportElisionAnalyzer<'a> {
Self::collect_computed_keys_from_ts_type(&array_type.element_type, result);
}
TSType::TSTupleType(tuple_type) => {
// `TSTupleElement` inherits the full set of `TSType`
// variants AND adds three of its own: `TSOptionalType`
// (`[number?]`), `TSRestType` (`[...string[]]`), and
// `TSNamedTupleMember` (`[name: string]`). The previous
// `element.to_ts_type()` call panicked with
// `Option::unwrap() on a None value` whenever a tuple
// contained any of those three variants — common in
// real code, e.g. function signatures expressed as
// tuple types in component decorator metadata.
//
// `TSTupleElement` adds `TSOptionalType` and `TSRestType` on top of the
// inherited `TSType` variants. Use `as_ts_type()` for the common inherited
// path and unpack the two named variants explicitly.
// `TSNamedTupleMember` is an inherited `TSType` variant and is handled in
// the `TSType::TSNamedTupleMember` arm of `collect_computed_keys_from_ts_type`.
for element in &tuple_type.element_types {
Self::collect_computed_keys_from_ts_type(element.to_ts_type(), result);
if let Some(ty) = element.as_ts_type() {
Self::collect_computed_keys_from_ts_type(ty, result);
continue;
}
match element {
oxc_ast::ast::TSTupleElement::TSOptionalType(opt) => {
Self::collect_computed_keys_from_ts_type(&opt.type_annotation, result);
}
oxc_ast::ast::TSTupleElement::TSRestType(rest) => {
Self::collect_computed_keys_from_ts_type(&rest.type_annotation, result);
}
_ => {}
}
}
}
TSType::TSTypeReference(type_ref) => {
Expand All @@ -246,6 +273,11 @@ impl<'a> ImportElisionAnalyzer<'a> {
}
}
}
TSType::TSNamedTupleMember(named) => {
if let Some(inner) = named.element_type.as_ts_type() {
Self::collect_computed_keys_from_ts_type(inner, result);
}
}
TSType::TSParenthesizedType(paren_type) => {
Self::collect_computed_keys_from_ts_type(&paren_type.type_annotation, result);
}
Expand Down Expand Up @@ -1960,6 +1992,83 @@ class MyComponent {
);
}

#[test]
fn test_computed_key_in_optional_tuple_element_preserved() {
// TSOptionalType (`[number?]`) in a tuple previously caused a panic via
// `to_ts_type()` (which called `unwrap()` on None). The fix uses
// `as_ts_type()` and handles the three named TSTupleElement variants.
let source = r#"
import { Component, Input } from '@angular/core';
import { myKey } from './keys';

@Component({ selector: 'test' })
class MyComponent {
@Input() pair: [string?, { [myKey]: number }?];
}
"#;
let type_only = analyze_source(source);
assert!(
!type_only.contains("myKey"),
"myKey in optional tuple element type literal should be preserved"
);
}

#[test]
fn test_computed_key_in_rest_tuple_element_preserved() {
// TSRestType (`[...T[]]`) in a tuple previously caused a panic via
// `to_ts_type()`. The fix explicitly unwraps `TSRestType`.
let source = r#"
import { Component, Input } from '@angular/core';
import { myKey } from './keys';

@Component({ selector: 'test' })
class MyComponent {
@Input() pair: [string, ...{ [myKey]: number }[]];
}
"#;
let type_only = analyze_source(source);
assert!(
!type_only.contains("myKey"),
"myKey inside rest tuple element type should be preserved"
);
}

#[test]
fn test_computed_key_in_named_tuple_member_preserved() {
// TSNamedTupleMember (`[name: T]`) in a tuple previously caused a panic
// via `to_ts_type()`. The fix explicitly unwraps `TSNamedTupleMember`.
let source = r#"
import { Component, Input } from '@angular/core';
import { myKey } from './keys';

@Component({ selector: 'test' })
class MyComponent {
@Input() pair: [label: string, item: { [myKey]: number }];
}
"#;
let type_only = analyze_source(source);
assert!(
!type_only.contains("myKey"),
"myKey in named tuple member type literal should be preserved"
);
}

#[test]
fn test_tuple_with_mixed_element_kinds_no_panic() {
// Regression: a tuple with optional, rest, and named members together
// must not panic (TSOptionalType/TSRestType previously hit the unwrap in to_ts_type).
let source = r#"
import { Component, Input } from '@angular/core';

@Component({ selector: 'test' })
class MyComponent {
@Input() data: [label: string, value?: number, ...rest: string[]];
}
"#;
let type_only = analyze_source(source);
assert!(!type_only.contains("Component"), "Component should be preserved (decorator)");
}

#[test]
fn test_computed_key_in_parenthesized_type_preserved() {
// Review claim: TSParenthesizedType is not handled
Expand Down