1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7#[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#[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#[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#[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#[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#[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}