Skip to content

Commit

Permalink
feat(ecmascript): add ToBoolean, ToNumber, ToString
Browse files Browse the repository at this point in the history
  • Loading branch information
Boshen committed Oct 13, 2024
1 parent c56343d commit 2c10e74
Show file tree
Hide file tree
Showing 15 changed files with 339 additions and 261 deletions.
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 0 additions & 17 deletions crates/oxc_ast/src/ast_impl/js.rs
Original file line number Diff line number Diff line change
Expand Up @@ -257,23 +257,6 @@ impl<'a> Expression<'a> {
matches!(self, Expression::BinaryExpression(_) | Expression::LogicalExpression(_))
}

/// Returns literal's value converted to the Boolean type
/// returns `true` when node is truthy, `false` when node is falsy, `None` when it cannot be determined.
/// <https://tc39.es/ecma262/#sec-toboolean>
/// 1. If argument is a Boolean, return argument.
/// 2. If argument is one of undefined, null, +0𝔽, -0𝔽, NaN, 0ℤ, or the empty String, return false.
pub fn to_boolean(&self) -> Option<bool> {
match self {
Self::BooleanLiteral(lit) => Some(lit.value),
Self::NullLiteral(_) => Some(false),
Self::NumericLiteral(lit) => Some(lit.value != 0.0),
Self::BigIntLiteral(lit) => Some(!lit.is_zero()),
Self::RegExpLiteral(_) => Some(true),
Self::StringLiteral(lit) => Some(!lit.value.is_empty()),
_ => None,
}
}

pub fn get_member_expr(&self) -> Option<&MemberExpression<'a>> {
match self.get_inner_expression() {
Expression::ChainExpression(chain_expr) => chain_expr.expression.as_member_expression(),
Expand Down
2 changes: 2 additions & 0 deletions crates/oxc_ecmascript/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,7 @@ test = true
doctest = false

[dependencies]
num-traits = { workspace = true }
oxc_ast = { workspace = true }
oxc_span = { workspace = true }
oxc_syntax = { workspace = true }
18 changes: 14 additions & 4 deletions crates/oxc_ecmascript/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,21 @@ mod prop_name;
mod string_char_at;
mod string_index_of;
mod string_last_index_of;
mod to_boolean;
mod to_int_32;
mod to_number;
mod to_string;

pub use self::{
bound_names::BoundNames, is_simple_parameter_list::IsSimpleParameterList,
private_bound_identifiers::PrivateBoundIdentifiers, prop_name::PropName,
string_char_at::StringCharAt, string_index_of::StringIndexOf,
string_last_index_of::StringLastIndexOf, to_int_32::ToInt32,
bound_names::BoundNames,
is_simple_parameter_list::IsSimpleParameterList,
private_bound_identifiers::PrivateBoundIdentifiers,
prop_name::PropName,
string_char_at::StringCharAt,
string_index_of::StringIndexOf,
string_last_index_of::StringLastIndexOf,
to_boolean::ToBoolean,
to_int_32::ToInt32,
to_number::{NumberValue, ToNumber},
to_string::ToJsString,
};
107 changes: 107 additions & 0 deletions crates/oxc_ecmascript/src/to_boolean.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
#[allow(clippy::wildcard_imports)]
use oxc_ast::ast::*;

use crate::{NumberValue, ToNumber};

/// `ToBoolean`
///
/// <https://tc39.es/ecma262/#sec-toboolean>
pub trait ToBoolean<'a> {
fn to_boolean(&self) -> Option<bool>;
}

impl<'a> ToBoolean<'a> for Expression<'a> {
fn to_boolean(&self) -> Option<bool> {
match self {
Expression::RegExpLiteral(_)
| Expression::ArrayExpression(_)
| Expression::ArrowFunctionExpression(_)
| Expression::ClassExpression(_)
| Expression::FunctionExpression(_)
| Expression::NewExpression(_)
| Expression::ObjectExpression(_) => Some(true),
Expression::NullLiteral(_) => Some(false),
Expression::BooleanLiteral(boolean_literal) => Some(boolean_literal.value),
Expression::NumericLiteral(number_literal) => Some(number_literal.value != 0.0),
Expression::BigIntLiteral(big_int_literal) => Some(!big_int_literal.is_zero()),
Expression::StringLiteral(string_literal) => Some(!string_literal.value.is_empty()),
Expression::TemplateLiteral(template_literal) => {
// only for ``
template_literal
.quasis
.first()
.filter(|quasi| quasi.tail)
.and_then(|quasi| quasi.value.cooked.as_ref())
.map(|cooked| !cooked.is_empty())
}
Expression::Identifier(ident) => match ident.name.as_str() {
"NaN" | "undefined" => Some(false),
"Infinity" => Some(true),
_ => None,
},
Expression::AssignmentExpression(assign_expr) => {
match assign_expr.operator {
AssignmentOperator::LogicalAnd | AssignmentOperator::LogicalOr => None,
// For ASSIGN, the value is the value of the RHS.
_ => assign_expr.right.to_boolean(),
}
}
Expression::LogicalExpression(logical_expr) => {
match logical_expr.operator {
// true && true -> true
// true && false -> false
// a && true -> None
LogicalOperator::And => {
let left = logical_expr.left.to_boolean();
let right = logical_expr.right.to_boolean();
match (left, right) {
(Some(true), Some(true)) => Some(true),
(Some(false), _) | (_, Some(false)) => Some(false),
(None, _) | (_, None) => None,
}
}
// true || false -> true
// false || false -> false
// a || b -> None
LogicalOperator::Or => {
let left = logical_expr.left.to_boolean();
let right = logical_expr.right.to_boolean();

match (left, right) {
(Some(true), _) | (_, Some(true)) => Some(true),
(Some(false), Some(false)) => Some(false),
(None, _) | (_, None) => None,
}
}
LogicalOperator::Coalesce => None,
}
}
Expression::SequenceExpression(sequence_expr) => {
// For sequence expression, the value is the value of the RHS.
sequence_expr.expressions.last().and_then(ToBoolean::to_boolean)
}
Expression::UnaryExpression(unary_expr) => {
if unary_expr.operator == UnaryOperator::Void {
Some(false)
} else if matches!(
unary_expr.operator,
UnaryOperator::BitwiseNot
| UnaryOperator::UnaryPlus
| UnaryOperator::UnaryNegation
) {
// ~0 -> true
// +1 -> true
// +0 -> false
// -0 -> false
self.to_number().map(|value| value != NumberValue::Number(0_f64))
} else if unary_expr.operator == UnaryOperator::LogicalNot {
// !true -> false
unary_expr.argument.to_boolean().map(|b| !b)
} else {
None
}
}
_ => None,
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
use num_traits::Zero;

#[derive(PartialEq)]
#[allow(clippy::wildcard_imports)]
use oxc_ast::ast::*;
use oxc_syntax::operator::UnaryOperator;

use crate::ToBoolean;

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum NumberValue {
Number(f64),
PositiveInfinity,
Expand Down Expand Up @@ -148,3 +154,62 @@ impl TryFrom<NumberValue> for f64 {
}
}
}

/// `ToNumber`
///
/// <https://tc39.es/ecma262/#sec-tonumber>
pub trait ToNumber<'a> {
fn to_number(&self) -> Option<NumberValue>;
}

impl<'a> ToNumber<'a> for Expression<'a> {
fn to_number(&self) -> Option<NumberValue> {
match self {
Expression::NumericLiteral(number_literal) => {
Some(NumberValue::Number(number_literal.value))
}
Expression::UnaryExpression(unary_expr) => match unary_expr.operator {
UnaryOperator::UnaryPlus => unary_expr.argument.to_number(),
UnaryOperator::UnaryNegation => unary_expr.argument.to_number().map(|v| -v),
UnaryOperator::BitwiseNot => {
unary_expr.argument.to_number().map(|value| {
match value {
NumberValue::Number(num) => NumberValue::Number(f64::from(
!NumericLiteral::ecmascript_to_int32(num),
)),
// ~Infinity -> -1
// ~-Infinity -> -1
// ~NaN -> -1
_ => NumberValue::Number(-1_f64),
}
})
}
UnaryOperator::LogicalNot => self
.to_boolean()
.map(|tri| if tri { 1_f64 } else { 0_f64 })
.map(NumberValue::Number),
UnaryOperator::Void => Some(NumberValue::NaN),
_ => None,
},
Expression::BooleanLiteral(bool_literal) => {
if bool_literal.value {
Some(NumberValue::Number(1.0))
} else {
Some(NumberValue::Number(0.0))
}
}
Expression::NullLiteral(_) => Some(NumberValue::Number(0.0)),
Expression::Identifier(ident) => match ident.name.as_str() {
"Infinity" => Some(NumberValue::PositiveInfinity),
"NaN" | "undefined" => Some(NumberValue::NaN),
_ => None,
},
// TODO: will be implemented in next PR, just for test pass now.
Expression::StringLiteral(string_literal) => string_literal
.value
.parse::<f64>()
.map_or(Some(NumberValue::NaN), |num| Some(NumberValue::Number(num))),
_ => None,
}
}
}
113 changes: 113 additions & 0 deletions crates/oxc_ecmascript/src/to_string.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
use std::borrow::Cow;

#[allow(clippy::wildcard_imports)]
use oxc_ast::ast::*;
use oxc_syntax::operator::UnaryOperator;

use crate::ToBoolean;

/// `ToString`
///
/// <https://tc39.es/ecma262/#sec-tostring>
pub trait ToJsString<'a> {
fn to_js_string(&self) -> Option<Cow<'a, str>>;
}

impl<'a> ToJsString<'a> for Expression<'a> {
fn to_js_string(&self) -> Option<Cow<'a, str>> {
match self {
Expression::StringLiteral(lit) => lit.to_js_string(),
Expression::TemplateLiteral(lit) => lit.to_js_string(),
Expression::Identifier(ident) => ident.to_js_string(),
Expression::NumericLiteral(lit) => lit.to_js_string(),
Expression::BigIntLiteral(lit) => lit.to_js_string(),
Expression::NullLiteral(lit) => lit.to_js_string(),
Expression::BooleanLiteral(lit) => lit.to_js_string(),
Expression::UnaryExpression(e) => e.to_js_string(),
Expression::ArrayExpression(e) => e.to_js_string(),
Expression::ObjectExpression(e) => e.to_js_string(),
_ => None,
}
}
}

impl<'a> ToJsString<'a> for StringLiteral<'a> {
fn to_js_string(&self) -> Option<Cow<'a, str>> {
Some(Cow::Borrowed(self.value.as_str()))
}
}

impl<'a> ToJsString<'a> for TemplateLiteral<'a> {
fn to_js_string(&self) -> Option<Cow<'a, str>> {
let mut str = String::new();
for (i, quasi) in self.quasis.iter().enumerate() {
str.push_str(quasi.value.cooked.as_ref()?);

if i < self.expressions.len() {
let expr = &self.expressions[i];
let value = expr.to_js_string()?;
str.push_str(&value);
}
}
Some(Cow::Owned(str))
}
}

impl<'a> ToJsString<'a> for IdentifierReference<'a> {
fn to_js_string(&self) -> Option<Cow<'a, str>> {
let name = self.name.as_str();
matches!(name, "undefined" | "Infinity" | "NaN").then(|| Cow::Borrowed(name))
}
}

impl<'a> ToJsString<'a> for NumericLiteral<'a> {
fn to_js_string(&self) -> Option<Cow<'a, str>> {
// FIXME: to js number string
Some(Cow::Owned(self.value.to_string()))
}
}

impl<'a> ToJsString<'a> for BigIntLiteral<'a> {
fn to_js_string(&self) -> Option<Cow<'a, str>> {
// FIXME: to js bigint string
Some(Cow::Owned(self.raw.to_string()))
}
}

impl<'a> ToJsString<'a> for BooleanLiteral {
fn to_js_string(&self) -> Option<Cow<'a, str>> {
Some(Cow::Borrowed(if self.value { "true" } else { "false" }))
}
}

impl<'a> ToJsString<'a> for NullLiteral {
fn to_js_string(&self) -> Option<Cow<'a, str>> {
Some(Cow::Borrowed("null"))
}
}

impl<'a> ToJsString<'a> for UnaryExpression<'a> {
fn to_js_string(&self) -> Option<Cow<'a, str>> {
match self.operator {
UnaryOperator::Void => Some(Cow::Borrowed("undefined")),
UnaryOperator::LogicalNot => self
.argument
.to_boolean()
.map(|boolean| Cow::Borrowed(if boolean { "false" } else { "true" })),
_ => None,
}
}
}

impl<'a> ToJsString<'a> for ArrayExpression<'a> {
fn to_js_string(&self) -> Option<Cow<'a, str>> {
// TODO: https://github.com/google/closure-compiler/blob/e13f5cd0a5d3d35f2db1e6c03fdf67ef02946009/src/com/google/javascript/jscomp/NodeUtil.java#L302-L303
None
}
}

impl<'a> ToJsString<'a> for ObjectExpression<'a> {
fn to_js_string(&self) -> Option<Cow<'a, str>> {
Some(Cow::Borrowed("[object Object]"))
}
}
1 change: 1 addition & 0 deletions crates/oxc_linter/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ oxc_ast = { workspace = true }
oxc_cfg = { workspace = true }
oxc_codegen = { workspace = true }
oxc_diagnostics = { workspace = true }
oxc_ecmascript = { workspace = true }
oxc_index = { workspace = true }
oxc_macros = { workspace = true }
oxc_parser = { workspace = true }
Expand Down
1 change: 1 addition & 0 deletions crates/oxc_linter/src/ast_util.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use oxc_ast::{ast::BindingIdentifier, AstKind};
use oxc_ecmascript::ToBoolean;
use oxc_semantic::{AstNode, IsGlobalReference, NodeId, SymbolId};
use oxc_span::{GetSpan, Span};
use oxc_syntax::operator::{AssignmentOperator, BinaryOperator, LogicalOperator, UnaryOperator};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use oxc_ast::ast::{BlockStatement, FunctionBody, Statement, SwitchCase};
use oxc_ecmascript::ToBoolean;

/// `StatementReturnStatus` describes whether the CFG corresponding to
/// the statement is termitated by return statement in all/some/nome of
Expand Down
Loading

0 comments on commit 2c10e74

Please sign in to comment.