1use std::sync::Arc;
4
5use php_ast::{NamespaceBody, Stmt, StmtKind};
6use tower_lsp::lsp_types::{Location, Url};
7
8use crate::ast::{ParsedDoc, SourceView};
9
10#[inline]
22fn name_matches(repr: &str, word: &str, fqn: Option<&str>) -> bool {
23 repr == word || fqn.is_some_and(|f| repr.trim_start_matches('\\') == f)
24}
25
26pub fn find_implementations(
34 word: &str,
35 fqn: Option<&str>,
36 all_docs: &[(Url, Arc<ParsedDoc>)],
37) -> Vec<Location> {
38 let mut locations = Vec::new();
39 for (uri, doc) in all_docs {
40 let sv = doc.view();
41 collect_implementations(&doc.program().stmts, word, fqn, sv, uri, &mut locations);
42 }
43 locations
44}
45
46pub fn find_implementations_from_workspace(
53 word: &str,
54 fqn: Option<&str>,
55 wi: &crate::db::workspace_index::WorkspaceIndexData,
56) -> Vec<Location> {
57 let mut locations = Vec::new();
58 let mut push_refs = |key: &str| {
59 if let Some(refs) = wi.subtypes_of.get(key) {
60 for r in refs {
61 if let Some((uri, cls)) = wi.at(*r) {
62 let extends_match = cls
65 .parent
66 .as_deref()
67 .map(|p| name_matches(p, word, fqn))
68 .unwrap_or(false);
69 let implements_match = cls
70 .implements
71 .iter()
72 .any(|iface| name_matches(iface.as_ref(), word, fqn));
73 if extends_match || implements_match {
74 let pos = tower_lsp::lsp_types::Position {
75 line: cls.start_line,
76 character: 0,
77 };
78 locations.push(Location {
79 uri: uri.clone(),
80 range: tower_lsp::lsp_types::Range {
81 start: pos,
82 end: pos,
83 },
84 });
85 }
86 }
87 }
88 }
89 };
90 push_refs(word);
91 if let Some(f) = fqn
92 && f != word
93 {
94 push_refs(f);
95 let trimmed = f.trim_start_matches('\\');
97 if trimmed != f {
98 push_refs(trimmed);
99 }
100 }
101 locations.sort_by(|a, b| {
104 a.uri
105 .as_str()
106 .cmp(b.uri.as_str())
107 .then(a.range.start.line.cmp(&b.range.start.line))
108 });
109 locations.dedup_by(|a, b| a.uri == b.uri && a.range.start.line == b.range.start.line);
110 locations
111}
112
113fn collect_implementations(
114 stmts: &[Stmt<'_, '_>],
115 word: &str,
116 fqn: Option<&str>,
117 sv: SourceView<'_>,
118 uri: &Url,
119 out: &mut Vec<Location>,
120) {
121 for stmt in stmts {
122 match &stmt.kind {
123 StmtKind::Class(c) => {
124 let extends_match = c
125 .extends
126 .as_ref()
127 .map(|e| name_matches(e.to_string_repr().as_ref(), word, fqn))
128 .unwrap_or(false);
129
130 let implements_match = c
131 .implements
132 .iter()
133 .any(|iface| name_matches(iface.to_string_repr().as_ref(), word, fqn));
134
135 if (extends_match || implements_match)
136 && let Some(class_name) = c.name
137 {
138 out.push(Location {
139 uri: uri.clone(),
140 range: sv.name_range(class_name),
141 });
142 }
143 }
144 StmtKind::Enum(e) => {
145 let implements_match = e
146 .implements
147 .iter()
148 .any(|iface| name_matches(iface.to_string_repr().as_ref(), word, fqn));
149 if implements_match {
150 out.push(Location {
151 uri: uri.clone(),
152 range: sv.name_range(e.name),
153 });
154 }
155 }
156 StmtKind::Namespace(ns) => {
157 if let NamespaceBody::Braced(inner) = &ns.body {
158 collect_implementations(inner, word, fqn, sv, uri, out);
159 }
160 }
161 _ => {}
162 }
163 }
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169
170 fn uri(path: &str) -> Url {
171 Url::parse(&format!("file://{path}")).unwrap()
172 }
173
174 fn doc(path: &str, source: &str) -> (Url, Arc<ParsedDoc>) {
175 (uri(path), Arc::new(ParsedDoc::parse(source.to_string())))
176 }
177
178 #[test]
181 fn finds_class_implementing_interface() {
182 let src = "<?php\ninterface Countable {}\nclass MyList implements Countable {}";
183 let docs = vec![doc("/a.php", src)];
184 let locs = find_implementations("Countable", None, &docs);
185 assert_eq!(locs.len(), 1);
186 assert_eq!(locs[0].range.start.line, 2);
187 }
188
189 #[test]
190 fn finds_class_extending_parent() {
191 let src = "<?php\nclass Animal {}\nclass Dog extends Animal {}";
192 let docs = vec![doc("/a.php", src)];
193 let locs = find_implementations("Animal", None, &docs);
194 assert_eq!(locs.len(), 1);
195 }
196
197 #[test]
198 fn no_implementations_for_unknown_name() {
199 let src = "<?php\nclass Foo {}";
200 let docs = vec![doc("/a.php", src)];
201 let locs = find_implementations("Bar", None, &docs);
202 assert!(locs.is_empty());
203 }
204
205 #[test]
206 fn finds_across_multiple_docs() {
207 let a = doc("/a.php", "<?php\nclass DogA extends Animal {}");
208 let b = doc("/b.php", "<?php\nclass DogB extends Animal {}");
209 let locs = find_implementations("Animal", None, &[a, b]);
210 assert_eq!(locs.len(), 2);
211 }
212
213 #[test]
214 fn class_implementing_multiple_interfaces() {
215 let src = "<?php\nclass Repo implements Countable, Serializable {}";
216 let docs = vec![doc("/a.php", src)];
217 let countable = find_implementations("Countable", None, &docs);
218 let serializable = find_implementations("Serializable", None, &docs);
219 assert_eq!(countable.len(), 1);
220 assert_eq!(serializable.len(), 1);
221 }
222
223 #[test]
224 fn enum_implementing_interface_is_found() {
225 let src = "<?php\ninterface HasLabel {}\nenum Status: string implements HasLabel {\n case Active = 'active';\n}";
227 let docs = vec![doc("/a.php", src)];
228 let locs = find_implementations("HasLabel", None, &docs);
229 assert_eq!(
230 locs.len(),
231 1,
232 "expected enum Status as implementation of HasLabel, got: {:?}",
233 locs
234 );
235 assert_eq!(
236 locs[0].range.start.line, 2,
237 "enum declaration should be on line 2"
238 );
239 }
240
241 #[test]
242 fn multiple_classes_in_same_doc_all_found() {
243 let src = "<?php\nclass Base {}\nclass A extends Base {}\nclass B extends Base {}\nclass C extends Base {}";
245 let docs = vec![doc("/a.php", src)];
246 let locs = find_implementations("Base", None, &docs);
247 assert_eq!(locs.len(), 3);
248 let names: Vec<u32> = locs.iter().map(|l| l.range.start.line).collect();
249 assert!(names.contains(&2));
250 assert!(names.contains(&3));
251 assert!(names.contains(&4));
252 }
253
254 #[test]
255 fn class_that_extends_and_implements_produces_one_location() {
256 let src = "<?php\nclass Child extends Parent implements Iface {}";
260 let docs = vec![doc("/a.php", src)];
261 assert_eq!(find_implementations("Parent", None, &docs).len(), 1);
262 assert_eq!(find_implementations("Iface", None, &docs).len(), 1);
263 }
264
265 #[test]
266 fn partial_name_match_is_not_returned() {
267 let src = "<?php\nclass AnimalHouse extends Creature {}";
269 let docs = vec![doc("/a.php", src)];
270 let locs = find_implementations("Animal", None, &docs);
271 assert!(
272 locs.is_empty(),
273 "partial name 'Animal' must not match 'AnimalHouse extends Creature'"
274 );
275 }
276
277 #[test]
278 fn empty_docs_returns_empty() {
279 let locs = find_implementations("Animal", None, &[]);
280 assert!(locs.is_empty());
281 }
282
283 #[test]
284 fn braced_namespace_class_is_found() {
285 let src = "<?php\nnamespace App {\n class Dog extends Animal {}\n}";
287 let docs = vec![doc("/a.php", src)];
288 let locs = find_implementations("Animal", None, &docs);
289 assert_eq!(
290 locs.len(),
291 1,
292 "expected Dog inside braced namespace, got: {locs:?}"
293 );
294 assert_eq!(locs[0].range.start.line, 2);
295 }
296
297 #[test]
298 fn unbraced_namespace_class_is_found() {
299 let src = "<?php\nnamespace App;\nclass Dog extends Animal {}";
302 let docs = vec![doc("/a.php", src)];
303 let locs = find_implementations("Animal", None, &docs);
304 assert_eq!(
305 locs.len(),
306 1,
307 "expected Dog inside unbraced namespace, got: {locs:?}"
308 );
309 assert_eq!(locs[0].range.start.line, 2);
310 }
311
312 #[test]
313 fn fully_qualified_extends_does_not_match_without_fqn_context() {
314 let src = "<?php\nclass Dog extends \\Animal {}";
318 let docs = vec![doc("/a.php", src)];
319 let locs = find_implementations("Animal", None, &docs);
320 assert!(
321 locs.is_empty(),
322 "without FQN context, '\\\\Animal' must not match bare 'Animal'"
323 );
324 }
325
326 #[test]
327 fn fqn_context_finds_fully_qualified_extends() {
328 let src = "<?php\nclass Dog extends \\App\\Animal {}";
330 let docs = vec![doc("/a.php", src)];
331 let locs = find_implementations("Animal", Some("App\\Animal"), &docs);
332 assert_eq!(
333 locs.len(),
334 1,
335 "FQN-aware search must find 'extends \\\\App\\\\Animal', got: {locs:?}"
336 );
337 }
338
339 #[test]
340 fn fqn_context_finds_qualified_extends_without_leading_backslash() {
341 let src = "<?php\nclass Dog extends App\\Animal {}";
343 let docs = vec![doc("/a.php", src)];
344 let locs = find_implementations("Animal", Some("App\\Animal"), &docs);
345 assert_eq!(
346 locs.len(),
347 1,
348 "FQN-aware search must find 'extends App\\\\Animal', got: {locs:?}"
349 );
350 }
351
352 #[test]
353 fn fqn_context_still_matches_short_name_form() {
354 let src = "<?php\nclass Dog extends Animal {}";
357 let docs = vec![doc("/a.php", src)];
358 let locs = find_implementations("Animal", Some("App\\Animal"), &docs);
359 assert_eq!(
360 locs.len(),
361 1,
362 "short-name form must still match when FQN is provided, got: {locs:?}"
363 );
364 }
365
366 #[test]
367 fn anonymous_class_does_not_cause_panic() {
368 let src = "<?php\n$x = new class extends Animal {};";
371 let docs = vec![doc("/a.php", src)];
372 let _ = find_implementations("Animal", None, &docs);
375 }
376
377 #[test]
378 fn location_uri_matches_source_doc() {
379 let a = doc("/src/Dog.php", "<?php\nclass Dog extends Animal {}");
380 let b = doc("/src/Cat.php", "<?php\nclass Cat extends Animal {}");
381 let locs = find_implementations("Animal", None, &[a, b]);
382 assert_eq!(locs.len(), 2);
383 let uris: Vec<&str> = locs.iter().map(|l| l.uri.path()).collect();
384 assert!(uris.contains(&"/src/Dog.php"));
385 assert!(uris.contains(&"/src/Cat.php"));
386 }
387
388 fn make_index(path: &str, src: &str) -> (Url, std::sync::Arc<crate::file_index::FileIndex>) {
395 use crate::file_index::FileIndex;
396 let u = uri(path);
397 let d = ParsedDoc::parse(src.to_string());
398 (u.clone(), std::sync::Arc::new(FileIndex::extract(&d)))
399 }
400
401 #[test]
402 fn from_workspace_finds_implementing_class() {
403 let (circle_uri, circle_idx) = make_index(
404 "/circle.php",
405 "<?php\nclass Circle implements Drawable {\n public function draw(): void {}\n}",
406 );
407 let wi = crate::db::workspace_index::WorkspaceIndexData::from_files(vec![(
408 circle_uri.clone(),
409 circle_idx,
410 )]);
411 let locs = find_implementations_from_workspace("Drawable", None, &wi);
412 assert_eq!(
413 locs.len(),
414 1,
415 "expected Circle as implementation of Drawable"
416 );
417 assert_eq!(locs[0].uri, circle_uri);
418 assert_eq!(locs[0].range.start.line, 1, "Circle is declared on line 1");
419 }
420
421 #[test]
422 fn from_workspace_finds_extending_class() {
423 let (dog_uri, dog_idx) = make_index("/dog.php", "<?php\nclass Dog extends Animal {}");
424 let wi =
425 crate::db::workspace_index::WorkspaceIndexData::from_files(vec![(dog_uri, dog_idx)]);
426 let locs = find_implementations_from_workspace("Animal", None, &wi);
427 assert_eq!(locs.len(), 1, "expected Dog as subclass of Animal");
428 assert_eq!(locs[0].range.start.line, 1);
429 }
430
431 #[test]
432 fn from_workspace_finds_across_multiple_files() {
433 let (a_uri, a_idx) = make_index("/a.php", "<?php\nclass Cat extends Animal {}");
434 let (b_uri, b_idx) = make_index("/b.php", "<?php\nclass Dog extends Animal {}");
435 let wi = crate::db::workspace_index::WorkspaceIndexData::from_files(vec![
436 (a_uri, a_idx),
437 (b_uri, b_idx),
438 ]);
439 let locs = find_implementations_from_workspace("Animal", None, &wi);
440 assert_eq!(locs.len(), 2, "expected both Cat and Dog");
441 }
442}