mago_names/scope.rs
1use std::borrow::Cow;
2
3use foldhash::HashMap;
4use serde::Deserialize;
5use serde::Serialize;
6
7use mago_syntax::ast::Use;
8use mago_syntax::ast::UseItems;
9use mago_syntax::ast::UseType;
10
11use crate::kind::NameKind;
12
13/// Represents the scope for resolving PHP names, holding the current namespace
14/// and any 'use' aliases defined within it.
15///
16/// This struct keeps track of the current namespace and the different types
17/// of aliases (`use`, `use function`, `use const`). It provides methods to
18/// manage aliases and resolve names according to PHP's rules (handling FQNs,
19/// aliases, the `namespace` keyword, and namespace relativity).
20///
21/// Aliases are stored case-insensitively (keys in the maps are lowercase)
22/// but resolve to the original case-sensitive FQN.
23#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, Default)]
24pub struct NamespaceScope {
25 /// The fully qualified name of the current namespace context (e.g., "App\\Http\\Controllers").
26 /// `None` indicates the global namespace.
27 namespace_name: Option<String>,
28
29 /// Stores aliases for classes, interfaces, traits, and namespaces.
30 /// Key: Lowercase alias name. Value: FQN.
31 default_aliases: HashMap<String, String>,
32
33 /// Stores aliases for functions.
34 /// Key: Lowercase alias name. Value: FQN.
35 function_aliases: HashMap<String, String>,
36
37 /// Stores aliases for constants.
38 /// Key: Lowercase alias name. Value: FQN.
39 constant_aliases: HashMap<String, String>,
40}
41
42impl NamespaceScope {
43 /// Creates a new, empty scope, optionally associated with a namespace.
44 #[must_use]
45 pub fn new(namespace_name: Option<String>) -> Self {
46 NamespaceScope {
47 namespace_name,
48 default_aliases: HashMap::default(),
49 function_aliases: HashMap::default(),
50 constant_aliases: HashMap::default(),
51 }
52 }
53
54 /// Creates a new, empty scope representing the global namespace.
55 #[must_use]
56 pub fn global() -> Self {
57 NamespaceScope {
58 namespace_name: None,
59 default_aliases: HashMap::default(),
60 function_aliases: HashMap::default(),
61 constant_aliases: HashMap::default(),
62 }
63 }
64
65 /// Creates a new, empty scope representing the given namespace.
66 #[inline]
67 pub fn for_namespace(namespace: impl Into<String>) -> Self {
68 NamespaceScope {
69 namespace_name: Some(namespace.into()),
70 default_aliases: HashMap::default(),
71 function_aliases: HashMap::default(),
72 constant_aliases: HashMap::default(),
73 }
74 }
75
76 /// Checks if any aliases have been defined in this scope.
77 #[must_use]
78 pub fn has_aliases(&self) -> bool {
79 // Corrected implementation for checking if *any* alias map is non-empty
80 !self.default_aliases.is_empty() || !self.function_aliases.is_empty() || !self.constant_aliases.is_empty()
81 }
82
83 /// Returns the name of the current namespace, if this scope represents one.
84 #[must_use]
85 pub fn namespace_name(&self) -> Option<&str> {
86 self.namespace_name.as_deref()
87 }
88
89 /// Returns a reference to the map of default (class/namespace) aliases.
90 #[must_use]
91 pub fn default_aliases(&self) -> &HashMap<String, String> {
92 &self.default_aliases
93 }
94
95 /// Returns a reference to the map of function aliases.
96 #[must_use]
97 pub fn function_aliases(&self) -> &HashMap<String, String> {
98 &self.function_aliases
99 }
100
101 /// Returns a reference to the map of constant aliases.
102 #[must_use]
103 pub fn constant_aliases(&self) -> &HashMap<String, String> {
104 &self.constant_aliases
105 }
106
107 /// Populates the scope's alias tables based on a PHP `use` statement AST node.
108 ///
109 /// This method processes the different forms of `use` statements (simple, function/const,
110 /// grouped) and registers the corresponding aliases within this `NamespaceScope`.
111 /// It iterates through the items declared in the `use` statement and calls `self.add`
112 /// for each one with the appropriate kind, fully qualified name, and optional alias.
113 ///
114 /// # Arguments
115 ///
116 /// * `interner` - A string interner used to resolve identifiers/names from the AST nodes
117 /// into actual string (`&str`) representations.
118 /// * `r#use` - A reference to the `Use` AST node representing the `use` statement.
119 /// (Parameter is named `r#use` because `use` is a Rust keyword).
120 pub fn populate_from_use(&mut self, r#use: &Use<'_>) {
121 match &r#use.items {
122 UseItems::Sequence(use_item_sequence) => {
123 for use_item in &use_item_sequence.items {
124 let name = use_item.name.value().trim_start_matches('\\');
125 let alias = use_item.alias.as_ref().map(|alias_node| alias_node.identifier.value);
126
127 // Add as a default (class/namespace) alias
128 self.add(NameKind::Default, name, &alias);
129 }
130 }
131 UseItems::TypedSequence(typed_use_item_sequence) => {
132 // Determine if it's a function or const import based on the type node
133 let name_kind = match &typed_use_item_sequence.r#type {
134 UseType::Function(_) => NameKind::Function,
135 UseType::Const(_) => NameKind::Constant,
136 };
137
138 for use_item in &typed_use_item_sequence.items {
139 let name = use_item.name.value().trim_start_matches('\\');
140 let alias = use_item.alias.as_ref().map(|alias_node| alias_node.identifier.value);
141
142 // Add with the determined kind (Function or Constant)
143 self.add(name_kind, name, &alias);
144 }
145 }
146 UseItems::TypedList(typed_use_item_list) => {
147 // Determine the kind for the entire group
148 let name_kind = match &typed_use_item_list.r#type {
149 UseType::Function(_) => NameKind::Function,
150 UseType::Const(_) => NameKind::Constant,
151 };
152
153 // Get the common namespace prefix for the group
154 let prefix = (typed_use_item_list.namespace.value()).trim_start_matches('\\');
155
156 for use_item in &typed_use_item_list.items {
157 let name_part = use_item.name.value();
158 let alias = use_item.alias.as_ref().map(|alias_node| &alias_node.identifier.value);
159
160 // Construct the full FQN by combining prefix and name part
161 let fully_qualified_name = format!("{prefix}\\{name_part}");
162
163 // Add the alias for the fully constructed name
164 self.add(name_kind, fully_qualified_name, &alias);
165 }
166 }
167 UseItems::MixedList(mixed_use_item_list) => {
168 // Get the common namespace prefix for the group
169 let prefix = (mixed_use_item_list.namespace.value()).trim_start_matches('\\');
170
171 for mixed_use_item in &mixed_use_item_list.items {
172 // Determine the kind for *this specific item* within the mixed list
173 let name_kind = match &mixed_use_item.r#type {
174 None => NameKind::Default, // No type specified, defaults to class/namespace
175 Some(UseType::Function(_)) => NameKind::Function,
176 Some(UseType::Const(_)) => NameKind::Constant,
177 };
178
179 // Extract name/alias from the nested item structure (assuming `item` field)
180 let name_part = mixed_use_item.item.name.value();
181 let alias = mixed_use_item.item.alias.as_ref().map(|alias_node| &alias_node.identifier.value);
182
183 // Construct the full FQN: prefix\name_part
184 let fully_qualified_name = format!("{prefix}\\{name_part}");
185
186 // Add the alias with its specific kind
187 self.add(name_kind, fully_qualified_name, &alias);
188 }
189 }
190 }
191 }
192
193 /// Adds a new alias based on a 'use' statement to this scope.
194 ///
195 /// # Arguments
196 ///
197 /// * `kind` - The type of alias (`NameKind::Default`, `NameKind::Function`, `NameKind::Constant`).
198 /// * `name` - The fully qualified name being imported (e.g., "App\\Models\\User").
199 /// * `alias` - An optional explicit alias name (e.g., "`UserModel`"). If `None`, the alias
200 /// is derived from the last part of the `name` (e.g., "User" from "App\\Models\\User").
201 ///
202 /// The alias name (explicit or derived) is stored lowercase as the key.
203 #[inline]
204 pub fn add(&mut self, kind: NameKind, name: impl AsRef<str>, alias: &Option<impl AsRef<str>>) {
205 self.add_str(kind, name.as_ref(), alias.as_ref().map(std::convert::AsRef::as_ref));
206 }
207
208 /// non-generic version of `add` that takes a string slice.
209 fn add_str(&mut self, kind: NameKind, name_ref: &str, alias: Option<&str>) {
210 let alias_key = match alias {
211 Some(alias) => alias.to_ascii_lowercase(),
212 None => {
213 if let Some(last_backslash_pos) = name_ref.rfind('\\') {
214 name_ref[last_backslash_pos + 1..].to_ascii_lowercase()
215 } else {
216 name_ref.to_ascii_lowercase()
217 }
218 }
219 };
220
221 match kind {
222 NameKind::Default => self.default_aliases.insert(alias_key, name_ref.to_owned()),
223 NameKind::Function => self.function_aliases.insert(alias_key, name_ref.to_owned()),
224 NameKind::Constant => self.constant_aliases.insert(alias_key, name_ref.to_owned()),
225 };
226 }
227
228 /// Qualifies a simple name by prepending the current namespace, if applicable.
229 ///
230 /// This method is intended for simple names (containing no `\`) that are
231 /// *not* explicitly aliased or handled by `\` or `namespace\` prefixes.
232 /// If the current scope has a non-empty namespace name, it will be prepended.
233 /// Otherwise, the original name is returned.
234 ///
235 /// # Arguments
236 ///
237 /// * `name` - The simple name to qualify (e.g., "User").
238 ///
239 /// # Returns
240 ///
241 /// The qualified name (e.g., "App\\Models\\User") or the original name if
242 /// in the global scope, the namespace is empty, or the input name was not simple.
243 #[inline]
244 pub fn qualify_name(&self, name: impl AsRef<str>) -> String {
245 self.qualify_name_str(name.as_ref()).into_owned()
246 }
247
248 /// non-generic version of `qualify_name` that takes a string slice.
249 fn qualify_name_str<'a>(&self, name_ref: &'a str) -> Cow<'a, str> {
250 match &self.namespace_name {
251 // If we have a non-empty namespace, prepend it.
252 Some(ns) if !ns.is_empty() => Cow::Owned(format!("{ns}\\{name_ref}")),
253 // Otherwise (no namespace, or empty namespace), return the name as is.
254 _ => Cow::Borrowed(name_ref),
255 }
256 }
257
258 /// Resolves a name fully according to PHP's rules within this scope.
259 ///
260 /// # Arguments
261 /// * `kind` - The context (`Default`, `Function`, `Constant`).
262 /// * `name` - The name string to resolve.
263 ///
264 /// # Returns
265 ///
266 /// A tuple `(String, bool)`:
267 /// - The resolved or qualified name.
268 /// - `true` if resolved via explicit alias/construct (step 1), `false` otherwise.
269 #[inline]
270 pub fn resolve(&self, kind: NameKind, name: impl AsRef<str>) -> (String, bool) {
271 let (cow, imported) = self.resolve_str(kind, name.as_ref());
272
273 (cow.into_owned(), imported)
274 }
275
276 /// non-generic version of `resolve` that takes a string slice.
277 #[inline]
278 #[must_use]
279 pub fn resolve_str<'a>(&self, kind: NameKind, name_ref: &'a str) -> (Cow<'a, str>, bool) {
280 // Try resolving using explicit aliases and constructs
281 if let Some(resolved_name) = self.resolve_alias_str(kind, name_ref) {
282 return (resolved_name, true); // Resolved via alias or explicit construct
283 }
284
285 // Qualify it using the current namespace.
286 (self.qualify_name_str(name_ref), false)
287 }
288
289 /// Attempts to resolve a name using *only* explicit aliases and constructs.
290 ///
291 /// Does *not* attempt to qualify simple names relative to the current namespace if they aren't aliased.
292 ///
293 /// # Arguments
294 ///
295 /// * `kind` - The context (`Default`, `Function`, `Constant`).
296 /// * `name` - The name string to resolve.
297 ///
298 /// # Returns
299 ///
300 /// * `Some(String)` containing the resolved FQN if an explicit rule applies.
301 /// * `None` if no explicit rule resolves the name.
302 #[inline]
303 pub fn resolve_alias(&self, kind: NameKind, name: impl AsRef<str>) -> Option<String> {
304 self.resolve_alias_str(kind, name.as_ref()).map(std::borrow::Cow::into_owned)
305 }
306
307 /// non-generic version of `resolve_alias` that takes a string slice.
308 fn resolve_alias_str<'a>(&self, kind: NameKind, name_ref: &'a str) -> Option<Cow<'a, str>> {
309 if name_ref.is_empty() {
310 return None;
311 }
312
313 // Handle `\FQN`
314 if let Some(fqn) = name_ref.strip_prefix('\\') {
315 return Some(Cow::Borrowed(fqn));
316 }
317
318 let parts = name_ref.split('\\').collect::<Vec<_>>();
319 let first_part = parts[0];
320 let first_part_lower = first_part.to_ascii_lowercase();
321
322 if parts.len() > 1 {
323 let suffix = parts[1..].join("\\");
324
325 // Handle `namespace\Suffix`
326 if first_part_lower == "namespace" {
327 match &self.namespace_name {
328 Some(namespace_prefix) => {
329 let mut resolved = namespace_prefix.clone();
330 resolved.push('\\');
331 resolved.push_str(&suffix);
332 Some(Cow::Owned(resolved))
333 }
334 None => Some(Cow::Owned(suffix)), // Relative to global "" namespace
335 }
336 } else {
337 // Handle `Alias\Suffix`
338 match self.default_aliases.get(&first_part_lower) {
339 Some(resolved_alias_fqn) => {
340 let mut resolved = resolved_alias_fqn.clone();
341 resolved.push('\\');
342 resolved.push_str(&suffix);
343 Some(Cow::Owned(resolved))
344 }
345 None => None, // Alias not found
346 }
347 }
348 } else {
349 // Handle single-part alias lookup
350 (match kind {
351 NameKind::Default => self.default_aliases.get(&first_part_lower).cloned(),
352 NameKind::Function => self.function_aliases.get(&first_part_lower).cloned(),
353 NameKind::Constant => self.constant_aliases.get(&first_part_lower).cloned(),
354 })
355 .map(Cow::Owned)
356 }
357 }
358}