Skip to main content

use_php_symbol/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7/// PHP symbol kind metadata.
8#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
9pub enum SymbolKind {
10    Class,
11    Interface,
12    Trait,
13    Enum,
14    Function,
15    Constant,
16    Method,
17    Property,
18    Parameter,
19}
20
21impl SymbolKind {
22    pub const fn as_str(self) -> &'static str {
23        match self {
24            Self::Class => "class",
25            Self::Interface => "interface",
26            Self::Trait => "trait",
27            Self::Enum => "enum",
28            Self::Function => "function",
29            Self::Constant => "constant",
30            Self::Method => "method",
31            Self::Property => "property",
32            Self::Parameter => "parameter",
33        }
34    }
35}
36
37impl fmt::Display for SymbolKind {
38    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
39        formatter.write_str(self.as_str())
40    }
41}
42
43impl FromStr for SymbolKind {
44    type Err = PhpSymbolError;
45
46    fn from_str(input: &str) -> Result<Self, Self::Err> {
47        match normalized_label(input)?.as_str() {
48            "class" => Ok(Self::Class),
49            "interface" => Ok(Self::Interface),
50            "trait" => Ok(Self::Trait),
51            "enum" => Ok(Self::Enum),
52            "function" => Ok(Self::Function),
53            "constant" | "const" => Ok(Self::Constant),
54            "method" => Ok(Self::Method),
55            "property" => Ok(Self::Property),
56            "parameter" | "param" => Ok(Self::Parameter),
57            _ => Err(PhpSymbolError::UnknownLabel),
58        }
59    }
60}
61
62/// PHP class-like symbol kind metadata.
63#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
64pub enum PhpClassLikeKind {
65    Class,
66    Interface,
67    Trait,
68    Enum,
69}
70
71impl PhpClassLikeKind {
72    pub const fn as_str(self) -> &'static str {
73        match self {
74            Self::Class => "class",
75            Self::Interface => "interface",
76            Self::Trait => "trait",
77            Self::Enum => "enum",
78        }
79    }
80}
81
82/// PHP member kind metadata.
83#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
84pub enum PhpMemberKind {
85    Method,
86    Property,
87    Constant,
88    Case,
89}
90
91impl PhpMemberKind {
92    pub const fn as_str(self) -> &'static str {
93        match self {
94            Self::Method => "method",
95            Self::Property => "property",
96            Self::Constant => "constant",
97            Self::Case => "case",
98        }
99    }
100}
101
102/// Lightly validated PHP symbol name metadata.
103#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
104pub struct SymbolName(String);
105
106impl SymbolName {
107    pub fn new(input: &str) -> Result<Self, PhpSymbolError> {
108        let trimmed = input.trim();
109        if trimmed.is_empty() {
110            return Err(PhpSymbolError::Empty);
111        }
112        if !is_valid_php_symbol_name(trimmed) {
113            return Err(PhpSymbolError::InvalidName);
114        }
115        Ok(Self(trimmed.to_string()))
116    }
117
118    pub fn as_str(&self) -> &str {
119        &self.0
120    }
121
122    pub fn bare_name(&self) -> &str {
123        self.0.strip_prefix('$').unwrap_or(self.as_str())
124    }
125}
126
127impl fmt::Display for SymbolName {
128    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
129        formatter.write_str(self.as_str())
130    }
131}
132
133impl FromStr for SymbolName {
134    type Err = PhpSymbolError;
135
136    fn from_str(input: &str) -> Result<Self, Self::Err> {
137        Self::new(input)
138    }
139}
140
141/// PHP symbol metadata.
142#[derive(Clone, Debug, Eq, PartialEq)]
143pub struct PhpSymbol {
144    kind: SymbolKind,
145    name: SymbolName,
146}
147
148impl PhpSymbol {
149    pub const fn new(kind: SymbolKind, name: SymbolName) -> Self {
150        Self { kind, name }
151    }
152
153    pub const fn kind(&self) -> SymbolKind {
154        self.kind
155    }
156
157    pub const fn name(&self) -> &SymbolName {
158        &self.name
159    }
160}
161
162/// Error returned when PHP symbol metadata is invalid.
163#[derive(Clone, Copy, Debug, Eq, PartialEq)]
164pub enum PhpSymbolError {
165    Empty,
166    InvalidName,
167    UnknownLabel,
168}
169
170impl fmt::Display for PhpSymbolError {
171    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
172        match self {
173            Self::Empty => formatter.write_str("PHP symbol name cannot be empty"),
174            Self::InvalidName => formatter.write_str("PHP symbol name has an invalid shape"),
175            Self::UnknownLabel => formatter.write_str("unknown PHP symbol metadata label"),
176        }
177    }
178}
179
180impl Error for PhpSymbolError {}
181
182pub fn is_valid_php_symbol_name(input: &str) -> bool {
183    let trimmed = input.trim();
184    let bare = trimmed.strip_prefix('$').unwrap_or(trimmed);
185    let mut characters = bare.chars();
186    let Some(first) = characters.next() else {
187        return false;
188    };
189    (first == '_' || first.is_ascii_alphabetic())
190        && characters.all(|character| character == '_' || character.is_ascii_alphanumeric())
191}
192
193fn normalized_label(input: &str) -> Result<String, PhpSymbolError> {
194    let trimmed = input.trim();
195    if trimmed.is_empty() {
196        Err(PhpSymbolError::Empty)
197    } else {
198        Ok(trimmed.to_ascii_lowercase().replace(['-', '_', ' '], ""))
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use super::{
205        PhpClassLikeKind, PhpSymbol, PhpSymbolError, SymbolKind, SymbolName,
206        is_valid_php_symbol_name,
207    };
208
209    #[test]
210    fn validates_symbol_names() -> Result<(), PhpSymbolError> {
211        let name = SymbolName::new(" $value ")?;
212        let symbol = PhpSymbol::new(SymbolKind::Parameter, name);
213
214        assert_eq!(symbol.name().as_str(), "$value");
215        assert_eq!(symbol.name().bare_name(), "value");
216        assert!(is_valid_php_symbol_name("ExampleController"));
217        assert!(!is_valid_php_symbol_name("123bad"));
218        Ok(())
219    }
220
221    #[test]
222    fn exposes_class_like_labels() {
223        assert_eq!(PhpClassLikeKind::Interface.as_str(), "interface");
224        assert_eq!(SymbolKind::Method.to_string(), "method");
225    }
226}