1use std::collections::HashSet;
2use std::sync::Arc;
3
4use php_ast::{ClassMemberKind, EnumMemberKind, NamespaceBody, Span, Stmt, StmtKind};
5use tower_lsp::lsp_types::{Location, Position, Range, Url};
6
7use crate::ast::str_offset;
8use crate::ast::{ParsedDoc, offset_to_position};
9use crate::walk::{
10 class_refs_in_stmts, function_refs_in_stmts, method_refs_in_stmts, refs_in_stmts,
11 refs_in_stmts_with_use,
12};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum SymbolKind {
19 Function,
21 Method,
23 Class,
25}
26
27pub fn find_references(
32 word: &str,
33 all_docs: &[(Url, Arc<ParsedDoc>)],
34 include_declaration: bool,
35 kind: Option<SymbolKind>,
36) -> Vec<Location> {
37 find_references_inner(word, all_docs, include_declaration, false, kind)
38}
39
40pub fn find_references_with_use(
44 word: &str,
45 all_docs: &[(Url, Arc<ParsedDoc>)],
46 include_declaration: bool,
47) -> Vec<Location> {
48 find_references_inner(word, all_docs, include_declaration, true, None)
49}
50
51fn find_references_inner(
52 word: &str,
53 all_docs: &[(Url, Arc<ParsedDoc>)],
54 include_declaration: bool,
55 include_use: bool,
56 kind: Option<SymbolKind>,
57) -> Vec<Location> {
58 let mut locations = Vec::new();
59
60 for (uri, doc) in all_docs {
61 let source = doc.source();
62 let stmts = &doc.program().stmts;
63 let mut spans = Vec::new();
64
65 if include_use {
66 refs_in_stmts_with_use(source, stmts, word, &mut spans);
68 if !include_declaration {
69 let mut decl_spans = Vec::new();
70 collect_declaration_spans(source, stmts, word, None, &mut decl_spans);
71 let decl_set: HashSet<(u32, u32)> =
72 decl_spans.iter().map(|s| (s.start, s.end)).collect();
73 spans.retain(|span| !decl_set.contains(&(span.start, span.end)));
74 }
75 } else {
76 match kind {
77 Some(SymbolKind::Function) => function_refs_in_stmts(stmts, word, &mut spans),
78 Some(SymbolKind::Method) => method_refs_in_stmts(stmts, word, &mut spans),
79 Some(SymbolKind::Class) => class_refs_in_stmts(stmts, word, &mut spans),
80 None => {
82 refs_in_stmts(source, stmts, word, &mut spans);
83 if !include_declaration {
84 let mut decl_spans = Vec::new();
85 collect_declaration_spans(source, stmts, word, None, &mut decl_spans);
86 let decl_set: HashSet<(u32, u32)> =
87 decl_spans.iter().map(|s| (s.start, s.end)).collect();
88 spans.retain(|span| !decl_set.contains(&(span.start, span.end)));
89 }
90 }
91 }
92 if include_declaration && kind.is_some() {
96 collect_declaration_spans(source, stmts, word, kind, &mut spans);
97 }
98 }
99
100 for span in spans {
101 let start = offset_to_position(source, span.start);
102 let end = Position {
103 line: start.line,
104 character: start.character
105 + word.chars().map(|c| c.len_utf16() as u32).sum::<u32>(),
106 };
107 locations.push(Location {
108 uri: uri.clone(),
109 range: Range { start, end },
110 });
111 }
112 }
113
114 locations
115}
116
117fn declaration_name_span(source: &str, name: &str) -> Span {
119 let start = str_offset(source, name);
120 Span {
121 start,
122 end: start + name.len() as u32,
123 }
124}
125
126fn collect_declaration_spans(
135 source: &str,
136 stmts: &[Stmt<'_, '_>],
137 word: &str,
138 kind: Option<SymbolKind>,
139 out: &mut Vec<Span>,
140) {
141 let want_free = matches!(kind, None | Some(SymbolKind::Function));
142 let want_method = matches!(kind, None | Some(SymbolKind::Method));
143 let want_type = matches!(kind, None | Some(SymbolKind::Class));
144
145 for stmt in stmts {
146 match &stmt.kind {
147 StmtKind::Function(f) => {
148 if want_free && f.name == word {
149 out.push(declaration_name_span(source, f.name));
150 }
151 }
152 StmtKind::Class(c) => {
153 if want_type
154 && let Some(name) = c.name
155 && name == word
156 {
157 out.push(declaration_name_span(source, name));
158 }
159 if want_method {
160 for member in c.members.iter() {
161 if let ClassMemberKind::Method(m) = &member.kind
162 && m.name == word
163 {
164 out.push(declaration_name_span(source, m.name));
165 }
166 }
167 }
168 }
169 StmtKind::Interface(i) => {
170 if want_type && i.name == word {
171 out.push(declaration_name_span(source, i.name));
172 }
173 if want_method {
174 for member in i.members.iter() {
175 if let ClassMemberKind::Method(m) = &member.kind
176 && m.name == word
177 {
178 out.push(declaration_name_span(source, m.name));
179 }
180 }
181 }
182 }
183 StmtKind::Trait(t) => {
184 if want_type && t.name == word {
185 out.push(declaration_name_span(source, t.name));
186 }
187 if want_method {
188 for member in t.members.iter() {
189 if let ClassMemberKind::Method(m) = &member.kind
190 && m.name == word
191 {
192 out.push(declaration_name_span(source, m.name));
193 }
194 }
195 }
196 }
197 StmtKind::Enum(e) => {
198 if want_type && e.name == word {
199 out.push(declaration_name_span(source, e.name));
200 }
201 for member in e.members.iter() {
202 match &member.kind {
203 EnumMemberKind::Method(m) if want_method && m.name == word => {
204 out.push(declaration_name_span(source, m.name));
205 }
206 EnumMemberKind::Case(c) if want_type && c.name == word => {
207 out.push(declaration_name_span(source, c.name));
208 }
209 _ => {}
210 }
211 }
212 }
213 StmtKind::Namespace(ns) => {
214 if let NamespaceBody::Braced(inner) = &ns.body {
215 collect_declaration_spans(source, inner, word, kind, out);
216 }
217 }
218 _ => {}
219 }
220 }
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226
227 fn uri(path: &str) -> Url {
228 Url::parse(&format!("file://{path}")).unwrap()
229 }
230
231 fn doc(path: &str, source: &str) -> (Url, Arc<ParsedDoc>) {
232 (uri(path), Arc::new(ParsedDoc::parse(source.to_string())))
233 }
234
235 #[test]
236 fn finds_function_call_reference() {
237 let src = "<?php\nfunction greet() {}\ngreet();\ngreet();";
238 let docs = vec![doc("/a.php", src)];
239 let refs = find_references("greet", &docs, false, None);
240 assert_eq!(refs.len(), 2, "expected 2 call-site refs, got {:?}", refs);
241 }
242
243 #[test]
244 fn include_declaration_adds_def_site() {
245 let src = "<?php\nfunction greet() {}\ngreet();";
246 let docs = vec![doc("/a.php", src)];
247 let with_decl = find_references("greet", &docs, true, None);
248 let without_decl = find_references("greet", &docs, false, None);
249 assert_eq!(
251 without_decl.len(),
252 1,
253 "expected 1 call-site ref without declaration"
254 );
255 assert_eq!(
256 without_decl[0].range.start.line, 2,
257 "call site should be on line 2"
258 );
259 assert_eq!(
261 with_decl.len(),
262 2,
263 "expected 2 refs with declaration included"
264 );
265 }
266
267 #[test]
268 fn finds_new_expression_reference() {
269 let src = "<?php\nclass Foo {}\n$x = new Foo();";
270 let docs = vec![doc("/a.php", src)];
271 let refs = find_references("Foo", &docs, false, None);
272 assert_eq!(
273 refs.len(),
274 1,
275 "expected exactly 1 reference to Foo in new expr"
276 );
277 assert_eq!(
278 refs[0].range.start.line, 2,
279 "new Foo() reference should be on line 2"
280 );
281 }
282
283 #[test]
284 fn finds_reference_in_nested_function_call() {
285 let src = "<?php\nfunction greet() {}\necho(greet());";
286 let docs = vec![doc("/a.php", src)];
287 let refs = find_references("greet", &docs, false, None);
288 assert_eq!(
289 refs.len(),
290 1,
291 "expected exactly 1 nested function call reference"
292 );
293 assert_eq!(
294 refs[0].range.start.line, 2,
295 "nested greet() call should be on line 2"
296 );
297 }
298
299 #[test]
300 fn finds_references_across_multiple_docs() {
301 let a = doc("/a.php", "<?php\nfunction helper() {}");
302 let b = doc("/b.php", "<?php\nhelper();\nhelper();");
303 let refs = find_references("helper", &[a, b], false, None);
304 assert_eq!(refs.len(), 2, "expected 2 cross-file references");
305 assert!(refs.iter().all(|r| r.uri.path().ends_with("/b.php")));
306 }
307
308 #[test]
309 fn finds_method_call_reference() {
310 let src = "<?php\nclass Calc { public function add() {} }\n$c = new Calc();\n$c->add();";
311 let docs = vec![doc("/a.php", src)];
312 let refs = find_references("add", &docs, false, None);
313 assert_eq!(
314 refs.len(),
315 1,
316 "expected exactly 1 method call reference to 'add'"
317 );
318 assert_eq!(
319 refs[0].range.start.line, 3,
320 "add() call should be on line 3"
321 );
322 }
323
324 #[test]
325 fn finds_reference_inside_if_body() {
326 let src = "<?php\nfunction check() {}\nif (true) { check(); }";
327 let docs = vec![doc("/a.php", src)];
328 let refs = find_references("check", &docs, false, None);
329 assert_eq!(refs.len(), 1, "expected exactly 1 reference inside if body");
330 assert_eq!(
331 refs[0].range.start.line, 2,
332 "check() inside if should be on line 2"
333 );
334 }
335
336 #[test]
337 fn finds_use_statement_reference() {
338 let src = "<?php\nuse MyClass;\n$x = new MyClass();";
341 let docs = vec![doc("/a.php", src)];
342 let refs = find_references_with_use("MyClass", &docs, false);
343 assert_eq!(
345 refs.len(),
346 2,
347 "expected exactly 2 references, got: {:?}",
348 refs
349 );
350 let mut lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
351 lines.sort_unstable();
352 assert_eq!(
353 lines,
354 vec![1, 2],
355 "references should be on lines 1 (use) and 2 (new)"
356 );
357 }
358
359 #[test]
360 fn find_references_returns_correct_lines() {
361 let src = "<?php\nhelper();\nhelper();\nfunction helper() {}";
363 let docs = vec![doc("/a.php", src)];
364 let refs = find_references("helper", &docs, false, None);
365 assert_eq!(refs.len(), 2, "expected exactly 2 call-site references");
366 let mut lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
367 lines.sort_unstable();
368 assert_eq!(lines, vec![1, 2], "references should be on lines 1 and 2");
369 }
370
371 #[test]
372 fn declaration_excluded_when_flag_false() {
373 let src = "<?php\nfunction doWork() {}\ndoWork();\ndoWork();";
375 let docs = vec![doc("/a.php", src)];
376 let refs = find_references("doWork", &docs, false, None);
377 let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
379 assert!(
380 !lines.contains(&1),
381 "declaration line (1) must not appear when include_declaration=false, got: {:?}",
382 lines
383 );
384 assert_eq!(refs.len(), 2, "expected 2 call-site references only");
385 }
386
387 #[test]
388 fn partial_match_not_included() {
389 let src = "<?php\nfunction greet() {}\nfunction greeting() {}\ngreet();\ngreeting();";
391 let docs = vec![doc("/a.php", src)];
392 let refs = find_references("greet", &docs, false, None);
393 for r in &refs {
395 let span_len = r.range.end.character - r.range.start.character;
398 assert_eq!(
399 span_len, 5,
400 "reference span length should equal len('greet')=5, got {} at {:?}",
401 span_len, r
402 );
403 }
404 assert_eq!(
406 refs.len(),
407 1,
408 "expected exactly 1 reference to 'greet' (not 'greeting'), got: {:?}",
409 refs
410 );
411 }
412
413 #[test]
414 fn finds_reference_in_class_property_default() {
415 let src = "<?php\nclass Foo {\n public string $status = Status::ACTIVE;\n}";
417 let docs = vec![doc("/a.php", src)];
418 let refs = find_references("Status", &docs, false, None);
419 assert_eq!(
420 refs.len(),
421 1,
422 "expected exactly 1 reference to Status in property default, got: {:?}",
423 refs
424 );
425 assert_eq!(refs[0].range.start.line, 2, "reference should be on line 2");
426 }
427
428 #[test]
429 fn finds_reference_inside_enum_method_body() {
430 let src = "<?php\nfunction helper() {}\nenum Status {\n public function label(): string { return helper(); }\n}";
432 let docs = vec![doc("/a.php", src)];
433 let refs = find_references("helper", &docs, false, None);
434 assert_eq!(
435 refs.len(),
436 1,
437 "expected exactly 1 reference to helper() inside enum method, got: {:?}",
438 refs
439 );
440 assert_eq!(refs[0].range.start.line, 3, "reference should be on line 3");
441 }
442
443 #[test]
444 fn finds_reference_in_for_init_and_update() {
445 let src = "<?php\nfunction tick() {}\nfor (tick(); $i < 10; tick()) {}";
447 let docs = vec![doc("/a.php", src)];
448 let refs = find_references("tick", &docs, false, None);
449 assert_eq!(
450 refs.len(),
451 2,
452 "expected exactly 2 references to tick() (init + update), got: {:?}",
453 refs
454 );
455 assert!(refs.iter().all(|r| r.range.start.line == 2));
457 }
458
459 #[test]
462 fn function_kind_skips_method_call_with_same_name() {
463 let src = "<?php\nfunction get() {}\nget();\n$obj->get();";
465 let docs = vec![doc("/a.php", src)];
466 let refs = find_references("get", &docs, false, Some(SymbolKind::Function));
467 assert_eq!(
469 refs.len(),
470 1,
471 "expected 1 free-function ref, got: {:?}",
472 refs
473 );
474 assert_eq!(refs[0].range.start.line, 2);
475 }
476
477 #[test]
478 fn method_kind_skips_free_function_call_with_same_name() {
479 let src = "<?php\nfunction add() {}\nadd();\n$calc->add();";
481 let docs = vec![doc("/a.php", src)];
482 let refs = find_references("add", &docs, false, Some(SymbolKind::Method));
483 assert_eq!(refs.len(), 1, "expected 1 method ref, got: {:?}", refs);
485 assert_eq!(refs[0].range.start.line, 3);
486 }
487
488 #[test]
489 fn class_kind_finds_new_expression() {
490 let src = "<?php\nclass Foo {}\n$x = new Foo();\nFoo();";
492 let docs = vec![doc("/a.php", src)];
493 let refs = find_references("Foo", &docs, false, Some(SymbolKind::Class));
494 let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
496 assert!(
497 lines.contains(&2),
498 "expected new Foo() on line 2, got: {:?}",
499 refs
500 );
501 assert!(
502 !lines.contains(&3),
503 "free call Foo() should not appear as class ref, got: {:?}",
504 refs
505 );
506 }
507
508 #[test]
509 fn class_kind_finds_extends_and_implements() {
510 let src = "<?php\nclass Base {}\ninterface Iface {}\nclass Child extends Base implements Iface {}";
511 let docs = vec![doc("/a.php", src)];
512
513 let base_refs = find_references("Base", &docs, false, Some(SymbolKind::Class));
514 let lines_base: Vec<u32> = base_refs.iter().map(|r| r.range.start.line).collect();
515 assert!(
516 lines_base.contains(&3),
517 "expected extends Base on line 3, got: {:?}",
518 base_refs
519 );
520
521 let iface_refs = find_references("Iface", &docs, false, Some(SymbolKind::Class));
522 let lines_iface: Vec<u32> = iface_refs.iter().map(|r| r.range.start.line).collect();
523 assert!(
524 lines_iface.contains(&3),
525 "expected implements Iface on line 3, got: {:?}",
526 iface_refs
527 );
528 }
529
530 #[test]
531 fn class_kind_finds_type_hint() {
532 let src = "<?php\nclass Foo {}\nfunction take(Foo $x): void {}";
534 let docs = vec![doc("/a.php", src)];
535 let refs = find_references("Foo", &docs, false, Some(SymbolKind::Class));
536 let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
537 assert!(
538 lines.contains(&2),
539 "expected type hint Foo on line 2, got: {:?}",
540 refs
541 );
542 }
543
544 #[test]
547 fn function_declaration_span_points_to_name_not_keyword() {
548 let src = "<?php\nfunction greet() {}";
551 let docs = vec![doc("/a.php", src)];
552 let refs = find_references("greet", &docs, true, None);
553 assert_eq!(refs.len(), 1, "expected exactly 1 ref (the declaration)");
554 assert_eq!(
557 refs[0].range.start.line, 1,
558 "declaration should be on line 1"
559 );
560 assert_eq!(
561 refs[0].range.start.character, 9,
562 "declaration should start at the function name, not the 'function' keyword"
563 );
564 assert_eq!(
565 refs[0].range.end.character,
566 refs[0].range.start.character
567 + "greet".chars().map(|c| c.len_utf16() as u32).sum::<u32>(),
568 "range should span exactly the function name"
569 );
570 }
571
572 #[test]
573 fn class_declaration_span_points_to_name_not_keyword() {
574 let src = "<?php\nclass MyClass {}";
575 let docs = vec![doc("/a.php", src)];
576 let refs = find_references("MyClass", &docs, true, None);
577 assert_eq!(refs.len(), 1);
578 assert_eq!(refs[0].range.start.line, 1);
580 assert_eq!(
581 refs[0].range.start.character, 6,
582 "declaration should start at 'MyClass', not 'class'"
583 );
584 }
585
586 #[test]
587 fn method_declaration_span_points_to_name_not_keyword() {
588 let src = "<?php\nclass C {\n public function doThing() {}\n}\n(new C())->doThing();";
589 let docs = vec![doc("/a.php", src)];
590 let refs = find_references("doThing", &docs, true, None);
592 let decl_ref = refs
594 .iter()
595 .find(|r| r.range.start.line == 2)
596 .expect("no declaration ref on line 2");
597 assert_eq!(
599 decl_ref.range.start.character, 20,
600 "method declaration should start at the method name, not 'public function'"
601 );
602 }
603
604 #[test]
605 fn method_kind_with_include_declaration_does_not_return_free_function() {
606 let src =
616 "<?php\nfunction get() {}\nget();\nclass C { public function get() {} }\n$c->get();";
617 let docs = vec![doc("/a.php", src)];
618 let refs = find_references("get", &docs, true, Some(SymbolKind::Method));
619 let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
620 assert!(
621 lines.contains(&3),
622 "method declaration (line 3) must be present, got: {:?}",
623 lines
624 );
625 assert!(
626 lines.contains(&4),
627 "method call (line 4) must be present, got: {:?}",
628 lines
629 );
630 assert!(
631 !lines.contains(&1),
632 "free function declaration (line 1) must not appear when kind=Method, got: {:?}",
633 lines
634 );
635 assert!(
636 !lines.contains(&2),
637 "free function call (line 2) must not appear when kind=Method, got: {:?}",
638 lines
639 );
640 }
641
642 #[test]
643 fn function_kind_with_include_declaration_does_not_return_method_call() {
644 let src =
653 "<?php\nfunction add() {}\nadd();\nclass C { public function add() {} }\n$c->add();";
654 let docs = vec![doc("/a.php", src)];
655 let refs = find_references("add", &docs, true, Some(SymbolKind::Function));
656 let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
657 assert!(
658 lines.contains(&1),
659 "function declaration (line 1) must be present, got: {:?}",
660 lines
661 );
662 assert!(
663 lines.contains(&2),
664 "function call (line 2) must be present, got: {:?}",
665 lines
666 );
667 assert!(
668 !lines.contains(&3),
669 "method declaration (line 3) must not appear when kind=Function, got: {:?}",
670 lines
671 );
672 assert!(
673 !lines.contains(&4),
674 "method call (line 4) must not appear when kind=Function, got: {:?}",
675 lines
676 );
677 }
678
679 #[test]
680 fn interface_method_declaration_included_when_flag_true() {
681 let src = "<?php\ninterface I {\n public function add(): void;\n}\n$obj->add();";
691 let docs = vec![doc("/a.php", src)];
692
693 let refs = find_references("add", &docs, true, Some(SymbolKind::Method));
694 let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
695 assert!(
696 lines.contains(&2),
697 "interface method declaration (line 2) must appear with include_declaration=true, got: {:?}",
698 lines
699 );
700 assert!(
701 lines.contains(&4),
702 "call site (line 4) must appear, got: {:?}",
703 lines
704 );
705
706 let refs_no_decl = find_references("add", &docs, false, Some(SymbolKind::Method));
708 let lines_no_decl: Vec<u32> = refs_no_decl.iter().map(|r| r.range.start.line).collect();
709 assert!(
710 !lines_no_decl.contains(&2),
711 "interface method declaration must be excluded when include_declaration=false, got: {:?}",
712 lines_no_decl
713 );
714 }
715
716 #[test]
717 fn declaration_filter_finds_method_inside_same_named_class() {
718 let src = "<?php\nclass get { public function get() {} }\n$obj->get();";
727 let docs = vec![doc("/a.php", src)];
728
729 let refs = find_references("get", &docs, false, None);
732 let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
733 assert!(
734 !lines.contains(&1),
735 "declaration line (1) must not appear when include_declaration=false, got: {:?}",
736 lines
737 );
738 assert!(
739 lines.contains(&2),
740 "call site (line 2) must be present, got: {:?}",
741 lines
742 );
743
744 let refs_with = find_references("get", &docs, true, None);
747 assert_eq!(
748 refs_with.len(),
749 3,
750 "expected 3 refs (class decl + method decl + call), got: {:?}",
751 refs_with
752 );
753 }
754
755 #[test]
756 fn interface_method_declaration_included_with_kind_none() {
757 let src = "<?php\ninterface I {\n public function add(): void;\n}\n$obj->add();";
767 let docs = vec![doc("/a.php", src)];
768
769 let refs = find_references("add", &docs, true, None);
770 let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
771 assert!(
772 lines.contains(&2),
773 "interface method declaration (line 2) must appear with kind=None + include_declaration=true, got: {:?}",
774 lines
775 );
776 }
777
778 #[test]
779 fn interface_method_declaration_excluded_with_kind_none_flag_false() {
780 let src = "<?php\ninterface I {\n public function add(): void;\n}\n$obj->add();";
791 let docs = vec![doc("/a.php", src)];
792
793 let refs = find_references("add", &docs, false, None);
794 let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
795 assert!(
796 !lines.contains(&2),
797 "interface method declaration (line 2) must be excluded with kind=None + include_declaration=false, got: {:?}",
798 lines
799 );
800 assert!(
801 lines.contains(&4),
802 "call site (line 4) must be present, got: {:?}",
803 lines
804 );
805 }
806
807 #[test]
808 fn function_kind_does_not_include_interface_method_declaration() {
809 let src =
820 "<?php\nfunction add() {}\nadd();\ninterface I {\n public function add(): void;\n}";
821 let docs = vec![doc("/a.php", src)];
822
823 let refs = find_references("add", &docs, true, Some(SymbolKind::Function));
824 let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
825 assert!(
826 lines.contains(&1),
827 "free function declaration (line 1) must be present, got: {:?}",
828 lines
829 );
830 assert!(
831 lines.contains(&2),
832 "free function call (line 2) must be present, got: {:?}",
833 lines
834 );
835 assert!(
836 !lines.contains(&4),
837 "interface method declaration (line 4) must not appear with kind=Function, got: {:?}",
838 lines
839 );
840 }
841}