mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
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:
2 files changed
+79
-1
No files matched your search
@@ -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]
|
||||
|
||||
Reference in new issue
Block a user