use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct JsModule {
pub statements: Vec<Stmt>,
pub metadata: Option<GenerationMetadata>,
}
impl Default for JsModule {
fn default() -> Self {
Self::new()
}
}
impl JsModule {
#[must_use]
pub const fn new() -> Self {
Self {
statements: Vec::new(),
metadata: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GenerationMetadata {
pub tool: String,
pub version: String,
pub input_hash: String,
pub timestamp: String,
pub regenerate_cmd: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum Stmt {
Let {
name: Identifier,
value: Expr,
},
Const {
name: Identifier,
value: Expr,
},
Assign {
name: Identifier,
value: Expr,
},
MemberAssign {
object: Expr,
member: Identifier,
value: Expr,
},
AddAssign {
target: Expr,
value: Expr,
},
PostIncrement(Expr),
Expr(Expr),
Return(Option<Expr>),
If {
condition: Expr,
then_branch: Vec<Stmt>,
else_branch: Option<Vec<Stmt>>,
},
For {
var: Identifier,
start: Expr,
end: Expr,
body: Vec<Stmt>,
},
While {
condition: Expr,
body: Vec<Stmt>,
},
TryCatch {
body: Vec<Stmt>,
catch_var: Identifier,
handler: Vec<Stmt>,
},
Block(Vec<Stmt>),
Comment(String),
Class(JsClass),
Switch(JsSwitch),
OnMessage(Vec<Stmt>),
RegisterProcessor {
name: String,
class: Identifier,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum Expr {
Null,
Bool(bool),
Num(f64),
Str(String),
Ident(Identifier),
This,
Member {
object: Box<Expr>,
property: Identifier,
},
Index {
object: Box<Expr>,
index: Box<Expr>,
},
Call {
callee: Box<Expr>,
args: Vec<Expr>,
},
New {
constructor: Box<Expr>,
args: Vec<Expr>,
},
Await(Box<Expr>),
Import(Box<Expr>),
Binary {
left: Box<Expr>,
op: BinOp,
right: Box<Expr>,
},
Unary {
op: UnaryOp,
operand: Box<Expr>,
},
Ternary {
condition: Box<Expr>,
then_expr: Box<Expr>,
else_expr: Box<Expr>,
},
Object(Vec<(String, Expr)>),
Array(Vec<Expr>),
Arrow {
params: Vec<Identifier>,
body: Box<Expr>,
},
ArrowBlock {
params: Vec<Identifier>,
body: Vec<Stmt>,
},
Assign {
target: Box<Expr>,
value: Box<Expr>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum BinOp {
Add,
Sub,
Mul,
Div,
Mod,
Eq,
EqStrict,
Ne,
NeStrict,
Lt,
Le,
Gt,
Ge,
And,
Or,
BitAnd,
BitOr,
}
impl BinOp {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Add => "+",
Self::Sub => "-",
Self::Mul => "*",
Self::Div => "/",
Self::Mod => "%",
Self::Eq => "==",
Self::EqStrict => "===",
Self::Ne => "!=",
Self::NeStrict => "!==",
Self::Lt => "<",
Self::Le => "<=",
Self::Gt => ">",
Self::Ge => ">=",
Self::And => "&&",
Self::Or => "||",
Self::BitAnd => "&",
Self::BitOr => "|",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum UnaryOp {
Not,
Neg,
TypeOf,
}
impl UnaryOp {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Not => "!",
Self::Neg => "-",
Self::TypeOf => "typeof ",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Identifier(String);
impl Identifier {
pub const RESERVED_WORDS: &'static [&'static str] = &[
"break",
"case",
"catch",
"continue",
"debugger",
"default",
"delete",
"do",
"else",
"finally",
"for",
"function",
"if",
"in",
"instanceof",
"new",
"return",
"switch",
"this",
"throw",
"try",
"typeof",
"var",
"void",
"while",
"with",
"class",
"const",
"enum",
"export",
"extends",
"import",
"super",
"implements",
"interface",
"let",
"package",
"private",
"protected",
"public",
"static",
"yield",
"await",
"null",
"true",
"false",
];
pub fn new(name: impl Into<String>) -> crate::Result<Self> {
let name = name.into();
if name.is_empty() {
return Err(crate::JsGenError::InvalidIdentifier {
name,
reason: "identifier cannot be empty".to_string(),
});
}
let first = name.chars().next().unwrap_or(' ');
if first.is_ascii_digit() {
return Err(crate::JsGenError::InvalidIdentifier {
name,
reason: "identifier cannot start with a digit".to_string(),
});
}
for c in name.chars() {
if !c.is_ascii_alphanumeric() && c != '_' && c != '$' {
return Err(crate::JsGenError::InvalidIdentifier {
name,
reason: format!("invalid character '{c}'"),
});
}
}
if Self::RESERVED_WORDS.contains(&name.as_str()) {
return Err(crate::JsGenError::InvalidIdentifier {
name,
reason: "reserved word".to_string(),
});
}
Ok(Self(name))
}
#[must_use]
pub fn new_unchecked(name: &'static str) -> Self {
Self(name.to_string())
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for Identifier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct JsClass {
pub name: Identifier,
pub extends: Option<Identifier>,
pub constructor: Option<Vec<Stmt>>,
pub methods: Vec<JsMethod>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct JsMethod {
pub name: Identifier,
pub params: Vec<Identifier>,
pub body: Vec<Stmt>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct JsSwitch {
pub expr: Expr,
pub cases: Vec<(Expr, Vec<Stmt>)>,
pub default: Option<Vec<Stmt>>,
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn identifier_valid() {
assert!(Identifier::new("foo").is_ok());
assert!(Identifier::new("_bar").is_ok());
assert!(Identifier::new("$baz").is_ok());
assert!(Identifier::new("foo123").is_ok());
assert!(Identifier::new("camelCase").is_ok());
}
#[test]
fn identifier_invalid_reserved() {
let err = Identifier::new("class").unwrap_err();
assert!(err.to_string().contains("reserved word"));
}
#[test]
fn identifier_invalid_starts_digit() {
let err = Identifier::new("123foo").unwrap_err();
assert!(err.to_string().contains("cannot start with a digit"));
}
#[test]
fn identifier_invalid_empty() {
let err = Identifier::new("").unwrap_err();
assert!(err.to_string().contains("cannot be empty"));
}
#[test]
fn identifier_invalid_chars() {
let err = Identifier::new("foo-bar").unwrap_err();
assert!(err.to_string().contains("invalid character"));
}
#[test]
fn binop_as_str() {
assert_eq!(BinOp::Add.as_str(), "+");
assert_eq!(BinOp::EqStrict.as_str(), "===");
assert_eq!(BinOp::And.as_str(), "&&");
}
#[test]
fn unaryop_as_str() {
assert_eq!(UnaryOp::Not.as_str(), "!");
assert_eq!(UnaryOp::TypeOf.as_str(), "typeof ");
}
}