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