1use std::sync::Arc;
2
3use php_ast::{ClassMemberKind, EnumMemberKind, NamespaceBody, Stmt, StmtKind};
4use tower_lsp::lsp_types::{Location, Position, Range, Url};
5
6use crate::ast::{ParsedDoc, SourceView, str_offset};
7use crate::util::{strip_variable_sigil, utf16_code_units, word_at_position};
8use crate::walk::collect_var_refs_in_scope;
9
10fn zero_width_location(uri: &Url, line: u32) -> Location {
11 let pos = Position { line, character: 0 };
12 Location {
13 uri: uri.clone(),
14 range: Range {
15 start: pos,
16 end: pos,
17 },
18 }
19}
20
21pub fn goto_definition(
24 uri: &Url,
25 source: &str,
26 doc: &ParsedDoc,
27 other_docs: &[(Url, Arc<ParsedDoc>)],
28 position: Position,
29) -> Option<Location> {
30 let word = word_at_position(source, position)?;
31
32 let sv = doc.view();
34 if word.starts_with('$') {
35 let bare = word.trim_start_matches('$');
36 let byte_off = sv.byte_of_position(position) as usize;
37 let mut spans = Vec::new();
38 collect_var_refs_in_scope(&doc.program().stmts, bare, byte_off, &mut spans);
39 if let Some((span, _)) = spans.into_iter().min_by_key(|(s, _)| s.start) {
40 return Some(Location {
41 uri: uri.clone(),
42 range: Range {
43 start: sv.position_of(span.start),
44 end: sv.position_of(span.end),
45 },
46 });
47 }
48 }
49
50 if let Some(range) = scan_statements(sv, &doc.program().stmts, &word) {
51 return Some(Location {
52 uri: uri.clone(),
53 range,
54 });
55 }
56
57 for (other_uri, other_doc) in other_docs {
58 let other_sv = other_doc.view();
59 if let Some(range) = scan_statements(other_sv, &other_doc.program().stmts, &word) {
60 return Some(Location {
61 uri: other_uri.clone(),
62 range,
63 });
64 }
65 }
66
67 None
68}
69
70pub fn find_declaration_range(_source: &str, doc: &ParsedDoc, name: &str) -> Option<Range> {
73 let sv = doc.view();
74 scan_statements(sv, &doc.program().stmts, name)
75}
76
77fn scan_statements(sv: SourceView<'_>, stmts: &[Stmt<'_, '_>], word: &str) -> Option<Range> {
78 let bare = strip_variable_sigil(word);
80 for stmt in stmts {
81 match &stmt.kind {
82 StmtKind::Function(f) if f.name == word => {
83 return Some(sv.name_range(&f.name.to_string()));
84 }
85 StmtKind::Class(c)
86 if c.name.as_ref().map(|n| n.to_string()) == Some(word.to_string()) =>
87 {
88 let name = c.name.expect("match guard ensures Some");
89 return Some(sv.name_range(&name.to_string()));
90 }
91 StmtKind::Class(c) => {
92 for member in c.body.members.iter() {
93 match &member.kind {
94 ClassMemberKind::Method(m) if m.name == word => {
95 return Some(sv.name_range_in_span(&m.name.to_string(), member.span));
96 }
97 ClassMemberKind::ClassConst(cc) if cc.name == word => {
98 return Some(sv.name_range_in_span(&cc.name.to_string(), member.span));
99 }
100 ClassMemberKind::Property(p) if p.name == bare => {
101 return Some(sv.name_range_in_span(&p.name.to_string(), member.span));
102 }
103 ClassMemberKind::Method(m) if m.name == "__construct" => {
105 for p in m.params.iter() {
106 if p.visibility.is_some() && p.name == bare {
107 return Some(
108 sv.name_range_in_span(&p.name.to_string(), p.span),
109 );
110 }
111 }
112 }
113 _ => {}
114 }
115 }
116 }
117 StmtKind::Interface(i) => {
118 if i.name == word {
119 return Some(sv.name_range(&i.name.to_string()));
120 }
121 for member in i.body.members.iter() {
122 match &member.kind {
123 ClassMemberKind::Method(m) if m.name == word => {
124 return Some(sv.name_range(&m.name.to_string()));
125 }
126 ClassMemberKind::ClassConst(cc) if cc.name == word => {
127 return Some(sv.name_range(&cc.name.to_string()));
128 }
129 _ => {}
130 }
131 }
132 }
133 StmtKind::Trait(t) => {
134 if t.name == word {
135 return Some(sv.name_range(&t.name.to_string()));
136 }
137 for member in t.body.members.iter() {
138 match &member.kind {
139 ClassMemberKind::Method(m) if m.name == word => {
140 return Some(sv.name_range(&m.name.to_string()));
141 }
142 ClassMemberKind::ClassConst(cc) if cc.name == word => {
143 return Some(sv.name_range(&cc.name.to_string()));
144 }
145 ClassMemberKind::Property(p) if p.name == bare => {
146 return Some(sv.name_range(&p.name.to_string()));
147 }
148 _ => {}
149 }
150 }
151 }
152 StmtKind::Enum(e) if e.name == word => {
153 return Some(sv.name_range(&e.name.to_string()));
154 }
155 StmtKind::Enum(e) => {
156 for member in e.body.members.iter() {
157 match &member.kind {
158 EnumMemberKind::Method(m) if m.name == word => {
159 return Some(sv.name_range(&m.name.to_string()));
160 }
161 EnumMemberKind::Case(c) if c.name == word => {
162 return Some(sv.name_range(&c.name.to_string()));
163 }
164 _ => {}
165 }
166 }
167 }
168 StmtKind::Namespace(ns) => {
169 if let NamespaceBody::Braced(inner) = &ns.body
170 && let Some(range) = scan_statements(sv, &inner.stmts, word)
171 {
172 return Some(range);
173 }
174 }
175 _ => {}
176 }
177 }
178 None
179}
180
181pub fn find_in_indexes(
184 name: &str,
185 indexes: &[(
186 tower_lsp::lsp_types::Url,
187 std::sync::Arc<crate::file_index::FileIndex>,
188 )],
189) -> Option<Location> {
190 let bare = strip_variable_sigil(name);
191 for (uri, idx) in indexes {
192 for f in &idx.functions {
194 if f.name.as_ref() == bare || f.name.as_ref() == name {
195 return Some(zero_width_location(uri, f.start_line));
196 }
197 }
198 for cls in &idx.classes {
200 if cls.name.as_ref() == bare || cls.name.as_ref() == name {
201 return Some(zero_width_location(uri, cls.start_line));
202 }
203 for m in &cls.methods {
205 if m.name.as_ref() == name {
206 return Some(zero_width_location(uri, m.start_line));
207 }
208 }
209 for p in &cls.properties {
211 if p.name.as_ref() == bare {
212 return Some(zero_width_location(uri, p.start_line));
213 }
214 }
215 for cc in &cls.constants {
217 if cc.as_ref() == name {
218 let pos = tower_lsp::lsp_types::Position {
219 line: cls.start_line,
220 character: 0,
221 };
222 return Some(Location {
223 uri: uri.clone(),
224 range: Range {
225 start: pos,
226 end: pos,
227 },
228 });
229 }
230 }
231 for case in &cls.cases {
233 if case.as_ref() == name {
234 let pos = tower_lsp::lsp_types::Position {
235 line: cls.start_line,
236 character: 0,
237 };
238 return Some(Location {
239 uri: uri.clone(),
240 range: Range {
241 start: pos,
242 end: pos,
243 },
244 });
245 }
246 }
247 }
248 }
249 None
250}
251
252pub fn find_method_in_class_hierarchy(
258 class_name: &str,
259 method_name: &str,
260 indexes: &[(
261 tower_lsp::lsp_types::Url,
262 std::sync::Arc<crate::file_index::FileIndex>,
263 )],
264) -> Option<Location> {
265 let mut queue: Vec<String> = vec![class_name.to_owned()];
266 let mut visited = std::collections::HashSet::new();
267
268 while !queue.is_empty() {
269 let current = queue.remove(0);
270 if !visited.insert(current.clone()) {
271 continue;
272 }
273 for (uri, idx) in indexes {
274 for cls in &idx.classes {
275 if cls.name.as_ref() != current.as_str()
276 && cls.fqn.as_ref().trim_start_matches('\\') != current.as_str()
277 {
278 continue;
279 }
280 for m in &cls.methods {
281 if m.name.as_ref() == method_name {
282 let pos = tower_lsp::lsp_types::Position {
283 line: m.start_line,
284 character: 0,
285 };
286 return Some(Location {
287 uri: uri.clone(),
288 range: Range {
289 start: pos,
290 end: pos,
291 },
292 });
293 }
294 }
295 for trt in &cls.traits {
297 queue.push(trt.as_ref().to_owned());
298 }
299 if let Some(parent) = &cls.parent {
300 queue.push(parent.as_ref().to_owned());
301 }
302 }
303 }
304 }
305 None
306}
307
308fn _name_range_from_offset(sv: SourceView<'_>, name: &str) -> Range {
309 let start_offset = str_offset(sv.source(), name).unwrap_or(0);
310 let start = sv.position_of(start_offset);
311 Range {
312 start,
313 end: Position {
314 line: start.line,
315 character: start.character + utf16_code_units(name),
316 },
317 }
318}
319
320#[cfg(test)]
321mod tests {
322 use super::*;
323
324 fn make_index(path: &str, src: &str) -> (Url, std::sync::Arc<crate::file_index::FileIndex>) {
327 use crate::file_index::FileIndex;
328 let u = Url::parse(&format!("file://{path}")).unwrap();
329 let d = ParsedDoc::parse(src.to_string());
330 (u, std::sync::Arc::new(FileIndex::extract(&d)))
331 }
332
333 #[test]
334 fn hierarchy_finds_method_in_class_itself() {
335 let (uri, idx) = make_index(
336 "/a.php",
337 "<?php\nclass Foo { public function bar(): void {} }",
338 );
339 let indexes = vec![(uri, idx)];
340 let loc = find_method_in_class_hierarchy("Foo", "bar", &indexes);
341 assert!(loc.is_some(), "expected bar() in Foo");
342 assert_eq!(loc.unwrap().range.start.line, 1);
343 }
344
345 #[test]
346 fn hierarchy_finds_method_in_parent() {
347 let (base_uri, base_idx) = make_index(
348 "/Base.php",
349 "<?php\nclass Base { public function render(): void {} }",
350 );
351 let (cu, ci) = make_index("/Child.php", "<?php\nclass Child extends Base {}");
352 let indexes = vec![(base_uri.clone(), base_idx), (cu, ci)];
353 let loc = find_method_in_class_hierarchy("Child", "render", &indexes);
354 assert!(loc.is_some(), "expected render() found via parent Base");
355 assert_eq!(loc.unwrap().uri, base_uri);
356 }
357
358 #[test]
359 fn hierarchy_finds_method_in_trait() {
360 let (trait_uri, trait_idx) = make_index(
361 "/Renderable.php",
362 "<?php\ntrait Renderable { public function render(): void {} }",
363 );
364 let (pu, pi) = make_index("/Page.php", "<?php\nclass Page { use Renderable; }");
365 let indexes = vec![(trait_uri.clone(), trait_idx), (pu, pi)];
366 let loc = find_method_in_class_hierarchy("Page", "render", &indexes);
367 assert!(loc.is_some(), "expected render() found via trait");
368 assert_eq!(loc.unwrap().uri, trait_uri);
369 }
370
371 #[test]
372 fn hierarchy_returns_none_for_missing_method() {
373 let (uri, idx) = make_index("/Foo.php", "<?php\nclass Foo {}");
374 let indexes = vec![(uri, idx)];
375 assert!(find_method_in_class_hierarchy("Foo", "missing", &indexes).is_none());
376 }
377
378 #[test]
379 fn hierarchy_handles_cycle_without_panic() {
380 let (ua, ia) = make_index("/A.php", "<?php\nclass A extends B {}");
382 let (ub, ib) = make_index("/B.php", "<?php\nclass B extends A {}");
383 let indexes = vec![(ua, ia), (ub, ib)];
384 let loc = find_method_in_class_hierarchy("A", "missing", &indexes);
385 assert!(loc.is_none());
386 }
387}