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
11pub 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 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
61pub 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
68fn resolve_declaration_range(
70 sv: SourceView<'_>,
71 stmts: &[Stmt<'_, '_>],
72 word: &str,
73) -> Option<Range> {
74 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
88fn 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
136pub 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 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 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 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 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 for cc in &cls.constants {
172 if cc.as_ref() == name {
173 return Some(zero_width_location(uri, cls.start_line));
174 }
175 }
176 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
187pub 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 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 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 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}