Skip to main content

php_ast/
resolve.rs

1//! Namespace-aware name resolution for `use` imports.
2
3use std::collections::HashMap;
4
5use crate::ast::{NameKind, NamespaceDecl, Stmt, StmtKind, UseKind};
6use crate::owned;
7
8/// Which import table a name is looked up in.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10#[non_exhaustive]
11pub enum NameContext {
12    /// Classes, interfaces, traits, enums.
13    Class,
14    /// Functions.
15    Function,
16    /// Constants.
17    Const,
18}
19
20/// Result of resolving a name against the current namespace and imports.
21#[derive(Debug, Clone, PartialEq, Eq)]
22#[non_exhaustive]
23pub enum ResolvedName {
24    /// Fully qualified name without a leading backslash.
25    Fqn(String),
26    /// Unqualified function/const in a namespace with no matching import:
27    /// PHP tries `namespaced` first, then `global`.
28    Fallback {
29        /// Candidate inside the current namespace.
30        namespaced: String,
31        /// Candidate in the global namespace.
32        global: String,
33    },
34    /// `self`, `static` or `parent`; needs class context to resolve.
35    Special(SpecialClass),
36    /// Synthesised error-recovery name.
37    Error,
38}
39
40/// A class name that refers to the enclosing class or its parent.
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
42pub enum SpecialClass {
43    /// `self`
44    Self_,
45    /// `static`
46    Static,
47    /// `parent`
48    Parent,
49}
50
51impl SpecialClass {
52    /// The lowercase keyword.
53    pub fn as_str(self) -> &'static str {
54        match self {
55            SpecialClass::Self_ => "self",
56            SpecialClass::Static => "static",
57            SpecialClass::Parent => "parent",
58        }
59    }
60}
61
62/// Tracks the current namespace and `use` imports and resolves names against them.
63///
64/// Feed every statement to [`observe_stmt`](Self::observe_stmt) in source order
65/// (e.g. from `ScopeVisitor::visit_stmt`), then call [`resolve_name`](Self::resolve_name).
66/// Imports are cleared at each `namespace` statement; they are not popped at the end
67/// of a braced namespace block.
68///
69/// Builtin type names (`int`, `string`, …) are not special-cased.
70#[derive(Debug, Clone, Default)]
71pub struct NameResolver {
72    namespace: Option<String>,
73    classes: HashMap<String, String>,
74    functions: HashMap<String, String>,
75    consts: HashMap<String, String>,
76}
77
78impl NameResolver {
79    /// Creates a resolver in the global namespace with no imports.
80    pub fn new() -> Self {
81        Self::default()
82    }
83
84    /// Current namespace, or `None` for the global namespace.
85    pub fn namespace(&self) -> Option<&str> {
86        self.namespace.as_deref()
87    }
88
89    /// Enters `namespace` and clears all imports.
90    pub fn enter_namespace(&mut self, namespace: Option<&str>) {
91        self.namespace = namespace.filter(|n| !n.is_empty()).map(str::to_owned);
92        self.classes.clear();
93        self.functions.clear();
94        self.consts.clear();
95    }
96
97    /// Registers an import. `fqn` may carry a leading backslash; `alias` defaults
98    /// to the last segment of `fqn`.
99    pub fn add_import(&mut self, kind: UseKind, fqn: &str, alias: Option<&str>) {
100        let fqn = fqn.trim_start_matches('\\');
101        let local = alias.unwrap_or_else(|| fqn.rsplit('\\').next().unwrap_or(fqn));
102        if local.is_empty() {
103            return;
104        }
105        match kind {
106            UseKind::Normal => self
107                .classes
108                .insert(local.to_ascii_lowercase(), fqn.to_owned()),
109            UseKind::Function => self
110                .functions
111                .insert(local.to_ascii_lowercase(), fqn.to_owned()),
112            UseKind::Const => self.consts.insert(local.to_owned(), fqn.to_owned()),
113        };
114    }
115
116    /// Updates state for `namespace` and `use` statements; ignores everything else.
117    pub fn observe_stmt(&mut self, stmt: &Stmt<'_, '_>) {
118        match &stmt.kind {
119            StmtKind::Namespace(ns) => self.observe_namespace(ns),
120            StmtKind::Use(decl) => {
121                for item in decl.uses.iter() {
122                    self.add_import(
123                        item.kind.unwrap_or(decl.kind),
124                        &item.name.join_parts(),
125                        item.alias.and_then(|a| a.as_str()),
126                    );
127                }
128            }
129            _ => {}
130        }
131    }
132
133    fn observe_namespace(&mut self, ns: &NamespaceDecl<'_, '_>) {
134        let name = ns.name.as_ref().map(|n| n.join_parts());
135        self.enter_namespace(name.as_deref());
136    }
137
138    /// Owned-AST counterpart of [`observe_stmt`](Self::observe_stmt).
139    pub fn observe_owned_stmt(&mut self, stmt: &owned::Stmt) {
140        match &stmt.kind {
141            owned::StmtKind::Namespace(ns) => {
142                let name = ns.name.as_ref().map(|n| n.parts.join("\\"));
143                self.enter_namespace(name.as_deref());
144            }
145            owned::StmtKind::Use(decl) => {
146                for item in decl.uses.iter() {
147                    self.add_import(
148                        item.kind.unwrap_or(decl.kind),
149                        &item.name.parts.join("\\"),
150                        item.alias.as_ref().and_then(|a| a.as_str()),
151                    );
152                }
153            }
154            _ => {}
155        }
156    }
157
158    /// Resolves a borrowed-AST name.
159    pub fn resolve_name(&self, name: &crate::ast::Name<'_, '_>, ctx: NameContext) -> ResolvedName {
160        self.resolve(ctx, name.kind(), name.parts_slice())
161    }
162
163    /// Resolves an owned-AST name.
164    pub fn resolve_owned_name(&self, name: &owned::Name, ctx: NameContext) -> ResolvedName {
165        self.resolve(ctx, name.kind, &name.parts)
166    }
167
168    /// Resolves a name given as its kind and `\`-separated segments.
169    pub fn resolve<S: AsRef<str>>(
170        &self,
171        ctx: NameContext,
172        kind: NameKind,
173        parts: &[S],
174    ) -> ResolvedName {
175        let join = |parts: &[S]| {
176            parts
177                .iter()
178                .map(AsRef::as_ref)
179                .collect::<Vec<_>>()
180                .join("\\")
181        };
182        if parts.is_empty() || kind == NameKind::Error {
183            return ResolvedName::Error;
184        }
185        match kind {
186            NameKind::FullyQualified => ResolvedName::Fqn(join(parts)),
187            NameKind::Relative => ResolvedName::Fqn(self.prefix_namespace(&join(parts))),
188            NameKind::Qualified => {
189                let first = parts[0].as_ref().to_ascii_lowercase();
190                match self.classes.get(&first) {
191                    Some(target) => ResolvedName::Fqn(format!("{target}\\{}", join(&parts[1..]))),
192                    None => ResolvedName::Fqn(self.prefix_namespace(&join(parts))),
193                }
194            }
195            _ => self.resolve_unqualified(ctx, parts[0].as_ref()),
196        }
197    }
198
199    fn resolve_unqualified(&self, ctx: NameContext, name: &str) -> ResolvedName {
200        match ctx {
201            NameContext::Class => {
202                let lower = name.to_ascii_lowercase();
203                match lower.as_str() {
204                    "self" => return ResolvedName::Special(SpecialClass::Self_),
205                    "static" => return ResolvedName::Special(SpecialClass::Static),
206                    "parent" => return ResolvedName::Special(SpecialClass::Parent),
207                    _ => {}
208                }
209                match self.classes.get(&lower) {
210                    Some(target) => ResolvedName::Fqn(target.clone()),
211                    None => ResolvedName::Fqn(self.prefix_namespace(name)),
212                }
213            }
214            NameContext::Const
215                if ["true", "false", "null"]
216                    .iter()
217                    .any(|c| name.eq_ignore_ascii_case(c)) =>
218            {
219                // PHP binds these at compile time and forbids importing them.
220                ResolvedName::Fqn(name.to_owned())
221            }
222            NameContext::Function | NameContext::Const => {
223                let imported = if ctx == NameContext::Function {
224                    self.functions.get(&name.to_ascii_lowercase())
225                } else {
226                    self.consts.get(name)
227                };
228                match (imported, &self.namespace) {
229                    (Some(target), _) => ResolvedName::Fqn(target.clone()),
230                    (None, Some(ns)) => ResolvedName::Fallback {
231                        namespaced: format!("{ns}\\{name}"),
232                        global: name.to_owned(),
233                    },
234                    (None, None) => ResolvedName::Fqn(name.to_owned()),
235                }
236            }
237        }
238    }
239
240    fn prefix_namespace(&self, name: &str) -> String {
241        match &self.namespace {
242            Some(ns) => format!("{ns}\\{name}"),
243            None => name.to_owned(),
244        }
245    }
246}
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251
252    fn fqn(s: &str) -> ResolvedName {
253        ResolvedName::Fqn(s.into())
254    }
255
256    fn resolver() -> NameResolver {
257        let mut r = NameResolver::new();
258        r.enter_namespace(Some("App"));
259        r.add_import(UseKind::Normal, "Lib\\Foo", None);
260        r.add_import(UseKind::Normal, "\\Lib\\Bar", Some("Baz"));
261        r.add_import(UseKind::Function, "Lib\\helper", None);
262        r.add_import(UseKind::Const, "Lib\\LIMIT", None);
263        r
264    }
265
266    #[test]
267    fn class_names() {
268        let r = resolver();
269        let c = NameContext::Class;
270        assert_eq!(
271            r.resolve(c, NameKind::Unqualified, &["foo"]),
272            fqn("Lib\\Foo")
273        );
274        assert_eq!(
275            r.resolve(c, NameKind::Unqualified, &["Baz"]),
276            fqn("Lib\\Bar")
277        );
278        assert_eq!(
279            r.resolve(c, NameKind::Unqualified, &["Other"]),
280            fqn("App\\Other")
281        );
282        assert_eq!(
283            r.resolve(c, NameKind::Qualified, &["Foo", "X"]),
284            fqn("Lib\\Foo\\X")
285        );
286        assert_eq!(
287            r.resolve(c, NameKind::Qualified, &["Sub", "X"]),
288            fqn("App\\Sub\\X")
289        );
290        assert_eq!(r.resolve(c, NameKind::FullyQualified, &["Foo"]), fqn("Foo"));
291        assert_eq!(r.resolve(c, NameKind::Relative, &["Foo"]), fqn("App\\Foo"));
292        assert_eq!(
293            r.resolve(c, NameKind::Unqualified, &["Self"]),
294            ResolvedName::Special(SpecialClass::Self_)
295        );
296    }
297
298    #[test]
299    fn function_and_const_names() {
300        let r = resolver();
301        assert_eq!(
302            r.resolve(NameContext::Function, NameKind::Unqualified, &["HELPER"]),
303            fqn("Lib\\helper")
304        );
305        assert_eq!(
306            r.resolve(NameContext::Function, NameKind::Unqualified, &["strlen"]),
307            ResolvedName::Fallback {
308                namespaced: "App\\strlen".into(),
309                global: "strlen".into()
310            }
311        );
312        assert_eq!(
313            r.resolve(NameContext::Const, NameKind::Unqualified, &["LIMIT"]),
314            fqn("Lib\\LIMIT")
315        );
316        assert_eq!(
317            r.resolve(NameContext::Const, NameKind::Unqualified, &["NULL"]),
318            fqn("NULL")
319        );
320        assert_eq!(
321            r.resolve(NameContext::Const, NameKind::Unqualified, &["true"]),
322            fqn("true")
323        );
324        assert_eq!(
325            r.resolve(NameContext::Const, NameKind::Qualified, &["Sub", "null"]),
326            fqn("App\\Sub\\null")
327        );
328        // Const imports are case-sensitive.
329        assert!(matches!(
330            r.resolve(NameContext::Const, NameKind::Unqualified, &["limit"]),
331            ResolvedName::Fallback { .. }
332        ));
333    }
334
335    #[test]
336    fn global_namespace_and_reset() {
337        let mut r = resolver();
338        r.enter_namespace(None);
339        assert_eq!(
340            r.resolve(NameContext::Function, NameKind::Unqualified, &["strlen"]),
341            fqn("strlen")
342        );
343        assert_eq!(
344            r.resolve(NameContext::Class, NameKind::Unqualified, &["Foo"]),
345            fqn("Foo")
346        );
347    }
348
349    #[test]
350    fn error_name() {
351        let r = NameResolver::new();
352        assert_eq!(
353            r.resolve::<&str>(NameContext::Class, NameKind::Error, &[]),
354            ResolvedName::Error
355        );
356    }
357}