Skip to main content

ecma_syntax_cat/
identifier.rs

1//! Identifier newtypes.
2//!
3//! [`Identifier`] is the general-purpose ECMAScript identifier.  Reserved-
4//! word handling is a lexer concern, not an AST concern; this layer accepts
5//! any string that starts with `$`, `_`, or an ASCII letter and continues
6//! with `$`, `_`, ASCII letters, or ASCII digits.  Full Unicode identifier
7//! support (per ECMA-262) is a TODO for the lexer/v0.2.
8//!
9//! [`PrivateIdentifier`] is the `#name` form used in class bodies.
10
11use crate::error::Error;
12
13/// An ECMAScript identifier.
14///
15/// Construction validates the lexical shape but does not check for reserved
16/// words; a lexer or parser should filter those before constructing.
17#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
18pub struct Identifier(String);
19
20impl Identifier {
21    /// Build an identifier from a name string.
22    ///
23    /// # Errors
24    ///
25    /// Returns [`Error::InvalidIdentifier`] if `name` is empty or contains
26    /// any character outside the ASCII identifier alphabet.
27    pub fn new(name: impl Into<String>) -> Result<Self, Error> {
28        let name = name.into();
29        valid_identifier(&name)
30            .then_some(Self(name.clone()))
31            .ok_or(Error::InvalidIdentifier {
32                attempted: name,
33                reason: "must match [$_A-Za-z][$_A-Za-z0-9]*",
34            })
35    }
36
37    /// View the identifier name as a string slice.
38    #[must_use]
39    pub fn as_str(&self) -> &str {
40        &self.0
41    }
42}
43
44impl std::fmt::Display for Identifier {
45    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46        f.write_str(&self.0)
47    }
48}
49
50/// A private class field identifier (`#name`).
51#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
52pub struct PrivateIdentifier(String);
53
54impl PrivateIdentifier {
55    /// Build a private identifier from a name string (without the leading `#`).
56    ///
57    /// # Errors
58    ///
59    /// Returns [`Error::InvalidIdentifier`] if `name` is empty or contains
60    /// invalid characters.
61    pub fn new(name: impl Into<String>) -> Result<Self, Error> {
62        let name = name.into();
63        valid_identifier(&name)
64            .then_some(Self(name.clone()))
65            .ok_or(Error::InvalidIdentifier {
66                attempted: name,
67                reason: "private identifier must match [$_A-Za-z][$_A-Za-z0-9]* after the leading `#`",
68            })
69    }
70
71    /// View the name (without the leading `#`) as a string slice.
72    #[must_use]
73    pub fn as_str(&self) -> &str {
74        &self.0
75    }
76}
77
78impl std::fmt::Display for PrivateIdentifier {
79    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80        write!(f, "#{}", self.0)
81    }
82}
83
84fn valid_identifier(name: &str) -> bool {
85    let bytes = name.as_bytes();
86    if bytes.first().is_some_and(|&b| is_ident_start(b)) {
87        bytes.iter().skip(1).all(|&b| is_ident_continue(b))
88    } else {
89        false
90    }
91}
92
93fn is_ident_start(b: u8) -> bool {
94    b.is_ascii_alphabetic() || b == b'_' || b == b'$'
95}
96
97fn is_ident_continue(b: u8) -> bool {
98    b.is_ascii_alphanumeric() || b == b'_' || b == b'$'
99}