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, Default)]
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 the given kind.
86 #[inline]
87 pub fn add_symbol_name(&mut self, name: Word, kind: SymbolKind) {
88 self.namespaces.extend(get_symbol_namespaces(name));
89 self.all.insert(name, kind);
90 }
91
92 /// Retrieves the `SymbolKind` for a given symbol name, if known.
93 ///
94 /// # Arguments
95 ///
96 /// * `name`: The `Word` (likely FQCN) of the symbol to look up.
97 ///
98 /// # Returns
99 ///
100 /// `Some(SymbolKind)` if the symbol exists in the map, `None` otherwise.
101 #[inline]
102 #[must_use]
103 pub fn get_kind(&self, name: Word) -> Option<SymbolKind> {
104 self.all.get(&name).copied() // Use copied() since SymbolKind is Copy
105 }
106
107 /// Checks if a symbol with the given name is known.
108 ///
109 /// # Arguments
110 ///
111 /// * `name`: The `Word` (likely FQCN) of the symbol to check.
112 ///
113 /// # Returns
114 ///
115 /// `true` if the symbol exists in the map, `false` otherwise.
116 #[inline]
117 #[must_use]
118 pub fn contains(&self, name: Word) -> bool {
119 self.all.contains_key(&name)
120 }
121
122 /// Check if any symbol within the table is part of the given namespace.
123 ///
124 /// # Arguments
125 ///
126 /// * `namespace`: The `Word` of the namespace to check for.
127 ///
128 /// # Returns
129 ///
130 /// `true` if the namespace is present, `false` otherwise.
131 #[must_use]
132 pub fn contains_namespace(&self, namespace: Word) -> bool {
133 self.namespaces.contains(&namespace)
134 }
135
136 /// Checks if a symbol with the given name is an `Enum`.
137 ///
138 /// # Arguments
139 ///
140 /// * `name`: The `Word` (likely FQCN) of the symbol to check.
141 ///
142 /// # Returns
143 ///
144 /// `true` if the symbol is an `Enum`, `false` otherwise.
145 #[inline]
146 #[must_use]
147 pub fn contains_enum(&self, name: Word) -> bool {
148 matches!(self.get_kind(name), Some(SymbolKind::Enum))
149 }
150
151 /// Extends the current `Symbols` map with another one.
152 #[inline]
153 pub fn extend(&mut self, other: Symbols) {
154 self.namespaces.extend(other.namespaces);
155 for (entry, kind) in other.all {
156 self.all.entry(entry).or_insert(kind);
157 }
158 }
159
160 /// Extends the current `Symbols` map from a reference without consuming the source.
161 #[inline]
162 pub fn extend_ref(&mut self, other: &Symbols) {
163 self.namespaces.extend(other.namespaces.iter().copied());
164
165 for (entry, kind) in &other.all {
166 self.all.entry(*entry).or_insert(*kind);
167 }
168 }
169
170 /// Removes a symbol by its FQCN.
171 ///
172 /// Note: does not remove namespaces (they may be shared by other symbols).
173 #[inline]
174 pub fn remove(&mut self, name: Word) {
175 self.all.remove(&name);
176 }
177}
178
179/// Returns an iterator that yields all parent namespaces of a given symbol.
180///
181/// For example, if the symbol is `Foo\Bar\Baz\Qux`, the iterator yields:
182/// 1. `Foo`
183/// 2. `Foo\Bar`
184/// 3. `Foo\Bar\Baz`
185pub(super) fn get_symbol_namespaces(symbol_name: Word) -> impl Iterator<Item = Word> {
186 let bytes: Vec<u8> = symbol_name.as_bytes().to_vec();
187
188 (0..bytes.len()).filter_map(move |i| if bytes[i] == b'\\' { Some(word(&bytes[..i])) } else { None })
189}