Skip to main content

use_php_syntax/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7macro_rules! syntax_enum {
8    ($name:ident { $($variant:ident => $label:literal),+ $(,)? }) => {
9        #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
10        pub enum $name {
11            $($variant),+
12        }
13
14        impl $name {
15            pub const ALL: &'static [Self] = &[$(Self::$variant),+];
16
17            pub const fn as_str(self) -> &'static str {
18                match self {
19                    $(Self::$variant => $label),+
20                }
21            }
22        }
23
24        impl fmt::Display for $name {
25            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
26                formatter.write_str(self.as_str())
27            }
28        }
29
30        impl FromStr for $name {
31            type Err = PhpSyntaxError;
32
33            fn from_str(input: &str) -> Result<Self, Self::Err> {
34                match normalized_label(input)?.as_str() {
35                    $($label => Ok(Self::$variant),)+
36                    _ => Err(PhpSyntaxError::UnknownLabel),
37                }
38            }
39        }
40    };
41}
42
43syntax_enum!(PhpKeyword {
44    Abstract => "abstract",
45    And => "and",
46    Array => "array",
47    As => "as",
48    Break => "break",
49    Callable => "callable",
50    Case => "case",
51    Catch => "catch",
52    Class => "class",
53    Clone => "clone",
54    Const => "const",
55    Continue => "continue",
56    Declare => "declare",
57    Default => "default",
58    Do => "do",
59    Echo => "echo",
60    Else => "else",
61    Enum => "enum",
62    Extends => "extends",
63    Final => "final",
64    Finally => "finally",
65    Fn => "fn",
66    For => "for",
67    Foreach => "foreach",
68    Function => "function",
69    Global => "global",
70    If => "if",
71    Implements => "implements",
72    Interface => "interface",
73    Match => "match",
74    Namespace => "namespace",
75    New => "new",
76    Or => "or",
77    Private => "private",
78    Protected => "protected",
79    Public => "public",
80    Readonly => "readonly",
81    Return => "return",
82    Static => "static",
83    Switch => "switch",
84    Throw => "throw",
85    Trait => "trait",
86    Try => "try",
87    Use => "use",
88    While => "while",
89    Yield => "yield",
90});
91
92syntax_enum!(PhpVisibility {
93    Public => "public",
94    Protected => "protected",
95    Private => "private",
96});
97
98syntax_enum!(PhpDeclarationKind {
99    Class => "class",
100    Interface => "interface",
101    Trait => "trait",
102    Enum => "enum",
103    Function => "function",
104    Method => "method",
105    Property => "property",
106    Constant => "constant",
107});
108
109syntax_enum!(PhpModifier {
110    Static => "static",
111    Final => "final",
112    Abstract => "abstract",
113    Readonly => "readonly",
114});
115
116syntax_enum!(PhpControlFlowLabel {
117    If => "if",
118    Else => "else",
119    ElseIf => "elseif",
120    For => "for",
121    Foreach => "foreach",
122    While => "while",
123    DoWhile => "do-while",
124    Switch => "switch",
125    Match => "match",
126    Try => "try",
127    Catch => "catch",
128    Finally => "finally",
129    Break => "break",
130    Continue => "continue",
131    Return => "return",
132    Throw => "throw",
133    Yield => "yield",
134});
135
136/// Error returned when PHP syntax metadata cannot be parsed.
137#[derive(Clone, Copy, Debug, Eq, PartialEq)]
138pub enum PhpSyntaxError {
139    Empty,
140    UnknownLabel,
141}
142
143impl fmt::Display for PhpSyntaxError {
144    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
145        match self {
146            Self::Empty => formatter.write_str("PHP syntax metadata cannot be empty"),
147            Self::UnknownLabel => formatter.write_str("unknown PHP syntax metadata label"),
148        }
149    }
150}
151
152impl Error for PhpSyntaxError {}
153
154pub fn is_php_keyword(input: &str) -> bool {
155    input.parse::<PhpKeyword>().is_ok()
156}
157
158pub fn is_php_modifier(input: &str) -> bool {
159    input.parse::<PhpModifier>().is_ok()
160}
161
162fn normalized_label(input: &str) -> Result<String, PhpSyntaxError> {
163    let trimmed = input.trim();
164    if trimmed.is_empty() {
165        Err(PhpSyntaxError::Empty)
166    } else {
167        Ok(trimmed.to_ascii_lowercase().replace(['_', ' '], "-"))
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use super::{
174        PhpControlFlowLabel, PhpDeclarationKind, PhpKeyword, PhpModifier, PhpSyntaxError,
175        is_php_keyword, is_php_modifier,
176    };
177
178    #[test]
179    fn parses_common_syntax_labels() -> Result<(), PhpSyntaxError> {
180        assert!(is_php_keyword("readonly"));
181        assert!(is_php_modifier("final"));
182        assert_eq!("class".parse::<PhpKeyword>()?, PhpKeyword::Class);
183        assert_eq!(
184            "trait".parse::<PhpDeclarationKind>()?,
185            PhpDeclarationKind::Trait
186        );
187        assert_eq!(
188            "do while".parse::<PhpControlFlowLabel>()?,
189            PhpControlFlowLabel::DoWhile
190        );
191        Ok(())
192    }
193
194    #[test]
195    fn display_returns_source_labels() {
196        assert_eq!(PhpModifier::Readonly.to_string(), "readonly");
197    }
198}