Skip to main content

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}