Skip to main content

php_lsp/
definition.rs

1use std::sync::Arc;
2
3use php_ast::Stmt;
4use tower_lsp::lsp_types::{Location, Position, Range, Url};
5
6use crate::ast::{ParsedDoc, SourceView};
7use crate::resolve::{Container, Declaration, resolve_declaration};
8use crate::util::{strip_variable_sigil, word_at_position, zero_width_location};
9use crate::walk::collect_var_refs_in_scope;
10
11/// Find the definition of the symbol under `position`.
12/// Searches the current document first, then `other_docs` for cross-file resolution.
13pub fn goto_definition(
14    uri: &Url,
15    source: &str,
16    doc: &ParsedDoc,
17    other_docs: &[(Url, Arc<ParsedDoc>)],
18    position: Position,
19) -> Option<Location> {
20    let word = word_at_position(source, position)?;
21
22    // For $variable, find the first occurrence in scope (= the definition/assignment).
23    let sv = doc.view();
24    if word.starts_with('$') {
25        let bare = word.trim_start_matches('$');
26        let byte_off = sv.byte_of_position(position) as usize;
27        let mut spans = Vec::new();
28        collect_var_refs_in_scope(&doc.program().stmts, bare, byte_off, &mut spans);
29        if let Some((span, _)) = spans.into_iter().min_by_key(|(s, _)| s.start) {
30            return Some(Location {
31                uri: uri.clone(),
32                range: Range {
33                    start: sv.position_of(span.start),
34                    end: sv.position_of(span.end),
35                },
36            });
37        }
38    }
39
40    if let Some(range) = resolve_declaration_range(sv, &doc.program().stmts, &word) {
41        return Some(Location {
42            uri: uri.clone(),
43            range,
44        });
45    }
46
47    for (other_uri, other_doc) in other_docs {
48        let other_sv = other_doc.view();
49        if let Some(range) = resolve_declaration_range(other_sv, &other_doc.program().stmts, &word)
50        {
51            return Some(Location {
52                uri: other_uri.clone(),
53                range,
54            });
55        }
56    }
57
58    None
59}
60
61/// Search an AST for a declaration named `name`, returning its selection range.
62/// Used by the PSR-4 fallback in the backend after resolving a class to a file.
63pub fn find_declaration_range(_source: &str, doc: &ParsedDoc, name: &str) -> Option<Range> {
64    let sv = doc.view();
65    resolve_declaration_range(sv, &doc.program().stmts, name)
66}
67
68/// Resolve `word` to a declaration in `stmts` and return its precise name range.
69fn resolve_declaration_range(
70    sv: SourceView<'_>,
71    stmts: &[Stmt<'_, '_>],
72    word: &str,
73) -> Option<Range> {
74    // Definition resolves every declaration kind *except* enum constants
75    // (which the original walker never matched).
76    let decl = resolve_declaration(stmts, word, &|d| {
77        !matches!(
78            d,
79            Declaration::ClassConst {
80                container: Container::Enum,
81                ..
82            }
83        )
84    })?;
85    Some(declaration_name_range(sv, &decl))
86}
87
88/// Compute the name range for a resolved declaration.
89///
90/// Top-level declarations and class-body members use `name_range_in_span`
91/// (searches within the node's span, so a name repeated in an earlier docblock
92/// doesn't steal the match); interface/trait/enum members use the cheaper
93/// whole-source `name_range`. This mirrors the original per-arm choice exactly.
94fn declaration_name_range(sv: SourceView<'_>, decl: &Declaration<'_>) -> Range {
95    match decl {
96        Declaration::Function { decl, stmt_span } => {
97            sv.name_range_in_span(decl.name.or_error(), *stmt_span)
98        }
99        Declaration::Class {
100            name, stmt_span, ..
101        } => sv.name_range_in_span(name.or_error(), *stmt_span),
102        Declaration::Interface { decl, stmt_span } => {
103            sv.name_range_in_span(decl.name.or_error(), *stmt_span)
104        }
105        Declaration::Trait { decl, stmt_span } => {
106            sv.name_range_in_span(decl.name.or_error(), *stmt_span)
107        }
108        Declaration::Enum { decl, stmt_span } => {
109            sv.name_range_in_span(decl.name.or_error(), *stmt_span)
110        }
111        Declaration::Method {
112            method,
113            container: Container::Class,
114            member_span,
115        } => sv.name_range_in_span(method.name.or_error(), *member_span),
116        Declaration::ClassConst {
117            konst,
118            container: Container::Class,
119            member_span,
120        } => sv.name_range_in_span(konst.name.or_error(), *member_span),
121        Declaration::Property {
122            property,
123            container: Container::Class,
124            member_span,
125        } => sv.name_range_in_span(property.name.or_error(), *member_span),
126        Declaration::PromotedParam { param } => {
127            sv.name_range_in_span(param.name.or_error(), param.span)
128        }
129        Declaration::Method { method, .. } => sv.name_range(method.name.or_error()),
130        Declaration::ClassConst { konst, .. } => sv.name_range(konst.name.or_error()),
131        Declaration::Property { property, .. } => sv.name_range(property.name.or_error()),
132        Declaration::EnumCase { case, .. } => sv.name_range(case.name.or_error()),
133    }
134}
135
136/// Find a class/function declaration by name in a slice of `FileIndex` entries.
137/// Returns the URI and a line-level `Range`.
138pub fn find_declaration_in_indexes(
139    name: &str,
140    indexes: &[(
141        tower_lsp::lsp_types::Url,
142        std::sync::Arc<crate::file_index::FileIndex>,
143    )],
144) -> Option<Location> {
145    let bare = strip_variable_sigil(name);
146    for (uri, idx) in indexes {
147        // Check top-level functions.
148        for f in &idx.functions {
149            if f.name.as_ref() == bare || f.name.as_ref() == name {
150                return Some(zero_width_location(uri, f.start_line));
151            }
152        }
153        // Check classes / interfaces / traits / enums and their members.
154        for cls in &idx.classes {
155            if cls.name.as_ref() == bare || cls.name.as_ref() == name {
156                return Some(zero_width_location(uri, cls.start_line));
157            }
158            // Methods.
159            for m in &cls.methods {
160                if m.name.as_ref() == name {
161                    return Some(zero_width_location(uri, m.start_line));
162                }
163            }
164            // Properties (stored without `$`).
165            for p in &cls.properties {
166                if p.name.as_ref() == bare {
167                    return Some(zero_width_location(uri, p.start_line));
168                }
169            }
170            // Class constants.
171            for cc in &cls.constants {
172                if cc.as_ref() == name {
173                    return Some(zero_width_location(uri, cls.start_line));
174                }
175            }
176            // Enum cases.
177            for case in &cls.cases {
178                if case.as_ref() == name {
179                    return Some(zero_width_location(uri, cls.start_line));
180                }
181            }
182        }
183    }
184    None
185}
186
187/// Walk the class hierarchy (extends + traits) in the workspace index to find
188/// `method_name` defined in `class_name` or any of its superclasses/traits.
189///
190/// Returns the first match in PHP's resolution order: class itself → traits →
191/// parent → parent's traits, etc. Uses `indexes` so no disk I/O is needed.
192pub fn find_method_in_class_hierarchy(
193    class_name: &str,
194    method_name: &str,
195    indexes: &[(
196        tower_lsp::lsp_types::Url,
197        std::sync::Arc<crate::file_index::FileIndex>,
198    )],
199) -> Option<Location> {
200    let mut queue: Vec<String> = vec![class_name.to_owned()];
201    let mut visited = std::collections::HashSet::new();
202
203    while !queue.is_empty() {
204        let current = queue.remove(0);
205        if !visited.insert(current.clone()) {
206            continue;
207        }
208        for (uri, idx) in indexes {
209            for cls in &idx.classes {
210                if cls.name.as_ref() != current.as_str()
211                    && cls.fqn.as_ref().trim_start_matches('\\') != current.as_str()
212                {
213                    continue;
214                }
215                for m in &cls.methods {
216                    if m.name.as_ref() == method_name {
217                        return Some(zero_width_location(uri, m.start_line));
218                    }
219                }
220                // Traits first (PHP MRO), then parent.
221                for trt in &cls.traits {
222                    queue.push(trt.as_ref().to_owned());
223                }
224                if let Some(parent) = &cls.parent {
225                    queue.push(parent.as_ref().to_owned());
226                }
227            }
228        }
229    }
230    None
231}
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236
237    // ── find_method_in_class_hierarchy ───────────────────────────────────────
238
239    fn make_index(path: &str, src: &str) -> (Url, std::sync::Arc<crate::file_index::FileIndex>) {
240        use crate::file_index::FileIndex;
241        let u = Url::parse(&format!("file://{path}")).unwrap();
242        let d = ParsedDoc::parse(src.to_string());
243        (u, std::sync::Arc::new(FileIndex::extract(&d)))
244    }
245
246    #[test]
247    fn hierarchy_finds_method_in_class_itself() {
248        let (uri, idx) = make_index(
249            "/a.php",
250            "<?php\nclass Foo { public function bar(): void {} }",
251        );
252        let indexes = vec![(uri, idx)];
253        let loc = find_method_in_class_hierarchy("Foo", "bar", &indexes);
254        assert!(loc.is_some(), "expected bar() in Foo");
255        assert_eq!(loc.unwrap().range.start.line, 1);
256    }
257
258    #[test]
259    fn hierarchy_finds_method_in_parent() {
260        let (base_uri, base_idx) = make_index(
261            "/Base.php",
262            "<?php\nclass Base { public function render(): void {} }",
263        );
264        let (cu, ci) = make_index("/Child.php", "<?php\nclass Child extends Base {}");
265        let indexes = vec![(base_uri.clone(), base_idx), (cu, ci)];
266        let loc = find_method_in_class_hierarchy("Child", "render", &indexes);
267        assert!(loc.is_some(), "expected render() found via parent Base");
268        assert_eq!(loc.unwrap().uri, base_uri);
269    }
270
271    #[test]
272    fn hierarchy_finds_method_in_trait() {
273        let (trait_uri, trait_idx) = make_index(
274            "/Renderable.php",
275            "<?php\ntrait Renderable { public function render(): void {} }",
276        );
277        let (pu, pi) = make_index("/Page.php", "<?php\nclass Page { use Renderable; }");
278        let indexes = vec![(trait_uri.clone(), trait_idx), (pu, pi)];
279        let loc = find_method_in_class_hierarchy("Page", "render", &indexes);
280        assert!(loc.is_some(), "expected render() found via trait");
281        assert_eq!(loc.unwrap().uri, trait_uri);
282    }
283
284    #[test]
285    fn hierarchy_returns_none_for_missing_method() {
286        let (uri, idx) = make_index("/Foo.php", "<?php\nclass Foo {}");
287        let indexes = vec![(uri, idx)];
288        assert!(find_method_in_class_hierarchy("Foo", "missing", &indexes).is_none());
289    }
290
291    #[test]
292    fn hierarchy_handles_cycle_without_panic() {
293        // Bogus source where A extends B extends A — must not loop forever.
294        let (ua, ia) = make_index("/A.php", "<?php\nclass A extends B {}");
295        let (ub, ib) = make_index("/B.php", "<?php\nclass B extends A {}");
296        let indexes = vec![(ua, ia), (ub, ib)];
297        let loc = find_method_in_class_hierarchy("A", "missing", &indexes);
298        assert!(loc.is_none());
299    }
300}