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}