ecma-syntax-cat 0.1.0

ECMAScript abstract syntax tree as comp-cat-rs-idiomatic Rust types. ESTree-shaped, ES2024-complete, no panics, no Rc, no interior mutability. Foundation crate for boa-cat and related downstream tooling.
Documentation
//! Identifier newtypes.
//!
//! [`Identifier`] is the general-purpose ECMAScript identifier.  Reserved-
//! word handling is a lexer concern, not an AST concern; this layer accepts
//! any string that starts with `$`, `_`, or an ASCII letter and continues
//! with `$`, `_`, ASCII letters, or ASCII digits.  Full Unicode identifier
//! support (per ECMA-262) is a TODO for the lexer/v0.2.
//!
//! [`PrivateIdentifier`] is the `#name` form used in class bodies.

use crate::error::Error;

/// An ECMAScript identifier.
///
/// Construction validates the lexical shape but does not check for reserved
/// words; a lexer or parser should filter those before constructing.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Identifier(String);

impl Identifier {
    /// Build an identifier from a name string.
    ///
    /// # Errors
    ///
    /// Returns [`Error::InvalidIdentifier`] if `name` is empty or contains
    /// any character outside the ASCII identifier alphabet.
    pub fn new(name: impl Into<String>) -> Result<Self, Error> {
        let name = name.into();
        valid_identifier(&name)
            .then_some(Self(name.clone()))
            .ok_or(Error::InvalidIdentifier {
                attempted: name,
                reason: "must match [$_A-Za-z][$_A-Za-z0-9]*",
            })
    }

    /// View the identifier name as a string slice.
    #[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 {
        f.write_str(&self.0)
    }
}

/// A private class field identifier (`#name`).
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct PrivateIdentifier(String);

impl PrivateIdentifier {
    /// Build a private identifier from a name string (without the leading `#`).
    ///
    /// # Errors
    ///
    /// Returns [`Error::InvalidIdentifier`] if `name` is empty or contains
    /// invalid characters.
    pub fn new(name: impl Into<String>) -> Result<Self, Error> {
        let name = name.into();
        valid_identifier(&name)
            .then_some(Self(name.clone()))
            .ok_or(Error::InvalidIdentifier {
                attempted: name,
                reason: "private identifier must match [$_A-Za-z][$_A-Za-z0-9]* after the leading `#`",
            })
    }

    /// View the name (without the leading `#`) as a string slice.
    #[must_use]
    pub fn as_str(&self) -> &str {
        &self.0
    }
}

impl std::fmt::Display for PrivateIdentifier {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "#{}", self.0)
    }
}

fn valid_identifier(name: &str) -> bool {
    let bytes = name.as_bytes();
    if bytes.first().is_some_and(|&b| is_ident_start(b)) {
        bytes.iter().skip(1).all(|&b| is_ident_continue(b))
    } else {
        false
    }
}

fn is_ident_start(b: u8) -> bool {
    b.is_ascii_alphabetic() || b == b'_' || b == b'$'
}

fn is_ident_continue(b: u8) -> bool {
    b.is_ascii_alphanumeric() || b == b'_' || b == b'$'
}