Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion crates/oxc_codegen/src/binary_expr_visitor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,10 +184,15 @@ impl<'a> BinaryExpressionVisitor<'a> {
}
}
BinaryishOperator::Binary(BinaryOperator::Exponential) => {
if matches!(e.left(), Expression::UnaryExpression(_)) {
// Negative numbers are printed using a unary operator
if matches!(
e.left(),
Expression::UnaryExpression(_) | Expression::NumericLiteral(_)
) {
self.left_precedence = Precedence::Call;
}
}

_ => {}
}

Expand Down
8 changes: 8 additions & 0 deletions crates/oxc_codegen/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ bitflags! {
/// [In]
const FORBID_IN = 1 << 0;
const FORBID_CALL = 1 << 1;
const TYPESCRIPT = 1 << 2;
}
}

Expand All @@ -20,6 +21,13 @@ impl Context {
self.contains(Self::FORBID_CALL)
}

#[inline]
#[must_use]
pub fn with_typescript(mut self) -> Self {
self |= Self::TYPESCRIPT;
self
}

#[inline]
#[must_use]
pub fn and_forbid_in(self, include: bool) -> Self {
Expand Down
131 changes: 52 additions & 79 deletions crates/oxc_codegen/src/gen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1005,7 +1005,7 @@ impl<'a> GenExpr for Expression<'a> {
match self {
Self::BooleanLiteral(lit) => lit.print(p, ctx),
Self::NullLiteral(lit) => lit.print(p, ctx),
Self::NumericLiteral(lit) => lit.print(p, ctx),
Self::NumericLiteral(lit) => lit.print_expr(p, precedence, ctx),
Self::BigIntLiteral(lit) => lit.print(p, ctx),
Self::RegExpLiteral(lit) => lit.print(p, ctx),
Self::StringLiteral(lit) => lit.print(p, ctx),
Expand Down Expand Up @@ -1102,86 +1102,44 @@ impl Gen for NullLiteral {
}
}

// Need a space before "." if it could be parsed as a decimal point.
fn need_space_before_dot(s: &str, p: &mut Codegen) {
if !s.bytes().any(|b| matches!(b, b'.' | b'e' | b'x')) {
p.need_space_before_dot = p.code_len();
}
}

impl<'a> Gen for NumericLiteral<'a> {
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
fn gen(&self, p: &mut Codegen, _ctx: Context) {
impl<'a> GenExpr for NumericLiteral<'a> {
fn gen_expr(&self, p: &mut Codegen, precedence: Precedence, ctx: Context) {
p.add_source_mapping(self.span.start);
if !p.options.minify && !self.raw.is_empty() {
let value = self.value;
if ctx.contains(Context::TYPESCRIPT) {
p.print_str(self.raw);
need_space_before_dot(self.raw, p);
} else if self.value != f64::INFINITY {
} else if value.is_nan() {
p.print_space_before_identifier();
p.print_str("NaN");
} else if value.is_infinite() {
let wrap = (p.options.minify && precedence >= Precedence::Multiply)
|| (value.is_sign_negative() && precedence >= Precedence::Prefix);
p.wrap(wrap, |p| {
if value.is_sign_negative() {
p.print_space_before_operator(Operator::Unary(UnaryOperator::UnaryNegation));
p.print_ascii_byte(b'-');
} else {
p.print_space_before_identifier();
}
if p.options.minify {
p.print_str("1/0");
} else {
p.print_str("1 / 0");
}
});
} else if value.is_sign_positive() {
p.print_space_before_identifier();
let abs_value = self.value.abs();
if self.value.is_sign_negative() {
p.print_space_before_operator(Operator::Unary(UnaryOperator::UnaryNegation));
p.print_str("-");
}
let result = get_minified_number(abs_value);
let bytes = result.as_str();
p.print_str(bytes);
need_space_before_dot(bytes, p);
} else if self.value == f64::INFINITY && self.raw.is_empty() {
p.print_str("Infinity");
need_space_before_dot("Infinity", p);
p.print_non_negative_float(value);
} else if precedence >= Precedence::Prefix {
p.print_str("(-");
p.print_non_negative_float(value.abs());
p.print_ascii_byte(b')');
} else {
p.print_str(self.raw);
need_space_before_dot(self.raw, p);
};
}
}

// https://github.com/terser/terser/blob/c5315c3fd6321d6b2e076af35a70ef532f498505/lib/output.js#L2418
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss, clippy::cast_possible_wrap)]
fn get_minified_number(num: f64) -> String {
use oxc_syntax::number::ToJsString;
if num < 1000.0 && num.fract() == 0.0 {
return num.to_js_string();
}

let mut s = num.to_js_string();

if s.starts_with("0.") {
s = s[1..].to_string();
}

s = s.cow_replacen("e+", "e", 1).to_string();

let mut candidates = vec![s.clone()];

if num.fract() == 0.0 {
candidates.push(format!("0x{:x}", num as u128));
}

if s.starts_with(".0") {
// create `1e-2`
if let Some((i, _)) = s[1..].bytes().enumerate().find(|(_, c)| *c != b'0') {
let len = i + 1; // `+1` to include the dot.
let digits = &s[len..];
candidates.push(format!("{digits}e-{}", digits.len() + len - 1));
p.print_space_before_operator(Operator::Unary(UnaryOperator::UnaryNegation));
p.print_ascii_byte(b'-');
p.print_non_negative_float(value.abs());
}
} else if s.ends_with('0') {
// create 1e2
if let Some((len, _)) = s.bytes().rev().enumerate().find(|(_, c)| *c != b'0') {
candidates.push(format!("{}e{len}", &s[0..s.len() - len]));
}
} else if let Some((integer, point, exponent)) =
s.split_once('.').and_then(|(a, b)| b.split_once('e').map(|e| (a, e.0, e.1)))
{
// `1.2e101` -> ("1", "2", "101")
candidates.push(format!(
"{integer}{point}e{}",
exponent.parse::<isize>().unwrap() - point.len() as isize
));
}

candidates.into_iter().min_by_key(String::len).unwrap()
}

impl<'a> Gen for BigIntLiteral<'a> {
Expand Down Expand Up @@ -1568,13 +1526,25 @@ impl<'a> Gen for ObjectProperty<'a> {
}
}

if self.computed {
let mut computed = self.computed;

// "{ -1: 0 }" must be printed as "{ [-1]: 0 }"
// "{ 1/0: 0 }" must be printed as "{ [1/0]: 0 }"
if !computed {
if let Some(Expression::NumericLiteral(n)) = self.key.as_expression() {
if n.value.is_sign_negative() || n.value.is_infinite() {
computed = true;
}
}
}

if computed {
p.print_ascii_byte(b'[');
}
if !shorthand {
self.key.print(p, ctx);
}
if self.computed {
if computed {
p.print_ascii_byte(b']');
}
if !shorthand {
Expand Down Expand Up @@ -2843,6 +2813,7 @@ impl<'a> Gen for TSTypeAnnotation<'a> {

impl<'a> Gen for TSType<'a> {
fn gen(&self, p: &mut Codegen, ctx: Context) {
let ctx = ctx.with_typescript();
match self {
Self::TSFunctionType(ty) => ty.print(p, ctx),
Self::TSConstructorType(ty) => ty.print(p, ctx),
Expand Down Expand Up @@ -3162,7 +3133,7 @@ impl<'a> Gen for TSLiteral<'a> {
match self {
Self::BooleanLiteral(decl) => decl.print(p, ctx),
Self::NullLiteral(decl) => decl.print(p, ctx),
Self::NumericLiteral(decl) => decl.print(p, ctx),
Self::NumericLiteral(decl) => decl.print_expr(p, Precedence::Lowest, ctx),
Self::BigIntLiteral(decl) => decl.print(p, ctx),
Self::RegExpLiteral(decl) => decl.print(p, ctx),
Self::StringLiteral(decl) => decl.print(p, ctx),
Expand Down Expand Up @@ -3618,7 +3589,9 @@ impl<'a> Gen for TSEnumMember<'a> {
TSEnumMemberName::StaticIdentifier(decl) => decl.print(p, ctx),
TSEnumMemberName::StaticStringLiteral(decl) => decl.print(p, ctx),
TSEnumMemberName::StaticTemplateLiteral(decl) => decl.print(p, ctx),
TSEnumMemberName::StaticNumericLiteral(decl) => decl.print(p, ctx),
TSEnumMemberName::StaticNumericLiteral(decl) => {
decl.print_expr(p, Precedence::Lowest, ctx);
}
decl @ match_expression!(TSEnumMemberName) => {
p.print_str("[");
decl.to_expression().print_expr(p, Precedence::Lowest, ctx);
Expand Down
63 changes: 63 additions & 0 deletions crates/oxc_codegen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,69 @@ impl<'a> Codegen<'a> {
}
}

fn print_non_negative_float(&mut self, num: f64) {
use oxc_syntax::number::ToJsString;
if num < 1000.0 && num.fract() == 0.0 {
self.print_str(&num.to_js_string());
self.need_space_before_dot = self.code_len();
} else {
let s = Self::get_minified_number(num);
self.print_str(&s);
if !s.bytes().any(|b| matches!(b, b'.' | b'e' | b'x')) {
self.need_space_before_dot = self.code_len();
}
}
}

// `get_minified_number` from terser
// https://github.com/terser/terser/blob/c5315c3fd6321d6b2e076af35a70ef532f498505/lib/output.js#L2418
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss, clippy::cast_possible_wrap)]
fn get_minified_number(num: f64) -> String {
use cow_utils::CowUtils;
use oxc_syntax::number::ToJsString;
if num < 1000.0 && num.fract() == 0.0 {
return num.to_js_string();
}

let mut s = num.to_js_string();

if s.starts_with("0.") {
s = s[1..].to_string();
}

s = s.cow_replacen("e+", "e", 1).to_string();

let mut candidates = vec![s.clone()];

if num.fract() == 0.0 {
candidates.push(format!("0x{:x}", num as u128));
}

if s.starts_with(".0") {
// create `1e-2`
if let Some((i, _)) = s[1..].bytes().enumerate().find(|(_, c)| *c != b'0') {
let len = i + 1; // `+1` to include the dot.
let digits = &s[len..];
candidates.push(format!("{digits}e-{}", digits.len() + len - 1));
}
} else if s.ends_with('0') {
// create 1e2
if let Some((len, _)) = s.bytes().rev().enumerate().find(|(_, c)| *c != b'0') {
candidates.push(format!("{}e{len}", &s[0..s.len() - len]));
}
} else if let Some((integer, point, exponent)) =
s.split_once('.').and_then(|(a, b)| b.split_once('e').map(|e| (a, e.0, e.1)))
{
// `1.2e101` -> ("1", "2", "101")
candidates.push(format!(
"{integer}{point}e{}",
exponent.parse::<isize>().unwrap() - point.len() as isize
));
}

candidates.into_iter().min_by_key(String::len).unwrap()
}

#[inline]
fn wrap<F: FnMut(&mut Self)>(&mut self, wrap: bool, mut f: F) {
if wrap {
Expand Down
Loading