Skip to main content

php_lsp/
resolve.rs

1//! Centralized cursor resolution.
2//!
3//! `goto_definition`, `goto_declaration`, and `hover` all needed to answer the
4//! same question — *"which declaration in this AST is named `word`?"* — and each
5//! had its own near-identical statement walker (`scan_statements`,
6//! `find_any_declaration`, …). [`resolve_declaration`] is the single walker; it returns
7//! a borrowed handle to the matched node ([`Declaration`]) and leaves rendering (range
8//! vs. signature vs. abstract-filtering) to the caller.
9//!
10//! The walker performs *name matching*, not full cursor-context classification:
11//! it matches a declaration whose name equals `word`, exactly as the three
12//! original copies did. Distinguishing "method call vs. class name at this
13//! offset" is a separate, behavior-changing concern and intentionally not done
14//! here.
15//!
16//! Callers narrow the match with an `accept` predicate. Returning `false` means
17//! "skip this candidate and keep looking", mirroring the `_ => {}` fall-through
18//! in the original walkers. This is what lets declaration's two-pass logic
19//! (abstract first, then any) reuse the same traversal.
20
21use php_ast::{
22    ClassConstDecl, ClassDecl, ClassMemberKind, EnumCase, EnumDecl, EnumMemberKind, FunctionDecl,
23    Ident, InterfaceDecl, MethodDecl, NamespaceBody, Param, PropertyDecl, Span, Stmt, StmtKind,
24    TraitDecl,
25};
26
27use crate::util::strip_variable_sigil;
28
29/// Which type-like declaration a member belongs to. Lets callers reproduce
30/// per-container behavior (e.g. definition resolves enum constants differently
31/// from class constants) without re-walking the AST.
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum Container {
34    Class,
35    Interface,
36    Trait,
37    Enum,
38}
39
40/// A declaration node matched by name. Each variant borrows the matched AST node
41/// (arena-allocated, so it outlives the walk) plus the span(s) callers need to
42/// compute a precise name range.
43pub enum Declaration<'a> {
44    Function {
45        decl: &'a FunctionDecl<'a, 'a>,
46        stmt_span: Span,
47    },
48    Class {
49        decl: &'a ClassDecl<'a, 'a>,
50        /// The class name (always present — anonymous classes never match by name).
51        name: Ident<'a>,
52        stmt_span: Span,
53    },
54    Interface {
55        decl: &'a InterfaceDecl<'a, 'a>,
56        stmt_span: Span,
57    },
58    Trait {
59        decl: &'a TraitDecl<'a, 'a>,
60        stmt_span: Span,
61    },
62    Enum {
63        decl: &'a EnumDecl<'a, 'a>,
64        stmt_span: Span,
65    },
66    Method {
67        method: &'a MethodDecl<'a, 'a>,
68        container: Container,
69        member_span: Span,
70    },
71    ClassConst {
72        konst: &'a ClassConstDecl<'a, 'a>,
73        container: Container,
74        member_span: Span,
75    },
76    Property {
77        property: &'a PropertyDecl<'a, 'a>,
78        container: Container,
79        member_span: Span,
80    },
81    /// A constructor-promoted parameter, which acts as a property declaration.
82    PromotedParam { param: &'a Param<'a, 'a> },
83    EnumCase {
84        case: &'a EnumCase<'a, 'a>,
85        enum_name: Ident<'a>,
86        member_span: Span,
87    },
88}
89
90impl<'a> Declaration<'a> {
91    /// The identifier the cursor matched (without any `$` sigil).
92    pub fn name(&self) -> &'a str {
93        match self {
94            Declaration::Function { decl, .. } => decl.name.or_error(),
95            Declaration::Class { name, .. } => name.or_error(),
96            Declaration::Interface { decl, .. } => decl.name.or_error(),
97            Declaration::Trait { decl, .. } => decl.name.or_error(),
98            Declaration::Enum { decl, .. } => decl.name.or_error(),
99            Declaration::Method { method, .. } => method.name.or_error(),
100            Declaration::ClassConst { konst, .. } => konst.name.or_error(),
101            Declaration::Property { property, .. } => property.name.or_error(),
102            Declaration::PromotedParam { param } => param.name.or_error(),
103            Declaration::EnumCase { case, .. } => case.name.or_error(),
104        }
105    }
106}
107
108/// Find the first declaration named `word` that `accept` approves, scanning
109/// `stmts` in source order and recursing into braced namespaces.
110///
111/// A `$` sigil on `word` is stripped before matching property / promoted-param
112/// names (which are stored without it), matching the original walkers.
113pub fn resolve_declaration<'a>(
114    stmts: &'a [Stmt<'a, 'a>],
115    word: &str,
116    accept: &dyn Fn(&Declaration<'a>) -> bool,
117) -> Option<Declaration<'a>> {
118    let bare = strip_variable_sigil(word);
119    for stmt in stmts {
120        match &stmt.kind {
121            StmtKind::Function(f) if f.name == word => {
122                let d = Declaration::Function {
123                    decl: f,
124                    stmt_span: stmt.span,
125                };
126                if accept(&d) {
127                    return Some(d);
128                }
129            }
130            StmtKind::Class(c) => {
131                // Class name takes priority over members (match-arm order in the
132                // originals); fall through to members when the name is rejected.
133                if let Some(name) = c.name
134                    && name.or_error() == word
135                {
136                    let d = Declaration::Class {
137                        decl: c,
138                        name,
139                        stmt_span: stmt.span,
140                    };
141                    if accept(&d) {
142                        return Some(d);
143                    }
144                }
145                if let Some(d) =
146                    resolve_member(c.body.members.iter(), word, bare, Container::Class, accept)
147                {
148                    return Some(d);
149                }
150            }
151            StmtKind::Interface(i) => {
152                if i.name == word {
153                    let d = Declaration::Interface {
154                        decl: i,
155                        stmt_span: stmt.span,
156                    };
157                    if accept(&d) {
158                        return Some(d);
159                    }
160                }
161                if let Some(d) = resolve_member(
162                    i.body.members.iter(),
163                    word,
164                    bare,
165                    Container::Interface,
166                    accept,
167                ) {
168                    return Some(d);
169                }
170            }
171            StmtKind::Trait(t) => {
172                if t.name == word {
173                    let d = Declaration::Trait {
174                        decl: t,
175                        stmt_span: stmt.span,
176                    };
177                    if accept(&d) {
178                        return Some(d);
179                    }
180                }
181                if let Some(d) =
182                    resolve_member(t.body.members.iter(), word, bare, Container::Trait, accept)
183                {
184                    return Some(d);
185                }
186            }
187            StmtKind::Enum(e) => {
188                if e.name == word {
189                    let d = Declaration::Enum {
190                        decl: e,
191                        stmt_span: stmt.span,
192                    };
193                    if accept(&d) {
194                        return Some(d);
195                    }
196                }
197                for member in e.body.members.iter() {
198                    match &member.kind {
199                        EnumMemberKind::Case(c) if c.name == word => {
200                            let d = Declaration::EnumCase {
201                                case: c,
202                                enum_name: e.name,
203                                member_span: member.span,
204                            };
205                            if accept(&d) {
206                                return Some(d);
207                            }
208                        }
209                        EnumMemberKind::Method(m) if m.name == word => {
210                            let d = Declaration::Method {
211                                method: m,
212                                container: Container::Enum,
213                                member_span: member.span,
214                            };
215                            if accept(&d) {
216                                return Some(d);
217                            }
218                        }
219                        EnumMemberKind::ClassConst(cc) if cc.name == word => {
220                            let d = Declaration::ClassConst {
221                                konst: cc,
222                                container: Container::Enum,
223                                member_span: member.span,
224                            };
225                            if accept(&d) {
226                                return Some(d);
227                            }
228                        }
229                        _ => {}
230                    }
231                }
232            }
233            StmtKind::Namespace(ns) => {
234                if let NamespaceBody::Braced(inner) = &ns.body
235                    && let Some(d) = resolve_declaration(&inner.stmts, word, accept)
236                {
237                    return Some(d);
238                }
239            }
240            _ => {}
241        }
242    }
243    None
244}
245
246/// Scan class/interface/trait body members. Promoted-constructor parameters are
247/// only considered for `Container::Class` (where the originals handled them).
248fn resolve_member<'a>(
249    members: impl Iterator<Item = &'a php_ast::ClassMember<'a, 'a>>,
250    word: &str,
251    bare: &str,
252    container: Container,
253    accept: &dyn Fn(&Declaration<'a>) -> bool,
254) -> Option<Declaration<'a>> {
255    for member in members {
256        match &member.kind {
257            ClassMemberKind::Method(m) => {
258                if m.name == word {
259                    let d = Declaration::Method {
260                        method: m,
261                        container,
262                        member_span: member.span,
263                    };
264                    if accept(&d) {
265                        return Some(d);
266                    }
267                }
268                // Constructor-promoted parameters act as property declarations.
269                if container == Container::Class && m.name == "__construct" {
270                    for p in m.params.iter() {
271                        if p.visibility.is_some() && p.name == bare {
272                            let d = Declaration::PromotedParam { param: p };
273                            if accept(&d) {
274                                return Some(d);
275                            }
276                        }
277                    }
278                }
279            }
280            ClassMemberKind::ClassConst(cc) if cc.name == word => {
281                let d = Declaration::ClassConst {
282                    konst: cc,
283                    container,
284                    member_span: member.span,
285                };
286                if accept(&d) {
287                    return Some(d);
288                }
289            }
290            ClassMemberKind::Property(p) if p.name == bare => {
291                let d = Declaration::Property {
292                    property: p,
293                    container,
294                    member_span: member.span,
295                };
296                if accept(&d) {
297                    return Some(d);
298                }
299            }
300            _ => {}
301        }
302    }
303    None
304}