Skip to content

Commit 2be0457

Browse files
Brooooooklynclaude
andcommitted
fix(dts): follow alias chains of any length and keep U+2028 / U+2029 line breaks whole
An import-equals alias chain is followed until it ends, stopping only on a cycle, instead of giving up after 16 aliases (`A17.T` through 18 aliases is `T`, like ngtsc). Looking back for a `//` before a union now steps over a U+2028 / U+2029 line break by its full UTF-8 length, where it used to slice inside the character and panic. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent f744678 commit 2be0457

2 files changed

Lines changed: 55 additions & 4 deletions

File tree

‎crates/oxc_angular_compiler/src/directive/dts_type.rs‎

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -228,15 +228,22 @@ impl<'a> TypePrinter<'_, 'a> {
228228
'a: 'p,
229229
{
230230
let mut aliased = false;
231-
// Aliases can name aliases; a cycle is a TypeScript error.
232-
for _ in 0..16 {
231+
// Aliases can name aliases, through any number of them. Meeting one
232+
// again is a cycle (`import A = B; import B = A;`, `import A = A.B;`),
233+
// which TypeScript reports.
234+
let mut seen: std::vec::Vec<std::vec::Vec<&'p str>> = std::vec::Vec::new();
235+
loop {
233236
let (len, target) = match self.scope.alias(parts[0]) {
234237
Some(target) => (1, target),
235238
None => match self.scope.namespace_alias(&parts) {
236239
Some(found) => found,
237240
None => return Some(Resolved::Name(parts, aliased)),
238241
},
239242
};
243+
if seen.iter().any(|alias| alias[..] == parts[..len]) {
244+
return None;
245+
}
246+
seen.push(parts[..len].to_vec());
240247
aliased = true;
241248
match target {
242249
// `import Core = require('@angular/core')`: `Core.X` is
@@ -252,7 +259,6 @@ impl<'a> TypePrinter<'_, 'a> {
252259
}
253260
}
254261
}
255-
None
256262
}
257263

258264
/// [`Self::type_name`] for a name whose head isn't an alias: `parts`, head
@@ -703,7 +709,11 @@ fn token_end_before(source: &str, start: usize) -> Option<usize> {
703709
let before = &source[..end];
704710
let trimmed = before.trim_end();
705711
if before[trimmed.len()..].contains(is_line_break) {
706-
let line_start = trimmed.rfind(is_line_break).map_or(0, |i| i + 1);
712+
// After the line break, which can be more than one byte (U+2028).
713+
let line_start = trimmed
714+
.char_indices()
715+
.rfind(|&(_, c)| is_line_break(c))
716+
.map_or(0, |(i, c)| i + c.len_utf8());
707717
if trimmed[line_start..].contains("//") {
708718
return None;
709719
}

‎crates/oxc_angular_compiler/tests/dts_input_transform_type_test.rs‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,18 @@ const COMMENTS_NOT_KEPT: &[(&str, &str, &str)] = &[
8383
("(x: string /** y */) => void", "(x: string /** y */) => void", "(x: string) => void"),
8484
("Array<string /** y */>", "Array<string /** y */>", "Array<string>"),
8585
("string |\n /** j */ number", "string | \n /** j */ number", "string | number"),
86+
// TypeScript reads past a U+2028 / U+2029 for the comments after `<` (it
87+
// only stops at `\n` and `\r`), and prints this one twice.
88+
(
89+
"Array<\u{2028}/* c */\nstring | number>",
90+
"Array</* c */ /* c */ string | number>",
91+
"Array<string | number>",
92+
),
93+
(
94+
"Array<\u{2029}/* c */\nstring | number>",
95+
"Array</* c */ /* c */ string | number>",
96+
"Array<string | number>",
97+
),
8698
];
8799

88100
#[test]
@@ -302,6 +314,31 @@ export class Dir {{
302314
);
303315
}
304316

317+
/// A chain of aliases is followed to its end however long it is: ngtsc 22.1.7
318+
/// writes `T` for `A17.T` through 18 aliases, and `T | number` through 40. A
319+
/// cycle (a TypeScript error, on which ngtsc throws) gives `unknown`.
320+
#[test]
321+
fn import_equals_alias_chains_are_followed_to_the_end() {
322+
let chain =
323+
|n: usize| -> String { (1..n).map(|i| format!("import A{i} = A{};\n", i - 1)).collect() };
324+
let types = ["A17.T", "A39.T | number", "X.T", "S.T"];
325+
let source = format!(
326+
"import {{Directive, Input}} from '@angular/core';
327+
namespace NS {{ export type T = string; }}
328+
import A0 = NS;
329+
{}import X = Y;
330+
import Y = X;
331+
import S = S.B;
332+
@Directive({{selector: '[d]'}})
333+
export class Dir {{
334+
{}}}
335+
",
336+
chain(40),
337+
members_typed(&types)
338+
);
339+
assert_eq!(accept_members(&source), expect_members(&["T", "T | number", "unknown", "unknown"]));
340+
}
341+
305342
/// An alias of another module (`import R = require('./other')`, or of an
306343
/// import) follows the rule for other modules: `unknown`. ngtsc 22.1.7 writes
307344
/// the bare name there (`Other`, `T`), which doesn't resolve in its `.d.ts`.
@@ -739,6 +776,10 @@ const MATCH: &[(&str, &str)] = &[
739776
("'\\x00'", "\"\\0\""),
740777
("'\\0' | '\\u00001'", "\"\\0\" | \"\\x001\""),
741778
("'\u{2028}\u{2029}\\u0085'", "\"\\u2028\\u2029\\u0085\""),
779+
// A U+2028 / U+2029 (a line break, but three bytes long) on the line
780+
// before a union.
781+
("{ a: 1;\u{2028} b:\n string | number }", "{ a: 1; b: string | number; }"),
782+
("{ a: 1;\u{2029} b:\n string | number }", "{ a: 1; b: string | number; }"),
742783
("'\\v\\f\\b'", "\"\\v\\f\\b\""),
743784
("'\\x7f'", "\"\u{7f}\""),
744785
("'a\"b\\'c`d'", "\"a\\\"b'c`d\""),

0 commit comments

Comments
 (0)