Skip to content

Commit 3cf7359

Browse files
fix(import_elision): handle tuple type elements without panicking (#272)
* fix(import_elision): handle tuple type elements without panicking `TSTupleElement::to_ts_type()` panicked when the element was `TSOptionalType` or `TSRestType` (discriminants outside the inherited `TSType` range). Switch to `as_ts_type()` for safety and add explicit branches for those two variants. Also adds `TSType::TSNamedTupleMember` handling in `collect_computed_keys_from_ts_type`: named tuple members are inherited `TSType` variants so they arrive there via `as_ts_type()`, not the match arm that was previously added for them (which was unreachable). Without this, computed property keys inside named tuple member element types were silently not traversed and their imports were incorrectly elided. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(import_elision): remove unreachable TSNamedTupleMember match arm TSNamedTupleMember is an inherited TSType variant so as_ts_type() always returns Some for it — the match arm added in the TSTupleType loop was dead code. Traversal is handled by the TSNamedTupleMember arm in collect_computed_keys_from_ts_type. Clean up the stale comment too. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 6d7b2ad commit 3cf7359

1 file changed

Lines changed: 110 additions & 1 deletion

File tree

crates/oxc_angular_compiler/src/component/import_elision.rs

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,8 +235,35 @@ impl<'a> ImportElisionAnalyzer<'a> {
235235
Self::collect_computed_keys_from_ts_type(&array_type.element_type, result);
236236
}
237237
TSType::TSTupleType(tuple_type) => {
238+
// `TSTupleElement` inherits the full set of `TSType`
239+
// variants AND adds three of its own: `TSOptionalType`
240+
// (`[number?]`), `TSRestType` (`[...string[]]`), and
241+
// `TSNamedTupleMember` (`[name: string]`). The previous
242+
// `element.to_ts_type()` call panicked with
243+
// `Option::unwrap() on a None value` whenever a tuple
244+
// contained any of those three variants — common in
245+
// real code, e.g. function signatures expressed as
246+
// tuple types in component decorator metadata.
247+
//
248+
// `TSTupleElement` adds `TSOptionalType` and `TSRestType` on top of the
249+
// inherited `TSType` variants. Use `as_ts_type()` for the common inherited
250+
// path and unpack the two named variants explicitly.
251+
// `TSNamedTupleMember` is an inherited `TSType` variant and is handled in
252+
// the `TSType::TSNamedTupleMember` arm of `collect_computed_keys_from_ts_type`.
238253
for element in &tuple_type.element_types {
239-
Self::collect_computed_keys_from_ts_type(element.to_ts_type(), result);
254+
if let Some(ty) = element.as_ts_type() {
255+
Self::collect_computed_keys_from_ts_type(ty, result);
256+
continue;
257+
}
258+
match element {
259+
oxc_ast::ast::TSTupleElement::TSOptionalType(opt) => {
260+
Self::collect_computed_keys_from_ts_type(&opt.type_annotation, result);
261+
}
262+
oxc_ast::ast::TSTupleElement::TSRestType(rest) => {
263+
Self::collect_computed_keys_from_ts_type(&rest.type_annotation, result);
264+
}
265+
_ => {}
266+
}
240267
}
241268
}
242269
TSType::TSTypeReference(type_ref) => {
@@ -246,6 +273,11 @@ impl<'a> ImportElisionAnalyzer<'a> {
246273
}
247274
}
248275
}
276+
TSType::TSNamedTupleMember(named) => {
277+
if let Some(inner) = named.element_type.as_ts_type() {
278+
Self::collect_computed_keys_from_ts_type(inner, result);
279+
}
280+
}
249281
TSType::TSParenthesizedType(paren_type) => {
250282
Self::collect_computed_keys_from_ts_type(&paren_type.type_annotation, result);
251283
}
@@ -1960,6 +1992,83 @@ class MyComponent {
19601992
);
19611993
}
19621994

1995+
#[test]
1996+
fn test_computed_key_in_optional_tuple_element_preserved() {
1997+
// TSOptionalType (`[number?]`) in a tuple previously caused a panic via
1998+
// `to_ts_type()` (which called `unwrap()` on None). The fix uses
1999+
// `as_ts_type()` and handles the three named TSTupleElement variants.
2000+
let source = r#"
2001+
import { Component, Input } from '@angular/core';
2002+
import { myKey } from './keys';
2003+
2004+
@Component({ selector: 'test' })
2005+
class MyComponent {
2006+
@Input() pair: [string?, { [myKey]: number }?];
2007+
}
2008+
"#;
2009+
let type_only = analyze_source(source);
2010+
assert!(
2011+
!type_only.contains("myKey"),
2012+
"myKey in optional tuple element type literal should be preserved"
2013+
);
2014+
}
2015+
2016+
#[test]
2017+
fn test_computed_key_in_rest_tuple_element_preserved() {
2018+
// TSRestType (`[...T[]]`) in a tuple previously caused a panic via
2019+
// `to_ts_type()`. The fix explicitly unwraps `TSRestType`.
2020+
let source = r#"
2021+
import { Component, Input } from '@angular/core';
2022+
import { myKey } from './keys';
2023+
2024+
@Component({ selector: 'test' })
2025+
class MyComponent {
2026+
@Input() pair: [string, ...{ [myKey]: number }[]];
2027+
}
2028+
"#;
2029+
let type_only = analyze_source(source);
2030+
assert!(
2031+
!type_only.contains("myKey"),
2032+
"myKey inside rest tuple element type should be preserved"
2033+
);
2034+
}
2035+
2036+
#[test]
2037+
fn test_computed_key_in_named_tuple_member_preserved() {
2038+
// TSNamedTupleMember (`[name: T]`) in a tuple previously caused a panic
2039+
// via `to_ts_type()`. The fix explicitly unwraps `TSNamedTupleMember`.
2040+
let source = r#"
2041+
import { Component, Input } from '@angular/core';
2042+
import { myKey } from './keys';
2043+
2044+
@Component({ selector: 'test' })
2045+
class MyComponent {
2046+
@Input() pair: [label: string, item: { [myKey]: number }];
2047+
}
2048+
"#;
2049+
let type_only = analyze_source(source);
2050+
assert!(
2051+
!type_only.contains("myKey"),
2052+
"myKey in named tuple member type literal should be preserved"
2053+
);
2054+
}
2055+
2056+
#[test]
2057+
fn test_tuple_with_mixed_element_kinds_no_panic() {
2058+
// Regression: a tuple with optional, rest, and named members together
2059+
// must not panic (TSOptionalType/TSRestType previously hit the unwrap in to_ts_type).
2060+
let source = r#"
2061+
import { Component, Input } from '@angular/core';
2062+
2063+
@Component({ selector: 'test' })
2064+
class MyComponent {
2065+
@Input() data: [label: string, value?: number, ...rest: string[]];
2066+
}
2067+
"#;
2068+
let type_only = analyze_source(source);
2069+
assert!(!type_only.contains("Component"), "Component should be preserved (decorator)");
2070+
}
2071+
19632072
#[test]
19642073
fn test_computed_key_in_parenthesized_type_preserved() {
19652074
// Review claim: TSParenthesizedType is not handled

0 commit comments

Comments
 (0)