Skip to content

Commit 413d7be

Browse files
committed
feat(transformer-dts): transform enum support (#3710)
1 parent 9493fbe commit 413d7be

File tree

7 files changed

+311
-2
lines changed

7 files changed

+311
-2
lines changed

crates/oxc_ast/src/ast_builder.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1899,6 +1899,16 @@ impl<'a> AstBuilder<'a> {
18991899
)
19001900
}
19011901

1902+
#[inline]
1903+
pub fn ts_enum_member(
1904+
self,
1905+
span: Span,
1906+
id: TSEnumMemberName<'a>,
1907+
initializer: Option<Expression<'a>>,
1908+
) -> TSEnumMember<'a> {
1909+
TSEnumMember { span, id, initializer }
1910+
}
1911+
19021912
#[inline]
19031913
pub fn decorator(self, span: Span, expression: Expression<'a>) -> Decorator<'a> {
19041914
Decorator { span, expression }

crates/oxc_ast/src/span.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -732,3 +732,14 @@ impl<'a> GetSpan for JSXMemberExpressionObject<'a> {
732732
}
733733
}
734734
}
735+
736+
impl<'a> GetSpan for TSEnumMemberName<'a> {
737+
fn span(&self) -> Span {
738+
match self {
739+
TSEnumMemberName::StaticIdentifier(ident) => ident.span,
740+
TSEnumMemberName::StaticStringLiteral(literal) => literal.span,
741+
TSEnumMemberName::StaticNumericLiteral(literal) => literal.span,
742+
expr @ match_expression!(TSEnumMemberName) => expr.to_expression().span(),
743+
}
744+
}
745+
}

crates/oxc_transformer_dts/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ oxc_ast = { workspace = true }
2525
oxc_span = { workspace = true }
2626
oxc_allocator = { workspace = true }
2727
oxc_diagnostics = { workspace = true }
28-
oxc_syntax = { workspace = true }
28+
oxc_syntax = { workspace = true, features = ["to_js_string"] }
2929

3030
rustc-hash = { workspace = true }
3131

crates/oxc_transformer_dts/src/declaration.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ impl<'a> TransformerDts<'a> {
213213
}
214214
Declaration::TSEnumDeclaration(enum_decl) => {
215215
if !check_binding || self.scope.has_reference(&enum_decl.id.name) {
216-
Some(self.ctx.ast.copy(decl))
216+
self.transform_ts_enum_declaration(enum_decl)
217217
} else {
218218
None
219219
}

crates/oxc_transformer_dts/src/diagnostics.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,8 @@ pub fn signature_computed_property_name(span: Span) -> OxcDiagnostic {
3131
OxcDiagnostic::error("Computed properties must be number or string literals, variables or dotted expressions with --isolatedDeclarations.")
3232
.with_label(span)
3333
}
34+
35+
pub fn enum_member_initializers(span: Span) -> OxcDiagnostic {
36+
OxcDiagnostic::error("Enum member initializers must be computable without references to external symbols with --isolatedDeclarations.")
37+
.with_label(span)
38+
}
Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
#[allow(clippy::wildcard_imports)]
2+
use oxc_ast::ast::*;
3+
4+
use oxc_span::{Atom, GetSpan, SPAN};
5+
use oxc_syntax::{
6+
number::{NumberBase, ToJsInt32, ToJsString},
7+
operator::{BinaryOperator, UnaryOperator},
8+
};
9+
use rustc_hash::FxHashMap;
10+
11+
use crate::{diagnostics::enum_member_initializers, TransformerDts};
12+
13+
#[derive(Debug, Clone)]
14+
enum ConstantValue {
15+
Number(f64),
16+
String(String),
17+
}
18+
19+
impl<'a> TransformerDts<'a> {
20+
pub fn transform_ts_enum_declaration(
21+
&mut self,
22+
decl: &TSEnumDeclaration<'a>,
23+
) -> Option<Declaration<'a>> {
24+
let mut members = self.ctx.ast.new_vec();
25+
let mut prev_initializer_value = Some(ConstantValue::Number(0.0));
26+
let mut prev_members = FxHashMap::default();
27+
for member in &decl.members {
28+
let value = if let Some(initializer) = &member.initializer {
29+
let computed_value =
30+
self.computed_constant_value(initializer, &decl.id.name, &prev_members);
31+
32+
if computed_value.is_none() {
33+
self.ctx.error(enum_member_initializers(member.id.span()));
34+
}
35+
36+
computed_value
37+
} else if let Some(ConstantValue::Number(v)) = prev_initializer_value {
38+
Some(ConstantValue::Number(v + 1.0))
39+
} else {
40+
None
41+
};
42+
43+
prev_initializer_value.clone_from(&value);
44+
45+
if let Some(value) = &value {
46+
let member_name = match &member.id {
47+
TSEnumMemberName::StaticIdentifier(id) => &id.name,
48+
TSEnumMemberName::StaticStringLiteral(str) => &str.value,
49+
#[allow(clippy::unnested_or_patterns)] // Clippy is wrong
50+
TSEnumMemberName::StaticNumericLiteral(_)
51+
| match_expression!(TSEnumMemberName) => {
52+
unreachable!()
53+
}
54+
};
55+
prev_members.insert(member_name.clone(), value.clone());
56+
}
57+
58+
let member = self.ctx.ast.ts_enum_member(
59+
member.span,
60+
self.ctx.ast.copy(&member.id),
61+
value.map(|v| match v {
62+
ConstantValue::Number(v) => {
63+
let is_negative = v < 0.0;
64+
65+
// Infinity
66+
let expr = if v.is_infinite() {
67+
let ident =
68+
IdentifierReference::new(SPAN, self.ctx.ast.new_atom("Infinity"));
69+
self.ctx.ast.identifier_reference_expression(ident)
70+
} else {
71+
let value = if is_negative { -v } else { v };
72+
self.ctx.ast.literal_number_expression(NumericLiteral {
73+
span: SPAN,
74+
value,
75+
raw: self.ctx.ast.new_str(&value.to_string()),
76+
base: NumberBase::Decimal,
77+
})
78+
};
79+
80+
if is_negative {
81+
self.ctx.ast.unary_expression(SPAN, UnaryOperator::UnaryNegation, expr)
82+
} else {
83+
expr
84+
}
85+
}
86+
ConstantValue::String(v) => self
87+
.ctx
88+
.ast
89+
.literal_string_expression(self.ctx.ast.string_literal(SPAN, &v)),
90+
}),
91+
);
92+
93+
members.push(member);
94+
}
95+
Some(self.ctx.ast.ts_enum_declaration(
96+
decl.span,
97+
self.ctx.ast.copy(&decl.id),
98+
members,
99+
self.modifiers_declare(),
100+
))
101+
}
102+
103+
/// Evaluate the expression to a constant value.
104+
/// Refer to [babel](https://github.com/babel/babel/blob/610897a9a96c5e344e77ca9665df7613d2f88358/packages/babel-plugin-transform-typescript/src/enum.ts#L241C1-L394C2)
105+
fn computed_constant_value(
106+
&self,
107+
expr: &Expression<'a>,
108+
enum_name: &Atom<'a>,
109+
prev_members: &FxHashMap<Atom<'a>, ConstantValue>,
110+
) -> Option<ConstantValue> {
111+
self.evaluate(expr, enum_name, prev_members)
112+
}
113+
114+
#[allow(clippy::unused_self)]
115+
fn evaluate_ref(
116+
&self,
117+
expr: &Expression<'a>,
118+
enum_name: &Atom<'a>,
119+
prev_members: &FxHashMap<Atom<'a>, ConstantValue>,
120+
) -> Option<ConstantValue> {
121+
match expr {
122+
match_member_expression!(Expression) => {
123+
let expr = expr.to_member_expression();
124+
let Expression::Identifier(ident) = expr.object() else { return None };
125+
if ident.name == enum_name {
126+
let property = expr.static_property_name()?;
127+
prev_members.get(property).cloned()
128+
} else {
129+
None
130+
}
131+
}
132+
Expression::Identifier(ident) => {
133+
if ident.name == "Infinity" {
134+
return Some(ConstantValue::Number(f64::INFINITY));
135+
} else if ident.name == "NaN" {
136+
return Some(ConstantValue::Number(f64::NAN));
137+
}
138+
139+
if let Some(value) = prev_members.get(&ident.name) {
140+
return Some(value.clone());
141+
}
142+
143+
None
144+
}
145+
_ => None,
146+
}
147+
}
148+
149+
fn evaluate(
150+
&self,
151+
expr: &Expression<'a>,
152+
enum_name: &Atom<'a>,
153+
prev_members: &FxHashMap<Atom<'a>, ConstantValue>,
154+
) -> Option<ConstantValue> {
155+
match expr {
156+
Expression::Identifier(_)
157+
| Expression::ComputedMemberExpression(_)
158+
| Expression::StaticMemberExpression(_)
159+
| Expression::PrivateFieldExpression(_) => {
160+
self.evaluate_ref(expr, enum_name, prev_members)
161+
}
162+
Expression::BinaryExpression(expr) => {
163+
self.eval_binary_expression(expr, enum_name, prev_members)
164+
}
165+
Expression::UnaryExpression(expr) => {
166+
self.eval_unary_expression(expr, enum_name, prev_members)
167+
}
168+
Expression::NumericLiteral(lit) => Some(ConstantValue::Number(lit.value)),
169+
Expression::StringLiteral(lit) => Some(ConstantValue::String(lit.value.to_string())),
170+
Expression::TemplateLiteral(lit) => {
171+
let mut value = String::new();
172+
for part in &lit.quasis {
173+
value.push_str(&part.value.raw);
174+
}
175+
Some(ConstantValue::String(value))
176+
}
177+
Expression::ParenthesizedExpression(expr) => {
178+
self.evaluate(&expr.expression, enum_name, prev_members)
179+
}
180+
_ => None,
181+
}
182+
}
183+
184+
#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss, clippy::cast_sign_loss)]
185+
fn eval_binary_expression(
186+
&self,
187+
expr: &BinaryExpression<'a>,
188+
enum_name: &Atom<'a>,
189+
prev_members: &FxHashMap<Atom<'a>, ConstantValue>,
190+
) -> Option<ConstantValue> {
191+
let left = self.evaluate(&expr.left, enum_name, prev_members)?;
192+
let right = self.evaluate(&expr.right, enum_name, prev_members)?;
193+
194+
if matches!(expr.operator, BinaryOperator::Addition)
195+
&& (matches!(left, ConstantValue::String(_))
196+
|| matches!(right, ConstantValue::String(_)))
197+
{
198+
let left_string = match left {
199+
ConstantValue::String(str) => str,
200+
ConstantValue::Number(v) => v.to_js_string(),
201+
};
202+
203+
let right_string = match right {
204+
ConstantValue::String(str) => str,
205+
ConstantValue::Number(v) => v.to_js_string(),
206+
};
207+
208+
return Some(ConstantValue::String(format!("{left_string}{right_string}")));
209+
}
210+
211+
let left = match left {
212+
ConstantValue::Number(v) => v,
213+
ConstantValue::String(_) => return None,
214+
};
215+
216+
let right = match right {
217+
ConstantValue::Number(v) => v,
218+
ConstantValue::String(_) => return None,
219+
};
220+
221+
match expr.operator {
222+
BinaryOperator::ShiftRight => Some(ConstantValue::Number(f64::from(
223+
left.to_js_int_32().wrapping_shr(right.to_js_int_32() as u32),
224+
))),
225+
BinaryOperator::ShiftRightZeroFill => Some(ConstantValue::Number(f64::from(
226+
(left.to_js_int_32() as u32).wrapping_shr(right.to_js_int_32() as u32),
227+
))),
228+
BinaryOperator::ShiftLeft => Some(ConstantValue::Number(f64::from(
229+
left.to_js_int_32().wrapping_shl(right.to_js_int_32() as u32),
230+
))),
231+
BinaryOperator::BitwiseXOR => {
232+
Some(ConstantValue::Number(f64::from(left.to_js_int_32() ^ right.to_js_int_32())))
233+
}
234+
BinaryOperator::BitwiseOR => {
235+
Some(ConstantValue::Number(f64::from(left.to_js_int_32() | right.to_js_int_32())))
236+
}
237+
BinaryOperator::BitwiseAnd => {
238+
Some(ConstantValue::Number(f64::from(left.to_js_int_32() & right.to_js_int_32())))
239+
}
240+
BinaryOperator::Multiplication => Some(ConstantValue::Number(left * right)),
241+
BinaryOperator::Division => Some(ConstantValue::Number(left / right)),
242+
BinaryOperator::Addition => Some(ConstantValue::Number(left + right)),
243+
BinaryOperator::Subtraction => Some(ConstantValue::Number(left - right)),
244+
BinaryOperator::Remainder => Some(ConstantValue::Number(left % right)),
245+
BinaryOperator::Exponential => Some(ConstantValue::Number(left.powf(right))),
246+
_ => None,
247+
}
248+
}
249+
250+
#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
251+
fn eval_unary_expression(
252+
&self,
253+
expr: &UnaryExpression<'a>,
254+
enum_name: &Atom<'a>,
255+
prev_members: &FxHashMap<Atom<'a>, ConstantValue>,
256+
) -> Option<ConstantValue> {
257+
let value = self.evaluate(&expr.argument, enum_name, prev_members)?;
258+
259+
let value = match value {
260+
ConstantValue::Number(value) => value,
261+
ConstantValue::String(_) => {
262+
let value = if expr.operator == UnaryOperator::UnaryNegation {
263+
ConstantValue::Number(f64::NAN)
264+
} else if expr.operator == UnaryOperator::BitwiseNot {
265+
ConstantValue::Number(-1.0)
266+
} else {
267+
value
268+
};
269+
return Some(value);
270+
}
271+
};
272+
273+
match expr.operator {
274+
UnaryOperator::UnaryPlus => Some(ConstantValue::Number(value)),
275+
UnaryOperator::UnaryNegation => Some(ConstantValue::Number(-value)),
276+
UnaryOperator::BitwiseNot => {
277+
Some(ConstantValue::Number(f64::from(!value.to_js_int_32())))
278+
}
279+
_ => None,
280+
}
281+
}
282+
}

crates/oxc_transformer_dts/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ mod class;
99
mod context;
1010
mod declaration;
1111
mod diagnostics;
12+
mod r#enum;
1213
mod function;
1314
mod inferrer;
1415
mod module;

0 commit comments

Comments
 (0)