Skip to main content

use_php_token/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7/// Broad PHP token category metadata.
8#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
9pub enum PhpTokenCategory {
10    OpeningTag,
11    ClosingTag,
12    Keyword,
13    Identifier,
14    Variable,
15    Literal,
16    Operator,
17    Delimiter,
18    Comment,
19    Whitespace,
20    Unknown,
21}
22
23impl PhpTokenCategory {
24    pub const fn as_str(self) -> &'static str {
25        match self {
26            Self::OpeningTag => "opening-tag",
27            Self::ClosingTag => "closing-tag",
28            Self::Keyword => "keyword",
29            Self::Identifier => "identifier",
30            Self::Variable => "variable",
31            Self::Literal => "literal",
32            Self::Operator => "operator",
33            Self::Delimiter => "delimiter",
34            Self::Comment => "comment",
35            Self::Whitespace => "whitespace",
36            Self::Unknown => "unknown",
37        }
38    }
39}
40
41impl fmt::Display for PhpTokenCategory {
42    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
43        formatter.write_str(self.as_str())
44    }
45}
46
47impl FromStr for PhpTokenCategory {
48    type Err = PhpTokenError;
49
50    fn from_str(input: &str) -> Result<Self, Self::Err> {
51        match normalized_label(input)?.as_str() {
52            "openingtag" => Ok(Self::OpeningTag),
53            "closingtag" => Ok(Self::ClosingTag),
54            "keyword" => Ok(Self::Keyword),
55            "identifier" => Ok(Self::Identifier),
56            "variable" => Ok(Self::Variable),
57            "literal" => Ok(Self::Literal),
58            "operator" => Ok(Self::Operator),
59            "delimiter" => Ok(Self::Delimiter),
60            "comment" => Ok(Self::Comment),
61            "whitespace" => Ok(Self::Whitespace),
62            "unknown" => Ok(Self::Unknown),
63            _ => Err(PhpTokenError::UnknownLabel),
64        }
65    }
66}
67
68/// PHP delimiter label metadata.
69#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
70pub enum PhpDelimiter {
71    OpenParen,
72    CloseParen,
73    OpenBrace,
74    CloseBrace,
75    OpenBracket,
76    CloseBracket,
77    Semicolon,
78    Comma,
79    Colon,
80    DoubleColon,
81    Arrow,
82    NamespaceSeparator,
83}
84
85impl PhpDelimiter {
86    pub const fn as_str(self) -> &'static str {
87        match self {
88            Self::OpenParen => "(",
89            Self::CloseParen => ")",
90            Self::OpenBrace => "{",
91            Self::CloseBrace => "}",
92            Self::OpenBracket => "[",
93            Self::CloseBracket => "]",
94            Self::Semicolon => ";",
95            Self::Comma => ",",
96            Self::Colon => ":",
97            Self::DoubleColon => "::",
98            Self::Arrow => "->",
99            Self::NamespaceSeparator => "\\",
100        }
101    }
102}
103
104impl fmt::Display for PhpDelimiter {
105    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
106        formatter.write_str(self.as_str())
107    }
108}
109
110/// PHP operator label metadata.
111#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
112pub enum PhpOperator {
113    Assign,
114    Plus,
115    Minus,
116    Multiply,
117    Divide,
118    Modulo,
119    Equal,
120    Identical,
121    NotEqual,
122    NotIdentical,
123    Spaceship,
124    NullCoalesce,
125    Elvis,
126}
127
128impl PhpOperator {
129    pub const fn as_str(self) -> &'static str {
130        match self {
131            Self::Assign => "=",
132            Self::Plus => "+",
133            Self::Minus => "-",
134            Self::Multiply => "*",
135            Self::Divide => "/",
136            Self::Modulo => "%",
137            Self::Equal => "==",
138            Self::Identical => "===",
139            Self::NotEqual => "!=",
140            Self::NotIdentical => "!==",
141            Self::Spaceship => "<=>",
142            Self::NullCoalesce => "??",
143            Self::Elvis => "?:",
144        }
145    }
146}
147
148impl fmt::Display for PhpOperator {
149    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
150        formatter.write_str(self.as_str())
151    }
152}
153
154/// PHP literal category metadata.
155#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
156pub enum PhpLiteralKind {
157    String,
158    Integer,
159    Float,
160    Boolean,
161    Null,
162    Array,
163    Heredoc,
164    Nowdoc,
165}
166
167impl PhpLiteralKind {
168    pub const fn as_str(self) -> &'static str {
169        match self {
170            Self::String => "string",
171            Self::Integer => "integer",
172            Self::Float => "float",
173            Self::Boolean => "boolean",
174            Self::Null => "null",
175            Self::Array => "array",
176            Self::Heredoc => "heredoc",
177            Self::Nowdoc => "nowdoc",
178        }
179    }
180}
181
182/// PHP comment category metadata.
183#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
184pub enum PhpCommentKind {
185    Line,
186    Block,
187    Docblock,
188    HashLine,
189}
190
191impl PhpCommentKind {
192    pub const fn as_str(self) -> &'static str {
193        match self {
194            Self::Line => "line",
195            Self::Block => "block",
196            Self::Docblock => "docblock",
197            Self::HashLine => "hash-line",
198        }
199    }
200}
201
202/// Non-empty token text metadata.
203#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
204pub struct PhpTokenText(String);
205
206impl PhpTokenText {
207    pub fn new(input: &str) -> Result<Self, PhpTokenError> {
208        let trimmed = input.trim();
209        if trimmed.is_empty() {
210            Err(PhpTokenError::Empty)
211        } else {
212            Ok(Self(trimmed.to_string()))
213        }
214    }
215
216    pub fn as_str(&self) -> &str {
217        &self.0
218    }
219}
220
221impl fmt::Display for PhpTokenText {
222    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
223        formatter.write_str(self.as_str())
224    }
225}
226
227impl FromStr for PhpTokenText {
228    type Err = PhpTokenError;
229
230    fn from_str(input: &str) -> Result<Self, Self::Err> {
231        Self::new(input)
232    }
233}
234
235/// Byte span metadata for a token in source text.
236#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
237pub struct PhpTokenSpan {
238    start: usize,
239    end: usize,
240}
241
242impl PhpTokenSpan {
243    pub const fn new(start: usize, end: usize) -> Result<Self, PhpTokenError> {
244        if end < start {
245            Err(PhpTokenError::InvalidSpan)
246        } else {
247            Ok(Self { start, end })
248        }
249    }
250
251    pub const fn start(self) -> usize {
252        self.start
253    }
254
255    pub const fn end(self) -> usize {
256        self.end
257    }
258
259    pub const fn len(self) -> usize {
260        self.end - self.start
261    }
262
263    pub const fn is_empty(self) -> bool {
264        self.start == self.end
265    }
266}
267
268/// Lightweight token metadata.
269#[derive(Clone, Debug, Eq, PartialEq)]
270pub struct PhpToken {
271    category: PhpTokenCategory,
272    text: PhpTokenText,
273    span: PhpTokenSpan,
274}
275
276impl PhpToken {
277    pub const fn new(category: PhpTokenCategory, text: PhpTokenText, span: PhpTokenSpan) -> Self {
278        Self {
279            category,
280            text,
281            span,
282        }
283    }
284
285    pub const fn category(&self) -> PhpTokenCategory {
286        self.category
287    }
288
289    pub const fn text(&self) -> &PhpTokenText {
290        &self.text
291    }
292
293    pub const fn span(&self) -> PhpTokenSpan {
294        self.span
295    }
296}
297
298/// Error returned when PHP token metadata is invalid.
299#[derive(Clone, Copy, Debug, Eq, PartialEq)]
300pub enum PhpTokenError {
301    Empty,
302    InvalidSpan,
303    UnknownLabel,
304}
305
306impl fmt::Display for PhpTokenError {
307    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
308        match self {
309            Self::Empty => formatter.write_str("PHP token metadata cannot be empty"),
310            Self::InvalidSpan => formatter.write_str("PHP token span end cannot precede start"),
311            Self::UnknownLabel => formatter.write_str("unknown PHP token metadata label"),
312        }
313    }
314}
315
316impl Error for PhpTokenError {}
317
318fn normalized_label(input: &str) -> Result<String, PhpTokenError> {
319    let trimmed = input.trim();
320    if trimmed.is_empty() {
321        Err(PhpTokenError::Empty)
322    } else {
323        Ok(trimmed.to_ascii_lowercase().replace(['-', '_', ' '], ""))
324    }
325}
326
327#[cfg(test)]
328mod tests {
329    use super::{
330        PhpDelimiter, PhpOperator, PhpToken, PhpTokenCategory, PhpTokenError, PhpTokenSpan,
331        PhpTokenText,
332    };
333
334    #[test]
335    fn builds_token_metadata() -> Result<(), PhpTokenError> {
336        let token = PhpToken::new(
337            PhpTokenCategory::Variable,
338            PhpTokenText::new(" $value ")?,
339            PhpTokenSpan::new(4, 10)?,
340        );
341
342        assert_eq!(token.category(), PhpTokenCategory::Variable);
343        assert_eq!(token.text().as_str(), "$value");
344        assert_eq!(token.span().len(), 6);
345        Ok(())
346    }
347
348    #[test]
349    fn exposes_operator_and_delimiter_labels() {
350        assert_eq!(PhpDelimiter::DoubleColon.to_string(), "::");
351        assert_eq!(PhpOperator::Spaceship.to_string(), "<=>");
352    }
353}