Skip to main content

mago_codex/
symbol.rs

1use mago_word::WordSet;
2use mago_word::word;
3
4use mago_word::Word;
5use mago_word::WordMap;
6
7/// A pair of `Word`s representing a symbol and its member.
8///
9/// This is used to uniquely identify a symbol and its member within the codebase,
10/// where the first `Word` is the symbol's fully qualified class name (FQCN)
11/// and the second `Word` is the member's name (e.g., method, property, constant),
12/// or an empty string if the symbol itself is being referenced (e.g., a class or function
13/// without a specific member).
14pub type SymbolIdentifier = (Word, Word);
15
16/// Represents the different kinds of top-level class-like structures in PHP.
17#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
18#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
19pub enum SymbolKind {
20    Class,
21    Enum,
22    Trait,
23    Interface,
24}
25
26impl SymbolKind {
27    /// Checks if this symbol kind is `Class`.
28    #[inline]
29    #[must_use]
30    pub const fn is_class(&self) -> bool {
31        matches!(self, SymbolKind::Class)
32    }
33
34    /// Checks if this symbol kind is `Enum`.
35    #[inline]
36    #[must_use]
37    pub const fn is_enum(&self) -> bool {
38        matches!(self, SymbolKind::Enum)
39    }
40
41    /// Checks if this symbol kind is `Trait`.
42    #[inline]
43    #[must_use]
44    pub const fn is_trait(&self) -> bool {
45        matches!(self, SymbolKind::Trait)
46    }
47
48    /// Checks if this symbol kind is `Interface`.
49    #[inline]
50    #[must_use]
51    pub const fn is_interface(&self) -> bool {
52        matches!(self, SymbolKind::Interface)
53    }
54
55    /// Returns the string representation of the symbol kind.
56    #[inline]
57    #[must_use]
58    pub const fn as_str(&self) -> &'static str {
59        match self {
60            SymbolKind::Class => "class",
61            SymbolKind::Enum => "enum",
62            SymbolKind::Trait => "trait",
63            SymbolKind::Interface => "interface",
64        }
65    }
66}
67
68/// Stores a map of all known class-like symbol names (FQCNs) to their corresponding `SymbolKind`.
69/// Provides basic methods for adding symbols and querying.
70#[derive(Clone, Debug, PartialEq, Eq)]
71#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
72pub struct Symbols {
73    all: WordMap<SymbolKind>,
74    namespaces: WordSet,
75}
76
77impl Symbols {
78    /// Creates a new, empty `Symbols` map.
79    #[inline]
80    #[must_use]
81    pub fn new() -> Symbols {
82        Symbols { all: WordMap::default(), namespaces: WordSet::default() }
83    }
84
85    /// Adds or updates a symbol name identified as a `Class`.
86    #[inline]
87    pub fn add_class_name(&mut self, name: Word) {
88        self.namespaces.extend(get_symbol_namespaces(name));
89        self.all.insert(name, SymbolKind::Class);
90    }
91
92    /// Adds or updates a symbol name identified as an `Interface`.
93    #[inline]
94    pub fn add_interface_name(&mut self, name: Word) {
95        self.namespaces.extend(get_symbol_namespaces(name));
96        self.all.insert(name, SymbolKind::Interface);
97    }
98
99    /// Adds or updates a symbol name identified as a `Trait`.
100    #[inline]
101    pub fn add_trait_name(&mut self, name: Word) {
102        self.namespaces.extend(get_symbol_namespaces(name));
103        self.all.insert(name, SymbolKind::Trait);
104    }
105
106    /// Adds or updates a symbol name identified as an `Enum`.
107    #[inline]
108    pub fn add_enum_name(&mut self, name: Word) {
109        self.namespaces.extend(get_symbol_namespaces(name));
110        self.all.insert(name, SymbolKind::Enum);
111    }
112
113    /// Retrieves the `SymbolKind` for a given symbol name, if known.
114    ///
115    /// # Arguments
116    ///
117    /// * `name`: The `Word` (likely FQCN) of the symbol to look up.
118    ///
119    /// # Returns
120    ///
121    /// `Some(SymbolKind)` if the symbol exists in the map, `None` otherwise.
122    #[inline]
123    #[must_use]
124    pub fn get_kind(&self, name: Word) -> Option<SymbolKind> {
125        self.all.get(&name).copied() // Use copied() since SymbolKind is Copy
126    }
127
128    /// Checks if a symbol with the given name is known.
129    ///
130    /// # Arguments
131    ///
132    /// * `name`: The `Word` (likely FQCN) of the symbol to check.
133    ///
134    /// # Returns
135    ///
136    /// `true` if the symbol exists in the map, `false` otherwise.
137    #[inline]
138    #[must_use]
139    pub fn contains(&self, name: Word) -> bool {
140        self.all.contains_key(&name)
141    }
142
143    /// Check if any symbol within the table is part of the given namespace.
144    ///
145    /// # Arguments
146    ///
147    /// * `namespace`: The `Word` of the namespace to check for.
148    ///
149    /// # Returns
150    ///
151    /// `true` if the namespace is present, `false` otherwise.
152    #[must_use]
153    pub fn contains_namespace(&self, namespace: Word) -> bool {
154        self.namespaces.contains(&namespace)
155    }
156
157    /// Checks if a symbol with the given name is a `Class`.
158    ///
159    /// # Arguments
160    ///
161    /// * `name`: The `Word` (likely FQCN) of the symbol to check.
162    ///
163    /// # Returns
164    ///
165    /// `true` if the symbol is a `Class`, `false` otherwise.
166    #[inline]
167    #[must_use]
168    pub fn contains_class(&self, name: Word) -> bool {
169        matches!(self.get_kind(name), Some(SymbolKind::Class))
170    }
171
172    /// Checks if a symbol with the given name is an `Interface`.
173    ///
174    /// # Arguments
175    ///
176    /// * `name`: The `Word` (likely FQCN) of the symbol to check.
177    ///
178    /// # Returns
179    ///
180    /// `true` if the symbol is an `Interface`, `false` otherwise.
181    #[inline]
182    #[must_use]
183    pub fn contains_interface(&self, name: Word) -> bool {
184        matches!(self.get_kind(name), Some(SymbolKind::Interface))
185    }
186
187    /// Checks if a symbol with the given name is a `Trait`.
188    ///
189    /// # Arguments
190    ///
191    /// * `name`: The `Word` (likely FQCN) of the symbol to check.
192    ///
193    /// # Returns
194    ///
195    /// `true` if the symbol is a `Trait`, `false` otherwise.
196    #[inline]
197    #[must_use]
198    pub fn contains_trait(&self, name: Word) -> bool {
199        matches!(self.get_kind(name), Some(SymbolKind::Trait))
200    }
201
202    /// Checks if a symbol with the given name is an `Enum`.
203    ///
204    /// # Arguments
205    ///
206    /// * `name`: The `Word` (likely FQCN) of the symbol to check.
207    ///
208    /// # Returns
209    ///
210    /// `true` if the symbol is an `Enum`, `false` otherwise.
211    #[inline]
212    #[must_use]
213    pub fn contains_enum(&self, name: Word) -> bool {
214        matches!(self.get_kind(name), Some(SymbolKind::Enum))
215    }
216
217    /// Returns a reference to the underlying map of all symbols.
218    #[inline]
219    #[must_use]
220    pub fn get_all(&self) -> &WordMap<SymbolKind> {
221        &self.all
222    }
223
224    /// Extends the current `Symbols` map with another one.
225    #[inline]
226    pub fn extend(&mut self, other: Symbols) {
227        self.namespaces.extend(other.namespaces);
228        for (entry, kind) in other.all {
229            self.all.entry(entry).or_insert(kind);
230        }
231    }
232
233    /// Extends the current `Symbols` map from a reference without consuming the source.
234    #[inline]
235    pub fn extend_ref(&mut self, other: &Symbols) {
236        self.namespaces.extend(other.namespaces.iter().copied());
237
238        for (entry, kind) in &other.all {
239            self.all.entry(*entry).or_insert(*kind);
240        }
241    }
242
243    /// Removes a symbol by its FQCN.
244    ///
245    /// Note: does not remove namespaces (they may be shared by other symbols).
246    #[inline]
247    pub fn remove(&mut self, name: Word) {
248        self.all.remove(&name);
249    }
250}
251
252/// Provides a default, empty `Symbols` map.
253impl Default for Symbols {
254    #[inline]
255    fn default() -> Self {
256        Self::new()
257    }
258}
259/// Returns an iterator that yields all parent namespaces of a given symbol.
260///
261/// For example, if the symbol is `Foo\Bar\Baz\Qux`, the iterator yields:
262/// 1. `Foo`
263/// 2. `Foo\Bar`
264/// 3. `Foo\Bar\Baz`
265pub(super) fn get_symbol_namespaces(symbol_name: Word) -> impl Iterator<Item = Word> {
266    let bytes: Vec<u8> = symbol_name.as_bytes().to_vec();
267
268    (0..bytes.len()).filter_map(move |i| if bytes[i] == b'\\' { Some(word(&bytes[..i])) } else { None })
269}