fix(codegen): preserve parentheses around intrinsic type reference (#23156)

At the top of a type alias body, a bare `intrinsic` is parsed as the `intrinsic` keyword, whereas `(intrinsic)` is a reference to a type named `intrinsic`. TypeScript treats these differently:

- `type T = intrinsic` → `intrinsic` keyword (TS2795)
- `type T = (intrinsic)` → type reference (TS2304)

With `preserve_parens: false`, the parser drops the `TSParenthesizedType` wrapper, leaving a bare `intrinsic` identifier reference. Codegen then printed `type t = intrinsic;`, which is reparsed as the keyword — a semantic change.

Fix: in `TSTypeAliasDeclaration` codegen, wrap the type annotation in parentheses when it is a bare `intrinsic` identifier reference (no type arguments, not qualified).

Closes #22216
This commit is contained in:
Boshen committed 2026-06-09 15:57:12 +00:00
1 parent d74964cda6
commit f5721c29f7
2 files changed
+79 -1

No files matched your search

+42 -1
View File
@@ -4016,6 +4016,42 @@ impl Gen for TSModuleBlock<'_> {
}
}
/// At the top of a type alias body, a bare `intrinsic` reference is parsed as the
/// `intrinsic` keyword (e.g. `type T = intrinsic`), whereas `(intrinsic)` is a reference to a
/// type named `intrinsic` (per TypeScript, the two differ). With `preserve_parens: false` the
/// parser drops the `TSParenthesizedType` wrapper, so codegen must re-add parentheses around the
/// whole alias body whenever its leftmost-printed type is a bare `intrinsic` reference. Otherwise
/// the output re-parses as the keyword, or fails to parse before `|`, `&`, `extends`, or `[]`.
///
/// The leftmost type is followed through the wrapper nodes that print a child type first, so e.g.
/// `(intrinsic)[]`, `(intrinsic)["x"]`, `(intrinsic) | T`, `(intrinsic) & T`, and
/// `(intrinsic) extends T ? ...` are all handled.
///
/// See <https://github.com/oxc-project/oxc/issues/22216>.
fn is_leftmost_intrinsic_reference(ty: &TSType) -> bool {
match ty {
TSType::TSTypeReference(reference) => {
reference.type_arguments.is_none()
&& matches!(
&reference.type_name,
TSTypeName::IdentifierReference(ident) if ident.name == "intrinsic"
)
}
TSType::TSArrayType(array) => is_leftmost_intrinsic_reference(&array.element_type),
TSType::TSIndexedAccessType(access) => is_leftmost_intrinsic_reference(&access.object_type),
TSType::TSUnionType(union) => {
union.types.first().is_some_and(is_leftmost_intrinsic_reference)
}
TSType::TSIntersectionType(intersection) => {
intersection.types.first().is_some_and(is_leftmost_intrinsic_reference)
}
TSType::TSConditionalType(conditional) => {
is_leftmost_intrinsic_reference(&conditional.check_type)
}
_ => false,
}
}
impl Gen for TSTypeAliasDeclaration<'_> {
fn r#gen(&self, p: &mut Codegen, ctx: Context) {
if self.declare {
@@ -4030,7 +4066,12 @@ impl Gen for TSTypeAliasDeclaration<'_> {
p.print_soft_space();
p.print_ascii_byte(b'=');
p.print_soft_space();
self.type_annotation.print(p, ctx);
// A leftmost bare `intrinsic` reference in the alias body must keep parentheses,
// otherwise it re-parses as the `intrinsic` keyword. See `is_leftmost_intrinsic_reference`.
let needs_parens = is_leftmost_intrinsic_reference(&self.type_annotation);
p.wrap(needs_parens, |p| {
self.type_annotation.print(p, ctx);
});
}
}
@@ -440,6 +440,43 @@ fn type_codegen_with_preserve_parens_off() {
"type T = ({ [K in keyof Obj]: Obj[K] } & {\n\ta: 1;\n}) & {\n\tb: 2;\n};\n",
parse_options,
);
// `type t = (intrinsic)` is a reference to a type named `intrinsic`, but `type t = intrinsic`
// is the `intrinsic` keyword, so the parentheses must be kept to preserve the meaning.
test_with_parse_options("type t = (intrinsic);", "type t = (intrinsic);\n", parse_options);
test_with_parse_options(
"type t = (intrinsic.foo);",
"type t = intrinsic.foo;\n",
parse_options,
);
// A leftmost bare `intrinsic` reference inside a compound type must also keep parentheses,
// otherwise the body re-parses as the `intrinsic` keyword and then fails before `[]`/`|`/`&`/`extends`.
test_with_parse_options("type t = (intrinsic)[];", "type t = (intrinsic[]);\n", parse_options);
test_with_parse_options(
"type t = (intrinsic)[\"x\"];",
"type t = (intrinsic[\"x\"]);\n",
parse_options,
);
test_with_parse_options(
"type t = (intrinsic) | string;",
"type t = (intrinsic | string);\n",
parse_options,
);
test_with_parse_options(
"type t = (intrinsic) & string;",
"type t = (intrinsic & string);\n",
parse_options,
);
test_with_parse_options(
"type t = (intrinsic) extends string ? 1 : 2;",
"type t = (intrinsic extends string ? 1 : 2);\n",
parse_options,
);
// `intrinsic` is not the leftmost type here, so no parentheses are needed.
test_with_parse_options(
"type t = string | (intrinsic);",
"type t = string | intrinsic;\n",
parse_options,
);
}
#[test]