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,
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_deref(),
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::Function | NameContext::Const => {
215                let imported = if ctx == NameContext::Function {
216                    self.functions.get(&name.to_ascii_lowercase())
217                } else {
218                    self.consts.get(name)
219                };
220                match (imported, &self.namespace) {
221                    (Some(target), _) => ResolvedName::Fqn(target.clone()),
222                    (None, Some(ns)) => ResolvedName::Fallback {
223                        namespaced: format!("{ns}\\{name}"),
224                        global: name.to_owned(),
225                    },
226                    (None, None) => ResolvedName::Fqn(name.to_owned()),
227                }
228            }
229        }
230    }
231
232    fn prefix_namespace(&self, name: &str) -> String {
233        match &self.namespace {
234            Some(ns) => format!("{ns}\\{name}"),
235            None => name.to_owned(),
236        }
237    }
238}
239
240#[cfg(test)]
241mod tests {
242    use super::*;
243
244    fn fqn(s: &str) -> ResolvedName {
245        ResolvedName::Fqn(s.into())
246    }
247
248    fn resolver() -> NameResolver {
249        let mut r = NameResolver::new();
250        r.enter_namespace(Some("App"));
251        r.add_import(UseKind::Normal, "Lib\\Foo", None);
252        r.add_import(UseKind::Normal, "\\Lib\\Bar", Some("Baz"));
253        r.add_import(UseKind::Function, "Lib\\helper", None);
254        r.add_import(UseKind::Const, "Lib\\LIMIT", None);
255        r
256    }
257
258    #[test]
259    fn class_names() {
260        let r = resolver();
261        let c = NameContext::Class;
262        assert_eq!(
263            r.resolve(c, NameKind::Unqualified, &["foo"]),
264            fqn("Lib\\Foo")
265        );
266        assert_eq!(
267            r.resolve(c, NameKind::Unqualified, &["Baz"]),
268            fqn("Lib\\Bar")
269        );
270        assert_eq!(
271            r.resolve(c, NameKind::Unqualified, &["Other"]),
272            fqn("App\\Other")
273        );
274        assert_eq!(
275            r.resolve(c, NameKind::Qualified, &["Foo", "X"]),
276            fqn("Lib\\Foo\\X")
277        );
278        assert_eq!(
279            r.resolve(c, NameKind::Qualified, &["Sub", "X"]),
280            fqn("App\\Sub\\X")
281        );
282        assert_eq!(r.resolve(c, NameKind::FullyQualified, &["Foo"]), fqn("Foo"));
283        assert_eq!(r.resolve(c, NameKind::Relative, &["Foo"]), fqn("App\\Foo"));
284        assert_eq!(
285            r.resolve(c, NameKind::Unqualified, &["Self"]),
286            ResolvedName::Special(SpecialClass::Self_)
287        );
288    }
289
290    #[test]
291    fn function_and_const_names() {
292        let r = resolver();
293        assert_eq!(
294            r.resolve(NameContext::Function, NameKind::Unqualified, &["HELPER"]),
295            fqn("Lib\\helper")
296        );
297        assert_eq!(
298            r.resolve(NameContext::Function, NameKind::Unqualified, &["strlen"]),
299            ResolvedName::Fallback {
300                namespaced: "App\\strlen".into(),
301                global: "strlen".into()
302            }
303        );
304        assert_eq!(
305            r.resolve(NameContext::Const, NameKind::Unqualified, &["LIMIT"]),
306            fqn("Lib\\LIMIT")
307        );
308        // Const imports are case-sensitive.
309        assert!(matches!(
310            r.resolve(NameContext::Const, NameKind::Unqualified, &["limit"]),
311            ResolvedName::Fallback { .. }
312        ));
313    }
314
315    #[test]
316    fn global_namespace_and_reset() {
317        let mut r = resolver();
318        r.enter_namespace(None);
319        assert_eq!(
320            r.resolve(NameContext::Function, NameKind::Unqualified, &["strlen"]),
321            fqn("strlen")
322        );
323        assert_eq!(
324            r.resolve(NameContext::Class, NameKind::Unqualified, &["Foo"]),
325            fqn("Foo")
326        );
327    }
328
329    #[test]
330    fn error_name() {
331        let r = NameResolver::new();
332        assert_eq!(
333            r.resolve::<&str>(NameContext::Class, NameKind::Error, &[]),
334            ResolvedName::Error
335        );
336    }
337}