1use std::collections::HashSet;
2use std::sync::Arc;
3
4use php_ast::{ClassMemberKind, EnumMemberKind, NamespaceBody, Span, Stmt, StmtKind};
5use rayon::prelude::*;
6use tower_lsp::lsp_types::{Location, Position, Range, Url};
7
8use crate::ast::{ParsedDoc, str_offset_in_range};
9use crate::util::utf16_code_units;
10use crate::walk::{
11 class_refs_in_stmts, fqn_new_class_refs_in_stmts, function_refs_in_stmts, method_refs_in_stmts,
12 new_refs_in_stmts, property_refs_in_stmts, refs_in_stmts, refs_in_stmts_with_use,
13};
14
15pub type RefLookup<'a> = dyn Fn(&str) -> Vec<(Arc<str>, u32, u16, u16)> + 'a;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum SymbolKind {
24 Function,
26 Method,
28 Class,
30 Property,
32}
33
34fn class_has_ancestor(
35 codebase: &mir_analyzer::db::MirDb,
36 class_fqcn: &str,
37 target_fqcn: &str,
38) -> bool {
39 mir_analyzer::db::extends_or_implements_via_db(codebase, class_fqcn, target_fqcn)
40}
41
42pub fn find_references(
47 word: &str,
48 all_docs: &[(Url, Arc<ParsedDoc>)],
49 include_declaration: bool,
50 kind: Option<SymbolKind>,
51) -> Vec<Location> {
52 find_references_inner(word, all_docs, include_declaration, false, kind, None)
53}
54
55pub fn find_references_with_target(
60 word: &str,
61 all_docs: &[(Url, Arc<ParsedDoc>)],
62 include_declaration: bool,
63 kind: Option<SymbolKind>,
64 target_fqn: &str,
65) -> Vec<Location> {
66 let include_use = kind.is_none();
72 find_references_inner(
73 word,
74 all_docs,
75 include_declaration,
76 include_use,
77 kind,
78 Some(target_fqn),
79 )
80}
81
82pub fn find_references_with_use(
86 word: &str,
87 all_docs: &[(Url, Arc<ParsedDoc>)],
88 include_declaration: bool,
89) -> Vec<Location> {
90 find_references_inner(word, all_docs, include_declaration, true, None, None)
91}
92
93pub fn find_constructor_references(
105 short_name: &str,
106 all_docs: &[(Url, Arc<ParsedDoc>)],
107 class_fqn: Option<&str>,
108) -> Vec<Location> {
109 all_docs
110 .par_iter()
111 .flat_map_iter(|(uri, doc)| {
112 if let Some(fqn) = class_fqn
116 && !doc_can_reference_target(doc, short_name, fqn)
117 && !doc.view().source().contains(fqn.trim_start_matches('\\'))
118 {
119 return Vec::new();
120 }
121 let mut spans = Vec::new();
122 new_refs_in_stmts(&doc.program().stmts, short_name, class_fqn, &mut spans);
123 let sv = doc.view();
124 spans
125 .into_iter()
126 .map(|span| {
127 let start = sv.position_of(span.start);
128 let end = sv.position_of(span.end);
129 Location {
130 uri: uri.clone(),
131 range: Range { start, end },
132 }
133 })
134 .collect::<Vec<_>>()
135 })
136 .collect()
137}
138
139pub fn find_references_codebase(
158 word: &str,
159 all_docs: &[(Url, Arc<ParsedDoc>)],
160 include_declaration: bool,
161 kind: Option<SymbolKind>,
162 codebase: &mir_analyzer::db::MirDb,
163 lookup_refs: &RefLookup<'_>,
164) -> Option<Vec<Location>> {
165 find_references_codebase_with_target(
166 word,
167 all_docs,
168 include_declaration,
169 kind,
170 None,
171 codebase,
172 lookup_refs,
173 )
174}
175
176pub fn find_references_codebase_with_target(
181 _word: &str,
182 _all_docs: &[(Url, Arc<ParsedDoc>)],
183 _include_declaration: bool,
184 kind: Option<SymbolKind>,
185 _target_fqn: Option<&str>,
186 _codebase: &mir_analyzer::db::MirDb,
187 _lookup_refs: &RefLookup<'_>,
188) -> Option<Vec<Location>> {
189 match kind {
190 Some(SymbolKind::Function) => {
191 None
194 }
195
196 Some(SymbolKind::Class) => None,
201
202 Some(SymbolKind::Method) => {
203 None
206 }
207
208 None => None,
210
211 Some(SymbolKind::Property) => None,
214 }
215}
216
217fn find_references_inner(
218 word: &str,
219 all_docs: &[(Url, Arc<ParsedDoc>)],
220 include_declaration: bool,
221 include_use: bool,
222 kind: Option<SymbolKind>,
223 target_fqn: Option<&str>,
224) -> Vec<Location> {
225 let namespace_filter_active =
234 matches!(kind, Some(SymbolKind::Function) | Some(SymbolKind::Class));
235 all_docs
236 .par_iter()
237 .flat_map_iter(|(uri, doc)| {
238 if namespace_filter_active
239 && let Some(target) = target_fqn
240 && !doc_can_reference_target(doc, word, target)
241 {
242 return Vec::new();
243 }
244 scan_doc(word, uri, doc, include_declaration, include_use, kind)
245 })
246 .collect()
247}
248
249fn doc_can_reference_target(doc: &ParsedDoc, word: &str, target_fqn: &str) -> bool {
254 let target = target_fqn.trim_start_matches('\\');
255 let imports = collect_file_imports(doc);
256 let resolved = crate::moniker::resolve_fqn(doc, word, &imports);
257 resolved == target
262 || (resolved == word && !target.contains('\\'))
263 || (resolved == word && target == format!("\\{word}"))
264}
265
266pub(crate) fn collect_file_imports(doc: &ParsedDoc) -> std::collections::HashMap<String, String> {
270 let mut out = std::collections::HashMap::new();
271 fn walk(stmts: &[Stmt<'_, '_>], out: &mut std::collections::HashMap<String, String>) {
272 for stmt in stmts {
273 match &stmt.kind {
274 StmtKind::Use(u) => {
275 for item in u.uses.iter() {
276 let fqn = item.name.to_string_repr().into_owned();
277 let short = item
278 .alias
279 .map(|a| a.to_string())
280 .unwrap_or_else(|| fqn.rsplit('\\').next().unwrap_or(&fqn).to_string());
281 out.insert(short, fqn);
282 }
283 }
284 StmtKind::Namespace(ns) => {
285 if let NamespaceBody::Braced(inner) = &ns.body {
286 walk(inner, out);
287 }
288 }
289 _ => {}
290 }
291 }
292 }
293 walk(&doc.program().stmts, &mut out);
294 out
295}
296
297pub(crate) fn collect_fqn_new_class_refs(doc: &ParsedDoc) -> Vec<String> {
302 fqn_new_class_refs_in_stmts(&doc.program().stmts)
303}
304
305fn scan_doc(
306 word: &str,
307 uri: &Url,
308 doc: &Arc<ParsedDoc>,
309 include_declaration: bool,
310 include_use: bool,
311 kind: Option<SymbolKind>,
312) -> Vec<Location> {
313 let source = doc.source();
314 if !source.contains(word) {
319 return Vec::new();
320 }
321 let stmts = &doc.program().stmts;
322 let mut spans = Vec::new();
323
324 if include_use {
325 refs_in_stmts_with_use(source, stmts, word, &mut spans);
327 if !include_declaration {
328 let mut decl_spans = Vec::new();
329 collect_declaration_spans(source, stmts, word, None, &mut decl_spans);
330 let decl_set: HashSet<(u32, u32)> =
331 decl_spans.iter().map(|s| (s.start, s.end)).collect();
332 spans.retain(|span| !decl_set.contains(&(span.start, span.end)));
333 }
334 } else {
335 match kind {
336 Some(SymbolKind::Function) => function_refs_in_stmts(stmts, word, &mut spans),
337 Some(SymbolKind::Method) => method_refs_in_stmts(stmts, word, &mut spans),
338 Some(SymbolKind::Class) => class_refs_in_stmts(stmts, word, &mut spans),
339 Some(SymbolKind::Property) => {
342 property_refs_in_stmts(source, stmts, word, &mut spans);
343 if !include_declaration {
344 let mut decl_spans = Vec::new();
345 collect_declaration_spans(
346 source,
347 stmts,
348 word,
349 Some(SymbolKind::Property),
350 &mut decl_spans,
351 );
352 let decl_set: HashSet<(u32, u32)> =
353 decl_spans.iter().map(|s| (s.start, s.end)).collect();
354 spans.retain(|span| !decl_set.contains(&(span.start, span.end)));
355 }
356 }
357 None => {
359 refs_in_stmts(source, stmts, word, &mut spans);
360 if !include_declaration {
361 let mut decl_spans = Vec::new();
362 collect_declaration_spans(source, stmts, word, None, &mut decl_spans);
363 let decl_set: HashSet<(u32, u32)> =
364 decl_spans.iter().map(|s| (s.start, s.end)).collect();
365 spans.retain(|span| !decl_set.contains(&(span.start, span.end)));
366 }
367 }
368 }
369 if include_declaration
374 && matches!(
375 kind,
376 Some(SymbolKind::Function) | Some(SymbolKind::Method) | Some(SymbolKind::Class)
377 )
378 {
379 collect_declaration_spans(source, stmts, word, kind, &mut spans);
380 }
381 }
382
383 let sv = doc.view();
384 let word_utf16_len: u32 = utf16_code_units(word);
385 spans
386 .into_iter()
387 .map(|span| {
388 let start = sv.position_of(span.start);
389 let end = Position {
390 line: start.line,
391 character: start.character + word_utf16_len,
392 };
393 Location {
394 uri: uri.clone(),
395 range: Range { start, end },
396 }
397 })
398 .collect()
399}
400
401fn declaration_name_span(source: &str, name: &str, stmt_span: Span) -> Span {
405 let start = str_offset_in_range(source, stmt_span, name).unwrap_or(stmt_span.start);
406 Span {
407 start,
408 end: start + name.len() as u32,
409 }
410}
411
412fn collect_method_decls_in_class(
418 source: &str,
419 stmts: &[Stmt<'_, '_>],
420 class_short: &str,
421 method_word: &str,
422 out: &mut Vec<Span>,
423) {
424 for stmt in stmts {
425 match &stmt.kind {
426 StmtKind::Class(c)
427 if c.name.as_ref().map(|n| n.to_string()) == Some(class_short.to_string()) =>
428 {
429 for member in c.members.iter() {
430 if let ClassMemberKind::Method(m) = &member.kind
431 && m.name == method_word
432 {
433 out.push(declaration_name_span(
434 source,
435 &m.name.to_string(),
436 stmt.span,
437 ));
438 }
439 }
440 }
441 StmtKind::Interface(i) if i.name == class_short => {
442 for member in i.members.iter() {
443 if let ClassMemberKind::Method(m) = &member.kind
444 && m.name == method_word
445 {
446 out.push(declaration_name_span(
447 source,
448 &m.name.to_string(),
449 stmt.span,
450 ));
451 }
452 }
453 }
454 StmtKind::Trait(t) if t.name == class_short => {
455 for member in t.members.iter() {
456 if let ClassMemberKind::Method(m) = &member.kind
457 && m.name == method_word
458 {
459 out.push(declaration_name_span(
460 source,
461 &m.name.to_string(),
462 stmt.span,
463 ));
464 }
465 }
466 }
467 StmtKind::Enum(e) if e.name == class_short => {
468 for member in e.members.iter() {
469 if let EnumMemberKind::Method(m) = &member.kind
470 && m.name == method_word
471 {
472 out.push(declaration_name_span(
473 source,
474 &m.name.to_string(),
475 stmt.span,
476 ));
477 }
478 }
479 }
480 StmtKind::Namespace(ns) => {
481 if let NamespaceBody::Braced(inner) = &ns.body {
482 collect_method_decls_in_class(source, inner, class_short, method_word, out);
483 }
484 }
485 _ => {}
486 }
487 }
488}
489
490fn collect_declaration_spans(
499 source: &str,
500 stmts: &[Stmt<'_, '_>],
501 word: &str,
502 kind: Option<SymbolKind>,
503 out: &mut Vec<Span>,
504) {
505 let want_free = matches!(kind, None | Some(SymbolKind::Function));
506 let want_method = matches!(kind, None | Some(SymbolKind::Method));
507 let want_type = matches!(kind, None | Some(SymbolKind::Class));
508 let want_property = matches!(kind, None | Some(SymbolKind::Property));
509
510 for stmt in stmts {
511 match &stmt.kind {
512 StmtKind::Function(f) if want_free && f.name == word => {
513 out.push(declaration_name_span(
514 source,
515 &f.name.to_string(),
516 stmt.span,
517 ));
518 }
519 StmtKind::Class(c) => {
520 if want_type
521 && let Some(name) = c.name
522 && name == word
523 {
524 out.push(declaration_name_span(source, &name.to_string(), stmt.span));
525 }
526 if want_method || want_property {
527 for member in c.members.iter() {
528 match &member.kind {
529 ClassMemberKind::Method(m) if want_method && m.name == word => {
530 out.push(declaration_name_span(
536 source,
537 &m.name.to_string(),
538 member.span,
539 ));
540 }
541 ClassMemberKind::Method(m)
542 if want_property && m.name == "__construct" =>
543 {
544 for p in m.params.iter() {
546 if p.visibility.is_some() && p.name == word {
547 out.push(declaration_name_span(
548 source,
549 &p.name.to_string(),
550 p.span,
551 ));
552 }
553 }
554 }
555 ClassMemberKind::Property(p) if want_property && p.name == word => {
556 out.push(declaration_name_span(
557 source,
558 &p.name.to_string(),
559 member.span,
560 ));
561 }
562 _ => {}
563 }
564 }
565 }
566 }
567 StmtKind::Interface(i) => {
568 if want_type && i.name == word {
569 out.push(declaration_name_span(
570 source,
571 &i.name.to_string(),
572 stmt.span,
573 ));
574 }
575 if want_method {
576 for member in i.members.iter() {
577 if let ClassMemberKind::Method(m) = &member.kind
578 && m.name == word
579 {
580 out.push(declaration_name_span(
581 source,
582 &m.name.to_string(),
583 member.span,
584 ));
585 }
586 }
587 }
588 }
589 StmtKind::Trait(t) => {
590 if want_type && t.name == word {
591 out.push(declaration_name_span(
592 source,
593 &t.name.to_string(),
594 stmt.span,
595 ));
596 }
597 if want_method || want_property {
598 for member in t.members.iter() {
599 match &member.kind {
600 ClassMemberKind::Method(m) if want_method && m.name == word => {
601 out.push(declaration_name_span(
602 source,
603 &m.name.to_string(),
604 stmt.span,
605 ));
606 }
607 ClassMemberKind::Property(p) if want_property && p.name == word => {
608 out.push(declaration_name_span(
609 source,
610 &p.name.to_string(),
611 stmt.span,
612 ));
613 }
614 _ => {}
615 }
616 }
617 }
618 }
619 StmtKind::Enum(e) => {
620 if want_type && e.name == word {
621 out.push(declaration_name_span(
622 source,
623 &e.name.to_string(),
624 stmt.span,
625 ));
626 }
627 for member in e.members.iter() {
628 match &member.kind {
629 EnumMemberKind::Method(m) if want_method && m.name == word => {
630 out.push(declaration_name_span(
631 source,
632 &m.name.to_string(),
633 stmt.span,
634 ));
635 }
636 EnumMemberKind::Case(c) if want_type && c.name == word => {
637 out.push(declaration_name_span(
638 source,
639 &c.name.to_string(),
640 stmt.span,
641 ));
642 }
643 _ => {}
644 }
645 }
646 }
647 StmtKind::Namespace(ns) => {
648 if let NamespaceBody::Braced(inner) = &ns.body {
649 collect_declaration_spans(source, inner, word, kind, out);
650 }
651 }
652 _ => {}
653 }
654 }
655}
656
657#[cfg(test)]
658mod tests {
659 use super::*;
660
661 fn uri(path: &str) -> Url {
662 Url::parse(&format!("file://{path}")).unwrap()
663 }
664
665 fn doc(path: &str, source: &str) -> (Url, Arc<ParsedDoc>) {
666 (uri(path), Arc::new(ParsedDoc::parse(source.to_string())))
667 }
668
669 #[test]
670 fn finds_function_call_reference() {
671 let src = "<?php\nfunction greet() {}\ngreet();\ngreet();";
672 let docs = vec![doc("/a.php", src)];
673 let refs = find_references("greet", &docs, false, None);
674 assert_eq!(refs.len(), 2, "expected 2 call-site refs, got {:?}", refs);
675 }
676
677 #[test]
678 fn include_declaration_adds_def_site() {
679 let src = "<?php\nfunction greet() {}\ngreet();";
680 let docs = vec![doc("/a.php", src)];
681 let with_decl = find_references("greet", &docs, true, None);
682 let without_decl = find_references("greet", &docs, false, None);
683 assert_eq!(
685 without_decl.len(),
686 1,
687 "expected 1 call-site ref without declaration"
688 );
689 assert_eq!(
690 without_decl[0].range.start.line, 2,
691 "call site should be on line 2"
692 );
693 assert_eq!(
695 with_decl.len(),
696 2,
697 "expected 2 refs with declaration included"
698 );
699 }
700
701 #[test]
702 fn finds_new_expression_reference() {
703 let src = "<?php\nclass Foo {}\n$x = new Foo();";
704 let docs = vec![doc("/a.php", src)];
705 let refs = find_references("Foo", &docs, false, None);
706 assert_eq!(
707 refs.len(),
708 1,
709 "expected exactly 1 reference to Foo in new expr"
710 );
711 assert_eq!(
712 refs[0].range.start.line, 2,
713 "new Foo() reference should be on line 2"
714 );
715 }
716
717 #[test]
718 fn finds_reference_in_nested_function_call() {
719 let src = "<?php\nfunction greet() {}\necho(greet());";
720 let docs = vec![doc("/a.php", src)];
721 let refs = find_references("greet", &docs, false, None);
722 assert_eq!(
723 refs.len(),
724 1,
725 "expected exactly 1 nested function call reference"
726 );
727 assert_eq!(
728 refs[0].range.start.line, 2,
729 "nested greet() call should be on line 2"
730 );
731 }
732
733 #[test]
734 fn finds_references_across_multiple_docs() {
735 let a = doc("/a.php", "<?php\nfunction helper() {}");
736 let b = doc("/b.php", "<?php\nhelper();\nhelper();");
737 let refs = find_references("helper", &[a, b], false, None);
738 assert_eq!(refs.len(), 2, "expected 2 cross-file references");
739 assert!(refs.iter().all(|r| r.uri.path().ends_with("/b.php")));
740 }
741
742 #[test]
743 fn finds_method_call_reference() {
744 let src = "<?php\nclass Calc { public function add() {} }\n$c = new Calc();\n$c->add();";
745 let docs = vec![doc("/a.php", src)];
746 let refs = find_references("add", &docs, false, None);
747 assert_eq!(
748 refs.len(),
749 1,
750 "expected exactly 1 method call reference to 'add'"
751 );
752 assert_eq!(
753 refs[0].range.start.line, 3,
754 "add() call should be on line 3"
755 );
756 }
757
758 #[test]
759 fn finds_reference_inside_if_body() {
760 let src = "<?php\nfunction check() {}\nif (true) { check(); }";
761 let docs = vec![doc("/a.php", src)];
762 let refs = find_references("check", &docs, false, None);
763 assert_eq!(refs.len(), 1, "expected exactly 1 reference inside if body");
764 assert_eq!(
765 refs[0].range.start.line, 2,
766 "check() inside if should be on line 2"
767 );
768 }
769
770 #[test]
771 fn finds_use_statement_reference() {
772 let src = "<?php\nuse MyClass;\n$x = new MyClass();";
775 let docs = vec![doc("/a.php", src)];
776 let refs = find_references_with_use("MyClass", &docs, false);
777 assert_eq!(
779 refs.len(),
780 2,
781 "expected exactly 2 references, got: {:?}",
782 refs
783 );
784 let mut lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
785 lines.sort_unstable();
786 assert_eq!(
787 lines,
788 vec![1, 2],
789 "references should be on lines 1 (use) and 2 (new)"
790 );
791 }
792
793 #[test]
794 fn find_references_returns_correct_lines() {
795 let src = "<?php\nhelper();\nhelper();\nfunction helper() {}";
797 let docs = vec![doc("/a.php", src)];
798 let refs = find_references("helper", &docs, false, None);
799 assert_eq!(refs.len(), 2, "expected exactly 2 call-site references");
800 let mut lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
801 lines.sort_unstable();
802 assert_eq!(lines, vec![1, 2], "references should be on lines 1 and 2");
803 }
804
805 #[test]
806 fn declaration_excluded_when_flag_false() {
807 let src = "<?php\nfunction doWork() {}\ndoWork();\ndoWork();";
809 let docs = vec![doc("/a.php", src)];
810 let refs = find_references("doWork", &docs, false, None);
811 let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
813 assert!(
814 !lines.contains(&1),
815 "declaration line (1) must not appear when include_declaration=false, got: {:?}",
816 lines
817 );
818 assert_eq!(refs.len(), 2, "expected 2 call-site references only");
819 }
820
821 #[test]
822 fn partial_match_not_included() {
823 let src = "<?php\nfunction greet() {}\nfunction greeting() {}\ngreet();\ngreeting();";
825 let docs = vec![doc("/a.php", src)];
826 let refs = find_references("greet", &docs, false, None);
827 for r in &refs {
829 let span_len = r.range.end.character - r.range.start.character;
832 assert_eq!(
833 span_len, 5,
834 "reference span length should equal len('greet')=5, got {} at {:?}",
835 span_len, r
836 );
837 }
838 assert_eq!(
840 refs.len(),
841 1,
842 "expected exactly 1 reference to 'greet' (not 'greeting'), got: {:?}",
843 refs
844 );
845 }
846
847 #[test]
848 fn finds_reference_in_class_property_default() {
849 let src = "<?php\nclass Foo {\n public string $status = Status::ACTIVE;\n}";
851 let docs = vec![doc("/a.php", src)];
852 let refs = find_references("Status", &docs, false, None);
853 assert_eq!(
854 refs.len(),
855 1,
856 "expected exactly 1 reference to Status in property default, got: {:?}",
857 refs
858 );
859 assert_eq!(refs[0].range.start.line, 2, "reference should be on line 2");
860 }
861
862 #[test]
863 fn class_const_access_span_covers_only_member_name() {
864 let src = "<?php\n$x = Status::ACTIVE;";
870 let docs = vec![doc("/a.php", src)];
871 let refs = find_references("ACTIVE", &docs, false, None);
872 assert_eq!(refs.len(), 1, "expected 1 reference, got: {:?}", refs);
873 let r = &refs[0].range;
874 assert_eq!(r.start.line, 1, "reference must be on line 1");
875 assert_eq!(
878 r.start.character, 13,
879 "range must start at 'ACTIVE' (char 13), not at 'Status' (char 5); got {:?}",
880 r
881 );
882 }
883
884 #[test]
885 fn class_const_access_no_duplicate_when_name_equals_class() {
886 let src = "<?php\n$x = Status::Status;";
896 let docs = vec![doc("/a.php", src)];
897 let refs = find_references("Status", &docs, false, None);
898 assert_eq!(
899 refs.len(),
900 2,
901 "expected exactly 2 refs (class side + member side), got: {:?}",
902 refs
903 );
904 let mut chars: Vec<u32> = refs.iter().map(|r| r.range.start.character).collect();
905 chars.sort_unstable();
906 assert_eq!(
907 chars,
908 vec![5, 13],
909 "class-side ref must be at char 5 and member-side at char 13, got: {:?}",
910 refs
911 );
912 }
913
914 #[test]
915 fn finds_reference_inside_enum_method_body() {
916 let src = "<?php\nfunction helper() {}\nenum Status {\n public function label(): string { return helper(); }\n}";
918 let docs = vec![doc("/a.php", src)];
919 let refs = find_references("helper", &docs, false, None);
920 assert_eq!(
921 refs.len(),
922 1,
923 "expected exactly 1 reference to helper() inside enum method, got: {:?}",
924 refs
925 );
926 assert_eq!(refs[0].range.start.line, 3, "reference should be on line 3");
927 }
928
929 #[test]
930 fn finds_reference_in_for_init_and_update() {
931 let src = "<?php\nfunction tick() {}\nfor (tick(); $i < 10; tick()) {}";
933 let docs = vec![doc("/a.php", src)];
934 let refs = find_references("tick", &docs, false, None);
935 assert_eq!(
936 refs.len(),
937 2,
938 "expected exactly 2 references to tick() (init + update), got: {:?}",
939 refs
940 );
941 assert!(refs.iter().all(|r| r.range.start.line == 2));
943 }
944
945 #[test]
948 fn function_kind_skips_method_call_with_same_name() {
949 let src = "<?php\nfunction get() {}\nget();\n$obj->get();";
951 let docs = vec![doc("/a.php", src)];
952 let refs = find_references("get", &docs, false, Some(SymbolKind::Function));
953 assert_eq!(
955 refs.len(),
956 1,
957 "expected 1 free-function ref, got: {:?}",
958 refs
959 );
960 assert_eq!(refs[0].range.start.line, 2);
961 }
962
963 #[test]
964 fn method_kind_skips_free_function_call_with_same_name() {
965 let src = "<?php\nfunction add() {}\nadd();\n$calc->add();";
967 let docs = vec![doc("/a.php", src)];
968 let refs = find_references("add", &docs, false, Some(SymbolKind::Method));
969 assert_eq!(refs.len(), 1, "expected 1 method ref, got: {:?}", refs);
971 assert_eq!(refs[0].range.start.line, 3);
972 }
973
974 #[test]
975 fn class_kind_finds_new_expression() {
976 let src = "<?php\nclass Foo {}\n$x = new Foo();\nFoo();";
978 let docs = vec![doc("/a.php", src)];
979 let refs = find_references("Foo", &docs, false, Some(SymbolKind::Class));
980 let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
982 assert!(
983 lines.contains(&2),
984 "expected new Foo() on line 2, got: {:?}",
985 refs
986 );
987 assert!(
988 !lines.contains(&3),
989 "free call Foo() should not appear as class ref, got: {:?}",
990 refs
991 );
992 }
993
994 #[test]
995 fn class_kind_finds_extends_and_implements() {
996 let src = "<?php\nclass Base {}\ninterface Iface {}\nclass Child extends Base implements Iface {}";
997 let docs = vec![doc("/a.php", src)];
998
999 let base_refs = find_references("Base", &docs, false, Some(SymbolKind::Class));
1000 let lines_base: Vec<u32> = base_refs.iter().map(|r| r.range.start.line).collect();
1001 assert!(
1002 lines_base.contains(&3),
1003 "expected extends Base on line 3, got: {:?}",
1004 base_refs
1005 );
1006
1007 let iface_refs = find_references("Iface", &docs, false, Some(SymbolKind::Class));
1008 let lines_iface: Vec<u32> = iface_refs.iter().map(|r| r.range.start.line).collect();
1009 assert!(
1010 lines_iface.contains(&3),
1011 "expected implements Iface on line 3, got: {:?}",
1012 iface_refs
1013 );
1014 }
1015
1016 #[test]
1017 fn class_kind_finds_type_hint() {
1018 let src = "<?php\nclass Foo {}\nfunction take(Foo $x): void {}";
1020 let docs = vec![doc("/a.php", src)];
1021 let refs = find_references("Foo", &docs, false, Some(SymbolKind::Class));
1022 let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
1023 assert!(
1024 lines.contains(&2),
1025 "expected type hint Foo on line 2, got: {:?}",
1026 refs
1027 );
1028 }
1029
1030 #[test]
1033 fn function_declaration_span_points_to_name_not_keyword() {
1034 let src = "<?php\nfunction greet() {}";
1037 let docs = vec![doc("/a.php", src)];
1038 let refs = find_references("greet", &docs, true, None);
1039 assert_eq!(refs.len(), 1, "expected exactly 1 ref (the declaration)");
1040 assert_eq!(
1043 refs[0].range.start.line, 1,
1044 "declaration should be on line 1"
1045 );
1046 assert_eq!(
1047 refs[0].range.start.character, 9,
1048 "declaration should start at the function name, not the 'function' keyword"
1049 );
1050 assert_eq!(
1051 refs[0].range.end.character,
1052 refs[0].range.start.character + utf16_code_units("greet"),
1053 "range should span exactly the function name"
1054 );
1055 }
1056
1057 #[test]
1058 fn class_declaration_span_points_to_name_not_keyword() {
1059 let src = "<?php\nclass MyClass {}";
1060 let docs = vec![doc("/a.php", src)];
1061 let refs = find_references("MyClass", &docs, true, None);
1062 assert_eq!(refs.len(), 1);
1063 assert_eq!(refs[0].range.start.line, 1);
1065 assert_eq!(
1066 refs[0].range.start.character, 6,
1067 "declaration should start at 'MyClass', not 'class'"
1068 );
1069 }
1070
1071 #[test]
1072 fn method_declaration_span_points_to_name_not_keyword() {
1073 let src = "<?php\nclass C {\n public function doThing() {}\n}\n(new C())->doThing();";
1074 let docs = vec![doc("/a.php", src)];
1075 let refs = find_references("doThing", &docs, true, None);
1077 let decl_ref = refs
1079 .iter()
1080 .find(|r| r.range.start.line == 2)
1081 .expect("no declaration ref on line 2");
1082 assert_eq!(
1084 decl_ref.range.start.character, 20,
1085 "method declaration should start at the method name, not 'public function'"
1086 );
1087 }
1088
1089 #[test]
1090 fn method_kind_with_include_declaration_does_not_return_free_function() {
1091 let src =
1101 "<?php\nfunction get() {}\nget();\nclass C { public function get() {} }\n$c->get();";
1102 let docs = vec![doc("/a.php", src)];
1103 let refs = find_references("get", &docs, true, Some(SymbolKind::Method));
1104 let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
1105 assert!(
1106 lines.contains(&3),
1107 "method declaration (line 3) must be present, got: {:?}",
1108 lines
1109 );
1110 assert!(
1111 lines.contains(&4),
1112 "method call (line 4) must be present, got: {:?}",
1113 lines
1114 );
1115 assert!(
1116 !lines.contains(&1),
1117 "free function declaration (line 1) must not appear when kind=Method, got: {:?}",
1118 lines
1119 );
1120 assert!(
1121 !lines.contains(&2),
1122 "free function call (line 2) must not appear when kind=Method, got: {:?}",
1123 lines
1124 );
1125 }
1126
1127 #[test]
1128 fn function_kind_with_include_declaration_does_not_return_method_call() {
1129 let src =
1138 "<?php\nfunction add() {}\nadd();\nclass C { public function add() {} }\n$c->add();";
1139 let docs = vec![doc("/a.php", src)];
1140 let refs = find_references("add", &docs, true, Some(SymbolKind::Function));
1141 let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
1142 assert!(
1143 lines.contains(&1),
1144 "function declaration (line 1) must be present, got: {:?}",
1145 lines
1146 );
1147 assert!(
1148 lines.contains(&2),
1149 "function call (line 2) must be present, got: {:?}",
1150 lines
1151 );
1152 assert!(
1153 !lines.contains(&3),
1154 "method declaration (line 3) must not appear when kind=Function, got: {:?}",
1155 lines
1156 );
1157 assert!(
1158 !lines.contains(&4),
1159 "method call (line 4) must not appear when kind=Function, got: {:?}",
1160 lines
1161 );
1162 }
1163
1164 #[test]
1165 fn interface_method_declaration_included_when_flag_true() {
1166 let src = "<?php\ninterface I {\n public function add(): void;\n}\n$obj->add();";
1176 let docs = vec![doc("/a.php", src)];
1177
1178 let refs = find_references("add", &docs, true, Some(SymbolKind::Method));
1179 let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
1180 assert!(
1181 lines.contains(&2),
1182 "interface method declaration (line 2) must appear with include_declaration=true, got: {:?}",
1183 lines
1184 );
1185 assert!(
1186 lines.contains(&4),
1187 "call site (line 4) must appear, got: {:?}",
1188 lines
1189 );
1190
1191 let refs_no_decl = find_references("add", &docs, false, Some(SymbolKind::Method));
1193 let lines_no_decl: Vec<u32> = refs_no_decl.iter().map(|r| r.range.start.line).collect();
1194 assert!(
1195 !lines_no_decl.contains(&2),
1196 "interface method declaration must be excluded when include_declaration=false, got: {:?}",
1197 lines_no_decl
1198 );
1199 }
1200
1201 #[test]
1202 fn declaration_filter_finds_method_inside_same_named_class() {
1203 let src = "<?php\nclass get { public function get() {} }\n$obj->get();";
1212 let docs = vec![doc("/a.php", src)];
1213
1214 let refs = find_references("get", &docs, false, None);
1217 let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
1218 assert!(
1219 !lines.contains(&1),
1220 "declaration line (1) must not appear when include_declaration=false, got: {:?}",
1221 lines
1222 );
1223 assert!(
1224 lines.contains(&2),
1225 "call site (line 2) must be present, got: {:?}",
1226 lines
1227 );
1228
1229 let refs_with = find_references("get", &docs, true, None);
1232 assert_eq!(
1233 refs_with.len(),
1234 3,
1235 "expected 3 refs (class decl + method decl + call), got: {:?}",
1236 refs_with
1237 );
1238 }
1239
1240 #[test]
1241 fn interface_method_declaration_included_with_kind_none() {
1242 let src = "<?php\ninterface I {\n public function add(): void;\n}\n$obj->add();";
1252 let docs = vec![doc("/a.php", src)];
1253
1254 let refs = find_references("add", &docs, true, None);
1255 let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
1256 assert!(
1257 lines.contains(&2),
1258 "interface method declaration (line 2) must appear with kind=None + include_declaration=true, got: {:?}",
1259 lines
1260 );
1261 }
1262
1263 #[test]
1264 fn interface_method_declaration_excluded_with_kind_none_flag_false() {
1265 let src = "<?php\ninterface I {\n public function add(): void;\n}\n$obj->add();";
1276 let docs = vec![doc("/a.php", src)];
1277
1278 let refs = find_references("add", &docs, false, None);
1279 let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
1280 assert!(
1281 !lines.contains(&2),
1282 "interface method declaration (line 2) must be excluded with kind=None + include_declaration=false, got: {:?}",
1283 lines
1284 );
1285 assert!(
1286 lines.contains(&4),
1287 "call site (line 4) must be present, got: {:?}",
1288 lines
1289 );
1290 }
1291
1292 #[test]
1293 fn function_kind_does_not_include_interface_method_declaration() {
1294 let src =
1305 "<?php\nfunction add() {}\nadd();\ninterface I {\n public function add(): void;\n}";
1306 let docs = vec![doc("/a.php", src)];
1307
1308 let refs = find_references("add", &docs, true, Some(SymbolKind::Function));
1309 let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
1310 assert!(
1311 lines.contains(&1),
1312 "free function declaration (line 1) must be present, got: {:?}",
1313 lines
1314 );
1315 assert!(
1316 lines.contains(&2),
1317 "free function call (line 2) must be present, got: {:?}",
1318 lines
1319 );
1320 assert!(
1321 !lines.contains(&4),
1322 "interface method declaration (line 4) must not appear with kind=Function, got: {:?}",
1323 lines
1324 );
1325 }
1326
1327 #[test]
1330 fn finds_function_call_inside_switch_case() {
1331 let src = "<?php\nfunction tick() {}\nswitch ($x) {\n case 1: tick(); break;\n}";
1334 let docs = vec![doc("/a.php", src)];
1335 let lines: Vec<u32> = find_references("tick", &docs, false, Some(SymbolKind::Function))
1336 .iter()
1337 .map(|r| r.range.start.line)
1338 .collect();
1339 assert!(
1340 lines.contains(&3),
1341 "tick() call inside switch case (line 3) must be present, got: {:?}",
1342 lines
1343 );
1344 }
1345
1346 #[test]
1347 fn finds_method_call_inside_switch_case() {
1348 let src = "<?php\nswitch ($x) {\n case 1: $obj->process(); break;\n}";
1350 let docs = vec![doc("/a.php", src)];
1351 let lines: Vec<u32> = find_references("process", &docs, false, Some(SymbolKind::Method))
1352 .iter()
1353 .map(|r| r.range.start.line)
1354 .collect();
1355 assert!(
1356 lines.contains(&2),
1357 "process() call inside switch case (line 2) must be present, got: {:?}",
1358 lines
1359 );
1360 }
1361
1362 #[test]
1363 fn finds_function_call_inside_switch_condition() {
1364 let src = "<?php\nfunction classify() {}\nswitch (classify()) { default: break; }";
1367 let docs = vec![doc("/a.php", src)];
1368 let lines: Vec<u32> = find_references("classify", &docs, false, Some(SymbolKind::Function))
1369 .iter()
1370 .map(|r| r.range.start.line)
1371 .collect();
1372 assert!(
1373 lines.contains(&2),
1374 "classify() in switch subject (line 2) must be present, got: {:?}",
1375 lines
1376 );
1377 }
1378
1379 #[test]
1380 fn finds_function_call_inside_throw() {
1381 let src = "<?php\nfunction makeException() {}\nthrow makeException();";
1384 let docs = vec![doc("/a.php", src)];
1385 let lines: Vec<u32> =
1386 find_references("makeException", &docs, false, Some(SymbolKind::Function))
1387 .iter()
1388 .map(|r| r.range.start.line)
1389 .collect();
1390 assert!(
1391 lines.contains(&2),
1392 "makeException() inside throw (line 2) must be present, got: {:?}",
1393 lines
1394 );
1395 }
1396
1397 #[test]
1398 fn finds_method_call_inside_throw() {
1399 let src = "<?php\nthrow $factory->create();";
1401 let docs = vec![doc("/a.php", src)];
1402 let lines: Vec<u32> = find_references("create", &docs, false, Some(SymbolKind::Method))
1403 .iter()
1404 .map(|r| r.range.start.line)
1405 .collect();
1406 assert!(
1407 lines.contains(&1),
1408 "create() inside throw (line 1) must be present, got: {:?}",
1409 lines
1410 );
1411 }
1412
1413 #[test]
1414 fn finds_method_call_inside_unset() {
1415 let src = "<?php\nunset($obj->getProp());";
1417 let docs = vec![doc("/a.php", src)];
1418 let lines: Vec<u32> = find_references("getProp", &docs, false, Some(SymbolKind::Method))
1419 .iter()
1420 .map(|r| r.range.start.line)
1421 .collect();
1422 assert!(
1423 lines.contains(&1),
1424 "getProp() inside unset (line 1) must be present, got: {:?}",
1425 lines
1426 );
1427 }
1428
1429 #[test]
1430 fn finds_static_method_call_in_class_property_default() {
1431 let src = "<?php\nclass Config {\n public array $data = self::defaults();\n public static function defaults(): array { return []; }\n}";
1436 let docs = vec![doc("/a.php", src)];
1437 let lines: Vec<u32> = find_references("defaults", &docs, false, Some(SymbolKind::Method))
1438 .iter()
1439 .map(|r| r.range.start.line)
1440 .collect();
1441 assert!(
1442 lines.contains(&2),
1443 "defaults() in class property default (line 2) must be present, got: {:?}",
1444 lines
1445 );
1446 }
1447
1448 #[test]
1449 fn finds_static_method_call_in_trait_property_default() {
1450 let src = "<?php\ntrait T {\n public int $x = self::init();\n public static function init(): int { return 0; }\n}";
1455 let docs = vec![doc("/a.php", src)];
1456 let lines: Vec<u32> = find_references("init", &docs, false, Some(SymbolKind::Method))
1457 .iter()
1458 .map(|r| r.range.start.line)
1459 .collect();
1460 assert!(
1461 lines.contains(&2),
1462 "init() in trait property default (line 2) must be present, got: {:?}",
1463 lines
1464 );
1465 }
1466
1467 #[test]
1470 fn property_kind_finds_instance_property_access() {
1471 let src = "<?php\nclass Order {\n public string $status = '';\n}\nfunction status() {}\n$o->status;\nstatus();";
1473 let docs = vec![doc("/a.php", src)];
1474 let refs = find_references("status", &docs, false, Some(SymbolKind::Property));
1475 let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
1476 assert!(
1477 lines.contains(&5),
1478 "$o->status access (line 5) must be present, got: {:?}",
1479 lines
1480 );
1481 assert!(
1482 !lines.contains(&6),
1483 "free function call status() (line 6) must not appear with kind=Property, got: {:?}",
1484 lines
1485 );
1486 }
1487
1488 #[test]
1489 fn property_kind_with_include_declaration_finds_decl() {
1490 let src = "<?php\nclass Foo {\n public int $count = 0;\n}\n$f->count;\n$f->count;";
1492 let docs = vec![doc("/a.php", src)];
1493 let refs_with = find_references("count", &docs, true, Some(SymbolKind::Property));
1494 let lines_with: Vec<u32> = refs_with.iter().map(|r| r.range.start.line).collect();
1495 assert!(
1496 lines_with.contains(&2),
1497 "property declaration (line 2) must be included with include_declaration=true, got: {:?}",
1498 lines_with
1499 );
1500 assert!(
1501 lines_with.contains(&4),
1502 "first access (line 4) must be included, got: {:?}",
1503 lines_with
1504 );
1505 assert!(
1506 lines_with.contains(&5),
1507 "second access (line 5) must be included, got: {:?}",
1508 lines_with
1509 );
1510 }
1511
1512 #[test]
1513 fn property_kind_excludes_declaration_when_flag_false() {
1514 let src = "<?php\nclass Foo {\n public int $count = 0;\n}\n$f->count;";
1516 let docs = vec![doc("/a.php", src)];
1517 let refs = find_references("count", &docs, false, Some(SymbolKind::Property));
1518 let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
1519 assert!(
1520 !lines.contains(&2),
1521 "property declaration (line 2) must be excluded when include_declaration=false, got: {:?}",
1522 lines
1523 );
1524 assert!(
1525 lines.contains(&4),
1526 "access (line 4) must be included, got: {:?}",
1527 lines
1528 );
1529 }
1530
1531 #[test]
1532 fn property_kind_does_not_match_method_with_same_name() {
1533 let src = "<?php\nclass Task {\n public bool $run = false;\n public function run(): void {}\n}\n$t->run;\n$t->run();";
1536 let docs = vec![doc("/a.php", src)];
1537 let refs = find_references("run", &docs, false, Some(SymbolKind::Property));
1538 let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
1539 assert!(
1540 lines.contains(&5),
1541 "property access $t->run (line 5) must be present, got: {:?}",
1542 lines
1543 );
1544 assert!(
1547 !lines.contains(&6),
1548 "method call $t->run() (line 6) must not appear with kind=Property, got: {:?}",
1549 lines
1550 );
1551 }
1552
1553 #[test]
1556 fn method_kind_finds_static_method_call() {
1557 let src = "<?php\nclass Builder {\n public static function create(): self { return new self(); }\n}\nBuilder::create();\n$b->create();";
1559 let docs = vec![doc("/a.php", src)];
1560 let refs = find_references("create", &docs, false, Some(SymbolKind::Method));
1561 let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
1562 assert!(
1563 lines.contains(&4),
1564 "Builder::create() static call (line 4) must be present, got: {:?}",
1565 lines
1566 );
1567 assert!(
1568 lines.contains(&5),
1569 "$b->create() instance call (line 5) must be present, got: {:?}",
1570 lines
1571 );
1572 }
1573
1574 #[test]
1577 fn find_references_with_target_includes_file_whose_namespace_resolves_to_target() {
1578 let src_a = "<?php\nnamespace Alpha;\nfunction make(): void { $w = new Widget(); }";
1581 let docs = vec![doc("/a.php", src_a)];
1582 let refs = find_references_with_target(
1583 "Widget",
1584 &docs,
1585 false,
1586 Some(SymbolKind::Class),
1587 "Alpha\\Widget",
1588 );
1589 let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
1590 assert!(
1591 lines.contains(&2),
1592 "new Widget() in Alpha namespace (line 2) must be included, got: {:?}",
1593 lines
1594 );
1595 }
1596
1597 #[test]
1598 fn find_references_with_target_excludes_file_with_different_namespace() {
1599 let src_a = "<?php\nnamespace Alpha;\n$w = new Widget();";
1602 let src_b = "<?php\nnamespace Beta;\n$w = new Widget();";
1603 let docs = vec![doc("/a.php", src_a), doc("/b.php", src_b)];
1604 let refs = find_references_with_target(
1605 "Widget",
1606 &docs,
1607 false,
1608 Some(SymbolKind::Class),
1609 "Alpha\\Widget",
1610 );
1611 let uris: Vec<&str> = refs.iter().map(|r| r.uri.as_str()).collect();
1612 assert!(
1613 uris.iter().any(|u| u.ends_with("/a.php")),
1614 "Alpha\\Widget in a.php must be included, got: {:?}",
1615 refs
1616 );
1617 assert!(
1618 !uris.iter().any(|u| u.ends_with("/b.php")),
1619 "Beta\\Widget in b.php must be excluded, got: {:?}",
1620 refs
1621 );
1622 }
1623
1624 #[test]
1625 fn find_references_with_target_global_function_fallback() {
1626 let src = "<?php\n$n = strlen('hello');";
1629 let docs = vec![doc("/a.php", src)];
1630 let refs = find_references_with_target(
1631 "strlen",
1632 &docs,
1633 false,
1634 Some(SymbolKind::Function),
1635 "strlen",
1636 );
1637 assert!(
1638 !refs.is_empty(),
1639 "strlen() in global-namespace file must be included, got: {:?}",
1640 refs
1641 );
1642 }
1643}