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};
9use crate::walk::{
10 class_refs_in_stmts, function_refs_in_stmts, method_refs_in_stmts, new_refs_in_stmts,
11 property_refs_in_stmts, refs_in_stmts, refs_in_stmts_with_use,
12};
13
14pub type RefLookup<'a> = dyn Fn(&str) -> Vec<(Arc<str>, u32, u32)> + 'a;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum SymbolKind {
23 Function,
25 Method,
27 Class,
29 Property,
31}
32
33pub fn find_references(
38 word: &str,
39 all_docs: &[(Url, Arc<ParsedDoc>)],
40 include_declaration: bool,
41 kind: Option<SymbolKind>,
42) -> Vec<Location> {
43 find_references_inner(word, all_docs, include_declaration, false, kind, None)
44}
45
46pub fn find_references_with_target(
51 word: &str,
52 all_docs: &[(Url, Arc<ParsedDoc>)],
53 include_declaration: bool,
54 kind: Option<SymbolKind>,
55 target_fqn: &str,
56) -> Vec<Location> {
57 find_references_inner(
58 word,
59 all_docs,
60 include_declaration,
61 false,
62 kind,
63 Some(target_fqn),
64 )
65}
66
67pub fn find_references_with_use(
71 word: &str,
72 all_docs: &[(Url, Arc<ParsedDoc>)],
73 include_declaration: bool,
74) -> Vec<Location> {
75 find_references_inner(word, all_docs, include_declaration, true, None, None)
76}
77
78pub fn find_constructor_references(
90 short_name: &str,
91 all_docs: &[(Url, Arc<ParsedDoc>)],
92 class_fqn: Option<&str>,
93) -> Vec<Location> {
94 let class_utf16_len: u32 = short_name.chars().map(|c| c.len_utf16() as u32).sum();
95 all_docs
96 .par_iter()
97 .flat_map_iter(|(uri, doc)| {
98 if let Some(fqn) = class_fqn
102 && !doc_can_reference_target(doc, short_name, fqn)
103 && !doc.view().source().contains(fqn.trim_start_matches('\\'))
104 {
105 return Vec::new();
106 }
107 let mut spans = Vec::new();
108 new_refs_in_stmts(&doc.program().stmts, short_name, class_fqn, &mut spans);
109 let sv = doc.view();
110 spans
111 .into_iter()
112 .map(|span| {
113 let start = sv.position_of(span.start);
114 let end = Position {
115 line: start.line,
116 character: start.character + class_utf16_len,
117 };
118 Location {
119 uri: uri.clone(),
120 range: Range { start, end },
121 }
122 })
123 .collect::<Vec<_>>()
124 })
125 .collect()
126}
127
128pub fn find_references_codebase(
147 word: &str,
148 all_docs: &[(Url, Arc<ParsedDoc>)],
149 include_declaration: bool,
150 kind: Option<SymbolKind>,
151 codebase: &mir_codebase::Codebase,
152 lookup_refs: &RefLookup<'_>,
153) -> Option<Vec<Location>> {
154 find_references_codebase_with_target(
155 word,
156 all_docs,
157 include_declaration,
158 kind,
159 None,
160 codebase,
161 lookup_refs,
162 )
163}
164
165pub fn find_references_codebase_with_target(
170 word: &str,
171 all_docs: &[(Url, Arc<ParsedDoc>)],
172 include_declaration: bool,
173 kind: Option<SymbolKind>,
174 target_fqn: Option<&str>,
175 codebase: &mir_codebase::Codebase,
176 lookup_refs: &RefLookup<'_>,
177) -> Option<Vec<Location>> {
178 let doc_map: std::collections::HashMap<&str, (&Url, &Arc<ParsedDoc>)> = all_docs
180 .iter()
181 .map(|(url, doc)| (url.as_str(), (url, doc)))
182 .collect();
183
184 let spans_to_location = |file: &str, start: u32, end: u32| -> Option<Location> {
185 let (url, doc) = doc_map.get(file)?;
186 let sv = doc.view();
187 let start_pos = sv.position_of(start);
188 let end_pos = sv.position_of(end);
189 Some(Location {
190 uri: (*url).clone(),
191 range: Range {
192 start: start_pos,
193 end: end_pos,
194 },
195 })
196 };
197
198 let target_fqn = target_fqn.map(|t| t.trim_start_matches('\\'));
200
201 match kind {
202 Some(SymbolKind::Function) => {
203 let fqns: Vec<Arc<str>> = if let Some(t) = target_fqn.filter(|t| t.contains('\\')) {
206 match codebase.functions.get(t) {
210 Some(entry) => vec![entry.key().clone()],
211 None => return None,
212 }
213 } else {
214 codebase
215 .functions
216 .iter()
217 .filter_map(|e| {
218 let fqn = e.key();
219 let short = fqn.rsplit('\\').next().unwrap_or(fqn.as_ref());
220 if short == word {
221 Some(fqn.clone())
222 } else {
223 None
224 }
225 })
226 .collect()
227 };
228
229 if fqns.is_empty() {
230 return None;
231 }
232
233 let mut call_site_count = 0usize;
234 let mut locations: Vec<Location> = Vec::new();
235 for fqn in &fqns {
236 for (file, start, end) in lookup_refs(fqn) {
237 if let Some(loc) = spans_to_location(&file, start, end) {
238 locations.push(loc);
239 call_site_count += 1;
240 }
241 }
242 if include_declaration
243 && let Some(func) = codebase.functions.get(fqn.as_ref())
244 && let Some(decl) = &func.location
245 && let Some(loc) = spans_to_location(&decl.file, decl.start, decl.end)
246 {
247 locations.push(loc);
248 }
249 }
250 if call_site_count == 0 {
254 return None;
255 }
256 Some(locations)
257 }
258
259 Some(SymbolKind::Class) => None,
264
265 Some(SymbolKind::Method) => {
266 let word_lower = word.to_lowercase();
267
268 let user_code_uris: HashSet<&str> =
272 all_docs.iter().map(|(url, _)| url.as_str()).collect();
273 let is_user_code = |loc: &Option<mir_codebase::storage::Location>| -> bool {
274 loc.as_ref()
275 .is_some_and(|l| user_code_uris.contains(l.file.as_ref()))
276 };
277
278 let mut method_keys: Vec<String> = Vec::new();
279 let mut candidate_arcs: Vec<Arc<str>> = Vec::new();
280
281 if let Some(owner_fqcn) = target_fqn {
282 let mut owners: Vec<Arc<str>> = Vec::new();
288
289 if let Some(entry) = codebase.classes.get(owner_fqcn) {
290 owners.push(entry.key().clone());
291 for e in codebase.classes.iter() {
292 if e.value()
293 .all_parents
294 .iter()
295 .any(|p| p.as_ref() == owner_fqcn)
296 {
297 owners.push(e.key().clone());
298 }
299 }
300 } else if let Some(entry) = codebase.enums.get(owner_fqcn) {
301 owners.push(entry.key().clone());
302 } else if let Some(entry) = codebase.interfaces.get(owner_fqcn) {
303 owners.push(entry.key().clone());
304 for e in codebase.classes.iter() {
305 if e.value()
306 .interfaces
307 .iter()
308 .any(|i| i.as_ref() == owner_fqcn)
309 {
310 owners.push(e.key().clone());
311 }
312 }
313 } else if let Some(entry) = codebase.traits.get(owner_fqcn) {
314 owners.push(entry.key().clone());
315 for e in codebase.classes.iter() {
316 if e.value().traits.iter().any(|t| t.as_ref() == owner_fqcn) {
317 owners.push(e.key().clone());
318 }
319 }
320 } else {
321 return None;
322 }
323
324 let mut call_site_count = 0usize;
328 let mut locations: Vec<Location> = Vec::new();
329 for owner in &owners {
330 let key = format!("{}::{}", owner, word_lower);
331 for (file, start, end) in lookup_refs(&key) {
332 if let Some(loc) = spans_to_location(&file, start, end) {
333 locations.push(loc);
334 call_site_count += 1;
335 }
336 }
337 }
338 if call_site_count == 0 {
343 return None;
344 }
345
346 if include_declaration {
347 for owner in &owners {
352 let decl_file =
353 codebase
354 .classes
355 .get(owner.as_ref())
356 .and_then(|e| {
357 e.value()
358 .own_methods
359 .get(word_lower.as_str())
360 .and_then(|m| m.location.as_ref().map(|l| l.file.clone()))
361 })
362 .or_else(|| {
363 codebase.enums.get(owner.as_ref()).and_then(|e| {
364 e.value().own_methods.get(word_lower.as_str()).and_then(
365 |m| m.location.as_ref().map(|l| l.file.clone()),
366 )
367 })
368 })
369 .or_else(|| {
370 codebase.interfaces.get(owner.as_ref()).and_then(|e| {
371 e.value().own_methods.get(word_lower.as_str()).and_then(
372 |m| m.location.as_ref().map(|l| l.file.clone()),
373 )
374 })
375 })
376 .or_else(|| {
377 codebase.traits.get(owner.as_ref()).and_then(|e| {
378 e.value().own_methods.get(word_lower.as_str()).and_then(
379 |m| m.location.as_ref().map(|l| l.file.clone()),
380 )
381 })
382 });
383 let Some(decl_file) = decl_file else { continue };
384 let Some((url, doc)) = all_docs
385 .iter()
386 .find(|(u, _)| u.as_str() == decl_file.as_ref())
387 else {
388 continue;
389 };
390 let short = owner.rsplit('\\').next().unwrap_or(owner.as_ref());
394 let mut spans: Vec<Span> = Vec::new();
395 collect_method_decls_in_class(
396 doc.source(),
397 &doc.program().stmts,
398 short,
399 word,
400 &mut spans,
401 );
402 let sv = doc.view();
403 let word_utf16_len: u32 = word.chars().map(|c| c.len_utf16() as u32).sum();
404 for span in spans {
405 let start = sv.position_of(span.start);
406 let end = Position {
407 line: start.line,
408 character: start.character + word_utf16_len,
409 };
410 locations.push(Location {
411 uri: (*url).clone(),
412 range: Range { start, end },
413 });
414 }
415 }
416 }
417
418 return if locations.is_empty() {
419 None
420 } else {
421 Some(locations)
422 };
423 } else {
424 for entry in codebase.classes.iter() {
427 let cls = entry.value();
428 if !is_user_code(&cls.location) {
429 continue;
430 }
431 if let Some(method) = cls.own_methods.get(word_lower.as_str())
432 && (cls.is_final || method.visibility == mir_codebase::Visibility::Private)
433 {
434 method_keys.push(format!("{}::{}", entry.key(), word_lower));
435 if include_declaration && let Some(loc) = &method.location {
436 candidate_arcs.push(loc.file.clone());
437 }
438 }
439 }
440 for entry in codebase.enums.iter() {
441 let enm = entry.value();
442 if !is_user_code(&enm.location) {
443 continue;
444 }
445 if let Some(method) = enm.own_methods.get(word_lower.as_str())
446 && method.visibility == mir_codebase::Visibility::Private
447 {
448 method_keys.push(format!("{}::{}", entry.key(), word_lower));
449 if include_declaration && let Some(loc) = &method.location {
450 candidate_arcs.push(loc.file.clone());
451 }
452 }
453 }
454
455 if method_keys.is_empty() {
456 return None;
457 }
458 }
459
460 for key in &method_keys {
462 for (file, _, _) in lookup_refs(key) {
463 candidate_arcs.push(file);
464 }
465 }
466 let candidate_uris: HashSet<&str> = candidate_arcs.iter().map(|a| a.as_ref()).collect();
467
468 let candidate_docs: Vec<(Url, Arc<ParsedDoc>)> = all_docs
470 .iter()
471 .filter(|(url, _)| candidate_uris.contains(url.as_str()))
472 .cloned()
473 .collect();
474
475 let locations = find_references_inner(
476 word,
477 &candidate_docs,
478 include_declaration,
479 false,
480 Some(SymbolKind::Method),
481 None,
482 );
483 Some(locations)
484 }
485
486 None => None,
488
489 Some(SymbolKind::Property) => None,
492 }
493}
494
495fn find_references_inner(
496 word: &str,
497 all_docs: &[(Url, Arc<ParsedDoc>)],
498 include_declaration: bool,
499 include_use: bool,
500 kind: Option<SymbolKind>,
501 target_fqn: Option<&str>,
502) -> Vec<Location> {
503 let namespace_filter_active =
512 matches!(kind, Some(SymbolKind::Function) | Some(SymbolKind::Class));
513 all_docs
514 .par_iter()
515 .flat_map_iter(|(uri, doc)| {
516 if namespace_filter_active
517 && let Some(target) = target_fqn
518 && !doc_can_reference_target(doc, word, target)
519 {
520 return Vec::new();
521 }
522 scan_doc(word, uri, doc, include_declaration, include_use, kind)
523 })
524 .collect()
525}
526
527fn doc_can_reference_target(doc: &ParsedDoc, word: &str, target_fqn: &str) -> bool {
532 let target = target_fqn.trim_start_matches('\\');
533 let imports = collect_file_imports(doc);
534 let resolved = crate::moniker::resolve_fqn(doc, word, &imports);
535 resolved == target
540 || (resolved == word && !target.contains('\\'))
541 || (resolved == word && target == format!("\\{word}"))
542}
543
544fn collect_file_imports(doc: &ParsedDoc) -> std::collections::HashMap<String, String> {
548 let mut out = std::collections::HashMap::new();
549 fn walk(stmts: &[Stmt<'_, '_>], out: &mut std::collections::HashMap<String, String>) {
550 for stmt in stmts {
551 match &stmt.kind {
552 StmtKind::Use(u) => {
553 for item in u.uses.iter() {
554 let fqn = item.name.to_string_repr().into_owned();
555 let short = item
556 .alias
557 .map(|a| a.to_string())
558 .unwrap_or_else(|| fqn.rsplit('\\').next().unwrap_or(&fqn).to_string());
559 out.insert(short, fqn);
560 }
561 }
562 StmtKind::Namespace(ns) => {
563 if let NamespaceBody::Braced(inner) = &ns.body {
564 walk(inner, out);
565 }
566 }
567 _ => {}
568 }
569 }
570 }
571 walk(&doc.program().stmts, &mut out);
572 out
573}
574
575fn scan_doc(
576 word: &str,
577 uri: &Url,
578 doc: &Arc<ParsedDoc>,
579 include_declaration: bool,
580 include_use: bool,
581 kind: Option<SymbolKind>,
582) -> Vec<Location> {
583 let source = doc.source();
584 if !source.contains(word) {
589 return Vec::new();
590 }
591 let stmts = &doc.program().stmts;
592 let mut spans = Vec::new();
593
594 if include_use {
595 refs_in_stmts_with_use(source, stmts, word, &mut spans);
597 if !include_declaration {
598 let mut decl_spans = Vec::new();
599 collect_declaration_spans(source, stmts, word, None, &mut decl_spans);
600 let decl_set: HashSet<(u32, u32)> =
601 decl_spans.iter().map(|s| (s.start, s.end)).collect();
602 spans.retain(|span| !decl_set.contains(&(span.start, span.end)));
603 }
604 } else {
605 match kind {
606 Some(SymbolKind::Function) => function_refs_in_stmts(stmts, word, &mut spans),
607 Some(SymbolKind::Method) => method_refs_in_stmts(stmts, word, &mut spans),
608 Some(SymbolKind::Class) => class_refs_in_stmts(stmts, word, &mut spans),
609 Some(SymbolKind::Property) => {
612 property_refs_in_stmts(source, stmts, word, &mut spans);
613 if !include_declaration {
614 let mut decl_spans = Vec::new();
615 collect_declaration_spans(
616 source,
617 stmts,
618 word,
619 Some(SymbolKind::Property),
620 &mut decl_spans,
621 );
622 let decl_set: HashSet<(u32, u32)> =
623 decl_spans.iter().map(|s| (s.start, s.end)).collect();
624 spans.retain(|span| !decl_set.contains(&(span.start, span.end)));
625 }
626 }
627 None => {
629 refs_in_stmts(source, stmts, word, &mut spans);
630 if !include_declaration {
631 let mut decl_spans = Vec::new();
632 collect_declaration_spans(source, stmts, word, None, &mut decl_spans);
633 let decl_set: HashSet<(u32, u32)> =
634 decl_spans.iter().map(|s| (s.start, s.end)).collect();
635 spans.retain(|span| !decl_set.contains(&(span.start, span.end)));
636 }
637 }
638 }
639 if include_declaration
644 && matches!(
645 kind,
646 Some(SymbolKind::Function) | Some(SymbolKind::Method) | Some(SymbolKind::Class)
647 )
648 {
649 collect_declaration_spans(source, stmts, word, kind, &mut spans);
650 }
651 }
652
653 let sv = doc.view();
654 let word_utf16_len: u32 = word.chars().map(|c| c.len_utf16() as u32).sum();
655 spans
656 .into_iter()
657 .map(|span| {
658 let start = sv.position_of(span.start);
659 let end = Position {
660 line: start.line,
661 character: start.character + word_utf16_len,
662 };
663 Location {
664 uri: uri.clone(),
665 range: Range { start, end },
666 }
667 })
668 .collect()
669}
670
671fn declaration_name_span(source: &str, name: &str) -> Span {
673 let start = str_offset(source, name);
674 Span {
675 start,
676 end: start + name.len() as u32,
677 }
678}
679
680fn collect_method_decls_in_class(
686 source: &str,
687 stmts: &[Stmt<'_, '_>],
688 class_short: &str,
689 method_word: &str,
690 out: &mut Vec<Span>,
691) {
692 for stmt in stmts {
693 match &stmt.kind {
694 StmtKind::Class(c) if c.name == Some(class_short) => {
695 for member in c.members.iter() {
696 if let ClassMemberKind::Method(m) = &member.kind
697 && m.name == method_word
698 {
699 out.push(declaration_name_span(source, m.name));
700 }
701 }
702 }
703 StmtKind::Interface(i) if i.name == class_short => {
704 for member in i.members.iter() {
705 if let ClassMemberKind::Method(m) = &member.kind
706 && m.name == method_word
707 {
708 out.push(declaration_name_span(source, m.name));
709 }
710 }
711 }
712 StmtKind::Trait(t) if t.name == class_short => {
713 for member in t.members.iter() {
714 if let ClassMemberKind::Method(m) = &member.kind
715 && m.name == method_word
716 {
717 out.push(declaration_name_span(source, m.name));
718 }
719 }
720 }
721 StmtKind::Enum(e) if e.name == class_short => {
722 for member in e.members.iter() {
723 if let EnumMemberKind::Method(m) = &member.kind
724 && m.name == method_word
725 {
726 out.push(declaration_name_span(source, m.name));
727 }
728 }
729 }
730 StmtKind::Namespace(ns) => {
731 if let NamespaceBody::Braced(inner) = &ns.body {
732 collect_method_decls_in_class(source, inner, class_short, method_word, out);
733 }
734 }
735 _ => {}
736 }
737 }
738}
739
740fn collect_declaration_spans(
749 source: &str,
750 stmts: &[Stmt<'_, '_>],
751 word: &str,
752 kind: Option<SymbolKind>,
753 out: &mut Vec<Span>,
754) {
755 let want_free = matches!(kind, None | Some(SymbolKind::Function));
756 let want_method = matches!(kind, None | Some(SymbolKind::Method));
757 let want_type = matches!(kind, None | Some(SymbolKind::Class));
758 let want_property = matches!(kind, None | Some(SymbolKind::Property));
759
760 for stmt in stmts {
761 match &stmt.kind {
762 StmtKind::Function(f) => {
763 if want_free && f.name == word {
764 out.push(declaration_name_span(source, f.name));
765 }
766 }
767 StmtKind::Class(c) => {
768 if want_type
769 && let Some(name) = c.name
770 && name == word
771 {
772 out.push(declaration_name_span(source, name));
773 }
774 if want_method || want_property {
775 for member in c.members.iter() {
776 match &member.kind {
777 ClassMemberKind::Method(m) if want_method && m.name == word => {
778 out.push(declaration_name_span(source, m.name));
779 }
780 ClassMemberKind::Method(m)
781 if want_property && m.name == "__construct" =>
782 {
783 for p in m.params.iter() {
785 if p.visibility.is_some() && p.name == word {
786 out.push(declaration_name_span(source, p.name));
787 }
788 }
789 }
790 ClassMemberKind::Property(p) if want_property && p.name == word => {
791 out.push(declaration_name_span(source, p.name));
792 }
793 _ => {}
794 }
795 }
796 }
797 }
798 StmtKind::Interface(i) => {
799 if want_type && i.name == word {
800 out.push(declaration_name_span(source, i.name));
801 }
802 if want_method {
803 for member in i.members.iter() {
804 if let ClassMemberKind::Method(m) = &member.kind
805 && m.name == word
806 {
807 out.push(declaration_name_span(source, m.name));
808 }
809 }
810 }
811 }
812 StmtKind::Trait(t) => {
813 if want_type && t.name == word {
814 out.push(declaration_name_span(source, t.name));
815 }
816 if want_method || want_property {
817 for member in t.members.iter() {
818 match &member.kind {
819 ClassMemberKind::Method(m) if want_method && m.name == word => {
820 out.push(declaration_name_span(source, m.name));
821 }
822 ClassMemberKind::Property(p) if want_property && p.name == word => {
823 out.push(declaration_name_span(source, p.name));
824 }
825 _ => {}
826 }
827 }
828 }
829 }
830 StmtKind::Enum(e) => {
831 if want_type && e.name == word {
832 out.push(declaration_name_span(source, e.name));
833 }
834 for member in e.members.iter() {
835 match &member.kind {
836 EnumMemberKind::Method(m) if want_method && m.name == word => {
837 out.push(declaration_name_span(source, m.name));
838 }
839 EnumMemberKind::Case(c) if want_type && c.name == word => {
840 out.push(declaration_name_span(source, c.name));
841 }
842 _ => {}
843 }
844 }
845 }
846 StmtKind::Namespace(ns) => {
847 if let NamespaceBody::Braced(inner) = &ns.body {
848 collect_declaration_spans(source, inner, word, kind, out);
849 }
850 }
851 _ => {}
852 }
853 }
854}
855
856#[cfg(test)]
857mod tests {
858 use super::*;
859
860 fn uri(path: &str) -> Url {
861 Url::parse(&format!("file://{path}")).unwrap()
862 }
863
864 fn doc(path: &str, source: &str) -> (Url, Arc<ParsedDoc>) {
865 (uri(path), Arc::new(ParsedDoc::parse(source.to_string())))
866 }
867
868 #[test]
869 fn finds_function_call_reference() {
870 let src = "<?php\nfunction greet() {}\ngreet();\ngreet();";
871 let docs = vec![doc("/a.php", src)];
872 let refs = find_references("greet", &docs, false, None);
873 assert_eq!(refs.len(), 2, "expected 2 call-site refs, got {:?}", refs);
874 }
875
876 #[test]
877 fn include_declaration_adds_def_site() {
878 let src = "<?php\nfunction greet() {}\ngreet();";
879 let docs = vec![doc("/a.php", src)];
880 let with_decl = find_references("greet", &docs, true, None);
881 let without_decl = find_references("greet", &docs, false, None);
882 assert_eq!(
884 without_decl.len(),
885 1,
886 "expected 1 call-site ref without declaration"
887 );
888 assert_eq!(
889 without_decl[0].range.start.line, 2,
890 "call site should be on line 2"
891 );
892 assert_eq!(
894 with_decl.len(),
895 2,
896 "expected 2 refs with declaration included"
897 );
898 }
899
900 #[test]
901 fn finds_new_expression_reference() {
902 let src = "<?php\nclass Foo {}\n$x = new Foo();";
903 let docs = vec![doc("/a.php", src)];
904 let refs = find_references("Foo", &docs, false, None);
905 assert_eq!(
906 refs.len(),
907 1,
908 "expected exactly 1 reference to Foo in new expr"
909 );
910 assert_eq!(
911 refs[0].range.start.line, 2,
912 "new Foo() reference should be on line 2"
913 );
914 }
915
916 #[test]
917 fn finds_reference_in_nested_function_call() {
918 let src = "<?php\nfunction greet() {}\necho(greet());";
919 let docs = vec![doc("/a.php", src)];
920 let refs = find_references("greet", &docs, false, None);
921 assert_eq!(
922 refs.len(),
923 1,
924 "expected exactly 1 nested function call reference"
925 );
926 assert_eq!(
927 refs[0].range.start.line, 2,
928 "nested greet() call should be on line 2"
929 );
930 }
931
932 #[test]
933 fn finds_references_across_multiple_docs() {
934 let a = doc("/a.php", "<?php\nfunction helper() {}");
935 let b = doc("/b.php", "<?php\nhelper();\nhelper();");
936 let refs = find_references("helper", &[a, b], false, None);
937 assert_eq!(refs.len(), 2, "expected 2 cross-file references");
938 assert!(refs.iter().all(|r| r.uri.path().ends_with("/b.php")));
939 }
940
941 #[test]
942 fn finds_method_call_reference() {
943 let src = "<?php\nclass Calc { public function add() {} }\n$c = new Calc();\n$c->add();";
944 let docs = vec![doc("/a.php", src)];
945 let refs = find_references("add", &docs, false, None);
946 assert_eq!(
947 refs.len(),
948 1,
949 "expected exactly 1 method call reference to 'add'"
950 );
951 assert_eq!(
952 refs[0].range.start.line, 3,
953 "add() call should be on line 3"
954 );
955 }
956
957 #[test]
958 fn finds_reference_inside_if_body() {
959 let src = "<?php\nfunction check() {}\nif (true) { check(); }";
960 let docs = vec![doc("/a.php", src)];
961 let refs = find_references("check", &docs, false, None);
962 assert_eq!(refs.len(), 1, "expected exactly 1 reference inside if body");
963 assert_eq!(
964 refs[0].range.start.line, 2,
965 "check() inside if should be on line 2"
966 );
967 }
968
969 #[test]
970 fn finds_use_statement_reference() {
971 let src = "<?php\nuse MyClass;\n$x = new MyClass();";
974 let docs = vec![doc("/a.php", src)];
975 let refs = find_references_with_use("MyClass", &docs, false);
976 assert_eq!(
978 refs.len(),
979 2,
980 "expected exactly 2 references, got: {:?}",
981 refs
982 );
983 let mut lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
984 lines.sort_unstable();
985 assert_eq!(
986 lines,
987 vec![1, 2],
988 "references should be on lines 1 (use) and 2 (new)"
989 );
990 }
991
992 #[test]
993 fn find_references_returns_correct_lines() {
994 let src = "<?php\nhelper();\nhelper();\nfunction helper() {}";
996 let docs = vec![doc("/a.php", src)];
997 let refs = find_references("helper", &docs, false, None);
998 assert_eq!(refs.len(), 2, "expected exactly 2 call-site references");
999 let mut lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
1000 lines.sort_unstable();
1001 assert_eq!(lines, vec![1, 2], "references should be on lines 1 and 2");
1002 }
1003
1004 #[test]
1005 fn declaration_excluded_when_flag_false() {
1006 let src = "<?php\nfunction doWork() {}\ndoWork();\ndoWork();";
1008 let docs = vec![doc("/a.php", src)];
1009 let refs = find_references("doWork", &docs, false, None);
1010 let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
1012 assert!(
1013 !lines.contains(&1),
1014 "declaration line (1) must not appear when include_declaration=false, got: {:?}",
1015 lines
1016 );
1017 assert_eq!(refs.len(), 2, "expected 2 call-site references only");
1018 }
1019
1020 #[test]
1021 fn partial_match_not_included() {
1022 let src = "<?php\nfunction greet() {}\nfunction greeting() {}\ngreet();\ngreeting();";
1024 let docs = vec![doc("/a.php", src)];
1025 let refs = find_references("greet", &docs, false, None);
1026 for r in &refs {
1028 let span_len = r.range.end.character - r.range.start.character;
1031 assert_eq!(
1032 span_len, 5,
1033 "reference span length should equal len('greet')=5, got {} at {:?}",
1034 span_len, r
1035 );
1036 }
1037 assert_eq!(
1039 refs.len(),
1040 1,
1041 "expected exactly 1 reference to 'greet' (not 'greeting'), got: {:?}",
1042 refs
1043 );
1044 }
1045
1046 #[test]
1047 fn finds_reference_in_class_property_default() {
1048 let src = "<?php\nclass Foo {\n public string $status = Status::ACTIVE;\n}";
1050 let docs = vec![doc("/a.php", src)];
1051 let refs = find_references("Status", &docs, false, None);
1052 assert_eq!(
1053 refs.len(),
1054 1,
1055 "expected exactly 1 reference to Status in property default, got: {:?}",
1056 refs
1057 );
1058 assert_eq!(refs[0].range.start.line, 2, "reference should be on line 2");
1059 }
1060
1061 #[test]
1062 fn class_const_access_span_covers_only_member_name() {
1063 let src = "<?php\n$x = Status::ACTIVE;";
1069 let docs = vec![doc("/a.php", src)];
1070 let refs = find_references("ACTIVE", &docs, false, None);
1071 assert_eq!(refs.len(), 1, "expected 1 reference, got: {:?}", refs);
1072 let r = &refs[0].range;
1073 assert_eq!(r.start.line, 1, "reference must be on line 1");
1074 assert_eq!(
1077 r.start.character, 13,
1078 "range must start at 'ACTIVE' (char 13), not at 'Status' (char 5); got {:?}",
1079 r
1080 );
1081 }
1082
1083 #[test]
1084 fn class_const_access_no_duplicate_when_name_equals_class() {
1085 let src = "<?php\n$x = Status::Status;";
1095 let docs = vec![doc("/a.php", src)];
1096 let refs = find_references("Status", &docs, false, None);
1097 assert_eq!(
1098 refs.len(),
1099 2,
1100 "expected exactly 2 refs (class side + member side), got: {:?}",
1101 refs
1102 );
1103 let mut chars: Vec<u32> = refs.iter().map(|r| r.range.start.character).collect();
1104 chars.sort_unstable();
1105 assert_eq!(
1106 chars,
1107 vec![5, 13],
1108 "class-side ref must be at char 5 and member-side at char 13, got: {:?}",
1109 refs
1110 );
1111 }
1112
1113 #[test]
1114 fn finds_reference_inside_enum_method_body() {
1115 let src = "<?php\nfunction helper() {}\nenum Status {\n public function label(): string { return helper(); }\n}";
1117 let docs = vec![doc("/a.php", src)];
1118 let refs = find_references("helper", &docs, false, None);
1119 assert_eq!(
1120 refs.len(),
1121 1,
1122 "expected exactly 1 reference to helper() inside enum method, got: {:?}",
1123 refs
1124 );
1125 assert_eq!(refs[0].range.start.line, 3, "reference should be on line 3");
1126 }
1127
1128 #[test]
1129 fn finds_reference_in_for_init_and_update() {
1130 let src = "<?php\nfunction tick() {}\nfor (tick(); $i < 10; tick()) {}";
1132 let docs = vec![doc("/a.php", src)];
1133 let refs = find_references("tick", &docs, false, None);
1134 assert_eq!(
1135 refs.len(),
1136 2,
1137 "expected exactly 2 references to tick() (init + update), got: {:?}",
1138 refs
1139 );
1140 assert!(refs.iter().all(|r| r.range.start.line == 2));
1142 }
1143
1144 #[test]
1147 fn function_kind_skips_method_call_with_same_name() {
1148 let src = "<?php\nfunction get() {}\nget();\n$obj->get();";
1150 let docs = vec![doc("/a.php", src)];
1151 let refs = find_references("get", &docs, false, Some(SymbolKind::Function));
1152 assert_eq!(
1154 refs.len(),
1155 1,
1156 "expected 1 free-function ref, got: {:?}",
1157 refs
1158 );
1159 assert_eq!(refs[0].range.start.line, 2);
1160 }
1161
1162 #[test]
1163 fn method_kind_skips_free_function_call_with_same_name() {
1164 let src = "<?php\nfunction add() {}\nadd();\n$calc->add();";
1166 let docs = vec![doc("/a.php", src)];
1167 let refs = find_references("add", &docs, false, Some(SymbolKind::Method));
1168 assert_eq!(refs.len(), 1, "expected 1 method ref, got: {:?}", refs);
1170 assert_eq!(refs[0].range.start.line, 3);
1171 }
1172
1173 #[test]
1174 fn class_kind_finds_new_expression() {
1175 let src = "<?php\nclass Foo {}\n$x = new Foo();\nFoo();";
1177 let docs = vec![doc("/a.php", src)];
1178 let refs = find_references("Foo", &docs, false, Some(SymbolKind::Class));
1179 let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
1181 assert!(
1182 lines.contains(&2),
1183 "expected new Foo() on line 2, got: {:?}",
1184 refs
1185 );
1186 assert!(
1187 !lines.contains(&3),
1188 "free call Foo() should not appear as class ref, got: {:?}",
1189 refs
1190 );
1191 }
1192
1193 #[test]
1194 fn class_kind_finds_extends_and_implements() {
1195 let src = "<?php\nclass Base {}\ninterface Iface {}\nclass Child extends Base implements Iface {}";
1196 let docs = vec![doc("/a.php", src)];
1197
1198 let base_refs = find_references("Base", &docs, false, Some(SymbolKind::Class));
1199 let lines_base: Vec<u32> = base_refs.iter().map(|r| r.range.start.line).collect();
1200 assert!(
1201 lines_base.contains(&3),
1202 "expected extends Base on line 3, got: {:?}",
1203 base_refs
1204 );
1205
1206 let iface_refs = find_references("Iface", &docs, false, Some(SymbolKind::Class));
1207 let lines_iface: Vec<u32> = iface_refs.iter().map(|r| r.range.start.line).collect();
1208 assert!(
1209 lines_iface.contains(&3),
1210 "expected implements Iface on line 3, got: {:?}",
1211 iface_refs
1212 );
1213 }
1214
1215 #[test]
1216 fn class_kind_finds_type_hint() {
1217 let src = "<?php\nclass Foo {}\nfunction take(Foo $x): void {}";
1219 let docs = vec![doc("/a.php", src)];
1220 let refs = find_references("Foo", &docs, false, Some(SymbolKind::Class));
1221 let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
1222 assert!(
1223 lines.contains(&2),
1224 "expected type hint Foo on line 2, got: {:?}",
1225 refs
1226 );
1227 }
1228
1229 #[test]
1232 fn function_declaration_span_points_to_name_not_keyword() {
1233 let src = "<?php\nfunction greet() {}";
1236 let docs = vec![doc("/a.php", src)];
1237 let refs = find_references("greet", &docs, true, None);
1238 assert_eq!(refs.len(), 1, "expected exactly 1 ref (the declaration)");
1239 assert_eq!(
1242 refs[0].range.start.line, 1,
1243 "declaration should be on line 1"
1244 );
1245 assert_eq!(
1246 refs[0].range.start.character, 9,
1247 "declaration should start at the function name, not the 'function' keyword"
1248 );
1249 assert_eq!(
1250 refs[0].range.end.character,
1251 refs[0].range.start.character
1252 + "greet".chars().map(|c| c.len_utf16() as u32).sum::<u32>(),
1253 "range should span exactly the function name"
1254 );
1255 }
1256
1257 #[test]
1258 fn class_declaration_span_points_to_name_not_keyword() {
1259 let src = "<?php\nclass MyClass {}";
1260 let docs = vec![doc("/a.php", src)];
1261 let refs = find_references("MyClass", &docs, true, None);
1262 assert_eq!(refs.len(), 1);
1263 assert_eq!(refs[0].range.start.line, 1);
1265 assert_eq!(
1266 refs[0].range.start.character, 6,
1267 "declaration should start at 'MyClass', not 'class'"
1268 );
1269 }
1270
1271 #[test]
1272 fn method_declaration_span_points_to_name_not_keyword() {
1273 let src = "<?php\nclass C {\n public function doThing() {}\n}\n(new C())->doThing();";
1274 let docs = vec![doc("/a.php", src)];
1275 let refs = find_references("doThing", &docs, true, None);
1277 let decl_ref = refs
1279 .iter()
1280 .find(|r| r.range.start.line == 2)
1281 .expect("no declaration ref on line 2");
1282 assert_eq!(
1284 decl_ref.range.start.character, 20,
1285 "method declaration should start at the method name, not 'public function'"
1286 );
1287 }
1288
1289 #[test]
1290 fn method_kind_with_include_declaration_does_not_return_free_function() {
1291 let src =
1301 "<?php\nfunction get() {}\nget();\nclass C { public function get() {} }\n$c->get();";
1302 let docs = vec![doc("/a.php", src)];
1303 let refs = find_references("get", &docs, true, Some(SymbolKind::Method));
1304 let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
1305 assert!(
1306 lines.contains(&3),
1307 "method declaration (line 3) must be present, got: {:?}",
1308 lines
1309 );
1310 assert!(
1311 lines.contains(&4),
1312 "method call (line 4) must be present, got: {:?}",
1313 lines
1314 );
1315 assert!(
1316 !lines.contains(&1),
1317 "free function declaration (line 1) must not appear when kind=Method, got: {:?}",
1318 lines
1319 );
1320 assert!(
1321 !lines.contains(&2),
1322 "free function call (line 2) must not appear when kind=Method, got: {:?}",
1323 lines
1324 );
1325 }
1326
1327 #[test]
1328 fn function_kind_with_include_declaration_does_not_return_method_call() {
1329 let src =
1338 "<?php\nfunction add() {}\nadd();\nclass C { public function add() {} }\n$c->add();";
1339 let docs = vec![doc("/a.php", src)];
1340 let refs = find_references("add", &docs, true, Some(SymbolKind::Function));
1341 let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
1342 assert!(
1343 lines.contains(&1),
1344 "function declaration (line 1) must be present, got: {:?}",
1345 lines
1346 );
1347 assert!(
1348 lines.contains(&2),
1349 "function call (line 2) must be present, got: {:?}",
1350 lines
1351 );
1352 assert!(
1353 !lines.contains(&3),
1354 "method declaration (line 3) must not appear when kind=Function, got: {:?}",
1355 lines
1356 );
1357 assert!(
1358 !lines.contains(&4),
1359 "method call (line 4) must not appear when kind=Function, got: {:?}",
1360 lines
1361 );
1362 }
1363
1364 #[test]
1365 fn interface_method_declaration_included_when_flag_true() {
1366 let src = "<?php\ninterface I {\n public function add(): void;\n}\n$obj->add();";
1376 let docs = vec![doc("/a.php", src)];
1377
1378 let refs = find_references("add", &docs, true, Some(SymbolKind::Method));
1379 let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
1380 assert!(
1381 lines.contains(&2),
1382 "interface method declaration (line 2) must appear with include_declaration=true, got: {:?}",
1383 lines
1384 );
1385 assert!(
1386 lines.contains(&4),
1387 "call site (line 4) must appear, got: {:?}",
1388 lines
1389 );
1390
1391 let refs_no_decl = find_references("add", &docs, false, Some(SymbolKind::Method));
1393 let lines_no_decl: Vec<u32> = refs_no_decl.iter().map(|r| r.range.start.line).collect();
1394 assert!(
1395 !lines_no_decl.contains(&2),
1396 "interface method declaration must be excluded when include_declaration=false, got: {:?}",
1397 lines_no_decl
1398 );
1399 }
1400
1401 #[test]
1402 fn declaration_filter_finds_method_inside_same_named_class() {
1403 let src = "<?php\nclass get { public function get() {} }\n$obj->get();";
1412 let docs = vec![doc("/a.php", src)];
1413
1414 let refs = find_references("get", &docs, false, None);
1417 let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
1418 assert!(
1419 !lines.contains(&1),
1420 "declaration line (1) must not appear when include_declaration=false, got: {:?}",
1421 lines
1422 );
1423 assert!(
1424 lines.contains(&2),
1425 "call site (line 2) must be present, got: {:?}",
1426 lines
1427 );
1428
1429 let refs_with = find_references("get", &docs, true, None);
1432 assert_eq!(
1433 refs_with.len(),
1434 3,
1435 "expected 3 refs (class decl + method decl + call), got: {:?}",
1436 refs_with
1437 );
1438 }
1439
1440 #[test]
1441 fn interface_method_declaration_included_with_kind_none() {
1442 let src = "<?php\ninterface I {\n public function add(): void;\n}\n$obj->add();";
1452 let docs = vec![doc("/a.php", src)];
1453
1454 let refs = find_references("add", &docs, true, None);
1455 let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
1456 assert!(
1457 lines.contains(&2),
1458 "interface method declaration (line 2) must appear with kind=None + include_declaration=true, got: {:?}",
1459 lines
1460 );
1461 }
1462
1463 #[test]
1464 fn interface_method_declaration_excluded_with_kind_none_flag_false() {
1465 let src = "<?php\ninterface I {\n public function add(): void;\n}\n$obj->add();";
1476 let docs = vec![doc("/a.php", src)];
1477
1478 let refs = find_references("add", &docs, false, None);
1479 let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
1480 assert!(
1481 !lines.contains(&2),
1482 "interface method declaration (line 2) must be excluded with kind=None + include_declaration=false, got: {:?}",
1483 lines
1484 );
1485 assert!(
1486 lines.contains(&4),
1487 "call site (line 4) must be present, got: {:?}",
1488 lines
1489 );
1490 }
1491
1492 #[test]
1493 fn function_kind_does_not_include_interface_method_declaration() {
1494 let src =
1505 "<?php\nfunction add() {}\nadd();\ninterface I {\n public function add(): void;\n}";
1506 let docs = vec![doc("/a.php", src)];
1507
1508 let refs = find_references("add", &docs, true, Some(SymbolKind::Function));
1509 let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
1510 assert!(
1511 lines.contains(&1),
1512 "free function declaration (line 1) must be present, got: {:?}",
1513 lines
1514 );
1515 assert!(
1516 lines.contains(&2),
1517 "free function call (line 2) must be present, got: {:?}",
1518 lines
1519 );
1520 assert!(
1521 !lines.contains(&4),
1522 "interface method declaration (line 4) must not appear with kind=Function, got: {:?}",
1523 lines
1524 );
1525 }
1526
1527 #[test]
1530 fn finds_function_call_inside_switch_case() {
1531 let src = "<?php\nfunction tick() {}\nswitch ($x) {\n case 1: tick(); break;\n}";
1534 let docs = vec![doc("/a.php", src)];
1535 let lines: Vec<u32> = find_references("tick", &docs, false, Some(SymbolKind::Function))
1536 .iter()
1537 .map(|r| r.range.start.line)
1538 .collect();
1539 assert!(
1540 lines.contains(&3),
1541 "tick() call inside switch case (line 3) must be present, got: {:?}",
1542 lines
1543 );
1544 }
1545
1546 #[test]
1547 fn finds_method_call_inside_switch_case() {
1548 let src = "<?php\nswitch ($x) {\n case 1: $obj->process(); break;\n}";
1550 let docs = vec![doc("/a.php", src)];
1551 let lines: Vec<u32> = find_references("process", &docs, false, Some(SymbolKind::Method))
1552 .iter()
1553 .map(|r| r.range.start.line)
1554 .collect();
1555 assert!(
1556 lines.contains(&2),
1557 "process() call inside switch case (line 2) must be present, got: {:?}",
1558 lines
1559 );
1560 }
1561
1562 #[test]
1563 fn finds_function_call_inside_switch_condition() {
1564 let src = "<?php\nfunction classify() {}\nswitch (classify()) { default: break; }";
1567 let docs = vec![doc("/a.php", src)];
1568 let lines: Vec<u32> = find_references("classify", &docs, false, Some(SymbolKind::Function))
1569 .iter()
1570 .map(|r| r.range.start.line)
1571 .collect();
1572 assert!(
1573 lines.contains(&2),
1574 "classify() in switch subject (line 2) must be present, got: {:?}",
1575 lines
1576 );
1577 }
1578
1579 #[test]
1580 fn finds_function_call_inside_throw() {
1581 let src = "<?php\nfunction makeException() {}\nthrow makeException();";
1584 let docs = vec![doc("/a.php", src)];
1585 let lines: Vec<u32> =
1586 find_references("makeException", &docs, false, Some(SymbolKind::Function))
1587 .iter()
1588 .map(|r| r.range.start.line)
1589 .collect();
1590 assert!(
1591 lines.contains(&2),
1592 "makeException() inside throw (line 2) must be present, got: {:?}",
1593 lines
1594 );
1595 }
1596
1597 #[test]
1598 fn finds_method_call_inside_throw() {
1599 let src = "<?php\nthrow $factory->create();";
1601 let docs = vec![doc("/a.php", src)];
1602 let lines: Vec<u32> = find_references("create", &docs, false, Some(SymbolKind::Method))
1603 .iter()
1604 .map(|r| r.range.start.line)
1605 .collect();
1606 assert!(
1607 lines.contains(&1),
1608 "create() inside throw (line 1) must be present, got: {:?}",
1609 lines
1610 );
1611 }
1612
1613 #[test]
1614 fn finds_method_call_inside_unset() {
1615 let src = "<?php\nunset($obj->getProp());";
1617 let docs = vec![doc("/a.php", src)];
1618 let lines: Vec<u32> = find_references("getProp", &docs, false, Some(SymbolKind::Method))
1619 .iter()
1620 .map(|r| r.range.start.line)
1621 .collect();
1622 assert!(
1623 lines.contains(&1),
1624 "getProp() inside unset (line 1) must be present, got: {:?}",
1625 lines
1626 );
1627 }
1628
1629 #[test]
1630 fn finds_static_method_call_in_class_property_default() {
1631 let src = "<?php\nclass Config {\n public array $data = self::defaults();\n public static function defaults(): array { return []; }\n}";
1636 let docs = vec![doc("/a.php", src)];
1637 let lines: Vec<u32> = find_references("defaults", &docs, false, Some(SymbolKind::Method))
1638 .iter()
1639 .map(|r| r.range.start.line)
1640 .collect();
1641 assert!(
1642 lines.contains(&2),
1643 "defaults() in class property default (line 2) must be present, got: {:?}",
1644 lines
1645 );
1646 }
1647
1648 #[test]
1649 fn finds_static_method_call_in_trait_property_default() {
1650 let src = "<?php\ntrait T {\n public int $x = self::init();\n public static function init(): int { return 0; }\n}";
1655 let docs = vec![doc("/a.php", src)];
1656 let lines: Vec<u32> = find_references("init", &docs, false, Some(SymbolKind::Method))
1657 .iter()
1658 .map(|r| r.range.start.line)
1659 .collect();
1660 assert!(
1661 lines.contains(&2),
1662 "init() in trait property default (line 2) must be present, got: {:?}",
1663 lines
1664 );
1665 }
1666
1667 fn make_class(
1670 fqcn: &str,
1671 is_final: bool,
1672 method_name: &str,
1673 visibility: mir_codebase::Visibility,
1674 ) -> mir_codebase::ClassStorage {
1675 use indexmap::IndexMap;
1676 let method = mir_codebase::MethodStorage {
1677 name: std::sync::Arc::from(method_name),
1678 fqcn: std::sync::Arc::from(fqcn),
1679 params: vec![],
1680 return_type: None,
1681 inferred_return_type: None,
1682 visibility,
1683 is_static: false,
1684 is_abstract: false,
1685 is_final: false,
1686 is_constructor: false,
1687 template_params: vec![],
1688 assertions: vec![],
1689 throws: vec![],
1690 deprecated: None,
1691 is_internal: false,
1692 is_pure: false,
1693 location: None,
1694 };
1695 let mut methods: IndexMap<
1696 std::sync::Arc<str>,
1697 std::sync::Arc<mir_codebase::MethodStorage>,
1698 > = IndexMap::new();
1699 methods.insert(
1701 std::sync::Arc::from(method_name.to_lowercase().as_str()),
1702 std::sync::Arc::new(method),
1703 );
1704 mir_codebase::ClassStorage {
1705 fqcn: std::sync::Arc::from(fqcn),
1706 short_name: std::sync::Arc::from(fqcn.rsplit('\\').next().unwrap_or(fqcn)),
1707 parent: None,
1708 extends_type_args: vec![],
1709 interfaces: vec![],
1710 traits: vec![],
1711 mixins: vec![],
1712 implements_type_args: vec![],
1713 own_methods: methods,
1714 own_properties: IndexMap::new(),
1715 own_constants: IndexMap::new(),
1716 template_params: vec![],
1717 is_abstract: false,
1718 is_final,
1719 is_readonly: false,
1720 all_parents: vec![],
1721 deprecated: None,
1722 is_internal: false,
1723 type_aliases: std::collections::HashMap::new(),
1724 pending_import_types: vec![],
1725 location: Some(mir_codebase::storage::Location {
1728 file: std::sync::Arc::from("file:///a.php"),
1729 start: 0,
1730 end: 0,
1731 line: 1,
1732 col: 0,
1733 }),
1734 }
1735 }
1736
1737 #[test]
1738 fn codebase_method_falls_back_for_public_method_on_nonfinal_class() {
1739 let cb = mir_codebase::Codebase::new();
1741 cb.classes.insert(
1742 std::sync::Arc::from("Foo"),
1743 make_class("Foo", false, "process", mir_codebase::Visibility::Public),
1744 );
1745 cb.mark_method_referenced_at(
1746 "Foo",
1747 "process",
1748 std::sync::Arc::from("file:///a.php"),
1749 10,
1750 17,
1751 );
1752
1753 let src = "<?php\nclass Foo { public function process() {} }\n$foo->process();";
1754 let docs = vec![doc("/a.php", src)];
1755 let result = find_references_codebase(
1756 "process",
1757 &docs,
1758 false,
1759 Some(SymbolKind::Method),
1760 &cb,
1761 &|k: &str| cb.get_reference_locations(k),
1762 );
1763 assert!(
1764 result.is_none(),
1765 "public method on non-final class must return None (fall back to AST), got: {:?}",
1766 result
1767 );
1768 }
1769
1770 #[test]
1771 fn codebase_method_fast_path_private_method_filters_files() {
1772 let cb = mir_codebase::Codebase::new();
1776 cb.classes.insert(
1777 std::sync::Arc::from("Foo"),
1778 make_class("Foo", false, "execute", mir_codebase::Visibility::Private),
1779 );
1780 cb.mark_method_referenced_at(
1782 "Foo",
1783 "execute",
1784 std::sync::Arc::from("file:///a.php"),
1785 10,
1786 17,
1787 );
1788
1789 let src_a = "<?php\nclass Foo {\n private function execute() {}\n public function run() { $this->execute(); }\n}";
1791 let src_b = "<?php\n$other->execute();";
1793
1794 let docs = vec![doc("/a.php", src_a), doc("/b.php", src_b)];
1795 let result = find_references_codebase(
1796 "execute",
1797 &docs,
1798 false,
1799 Some(SymbolKind::Method),
1800 &cb,
1801 &|k: &str| cb.get_reference_locations(k),
1802 );
1803
1804 assert!(
1805 result.is_some(),
1806 "private method must activate the fast path"
1807 );
1808 let locs = result.unwrap();
1809
1810 let uris: Vec<&str> = locs.iter().map(|l| l.uri.as_str()).collect();
1811 assert!(
1812 uris.iter().all(|u| u.ends_with("/a.php")),
1813 "all results must be from a.php (b.php was not in the codebase index), got: {:?}",
1814 locs
1815 );
1816 assert!(
1817 !locs.is_empty(),
1818 "expected at least the $this->execute() call in a.php, got: {:?}",
1819 locs
1820 );
1821 }
1822
1823 #[test]
1824 fn codebase_method_fast_path_final_class_filters_files() {
1825 let cb = mir_codebase::Codebase::new();
1828 cb.classes.insert(
1829 std::sync::Arc::from("Counter"),
1830 make_class(
1831 "Counter",
1832 true, "increment",
1834 mir_codebase::Visibility::Public,
1835 ),
1836 );
1837 cb.mark_method_referenced_at(
1838 "Counter",
1839 "increment",
1840 std::sync::Arc::from("file:///a.php"),
1841 10,
1842 19,
1843 );
1844
1845 let src_a = "<?php\nfinal class Counter {\n public function increment() {}\n}\n$c = new Counter();\n$c->increment();";
1846 let src_b = "<?php\n$other->increment();";
1847
1848 let docs = vec![doc("/a.php", src_a), doc("/b.php", src_b)];
1849 let result = find_references_codebase(
1850 "increment",
1851 &docs,
1852 false,
1853 Some(SymbolKind::Method),
1854 &cb,
1855 &|k: &str| cb.get_reference_locations(k),
1856 );
1857
1858 assert!(
1859 result.is_some(),
1860 "final class method must activate the fast path"
1861 );
1862 let locs = result.unwrap();
1863
1864 let uris: Vec<&str> = locs.iter().map(|l| l.uri.as_str()).collect();
1865 assert!(
1866 uris.iter().all(|u| u.ends_with("/a.php")),
1867 "all results must be from a.php only, got: {:?}",
1868 locs
1869 );
1870 }
1871
1872 #[test]
1873 fn codebase_method_fast_path_cross_file_reference() {
1874 let cb = mir_codebase::Codebase::new();
1878 cb.classes.insert(
1879 std::sync::Arc::from("Order"),
1880 make_class(
1881 "Order",
1882 true, "submit",
1884 mir_codebase::Visibility::Public,
1885 ),
1886 );
1887 cb.mark_method_referenced_at(
1889 "Order",
1890 "submit",
1891 std::sync::Arc::from("file:///caller.php"),
1892 50,
1893 56,
1894 );
1895
1896 let src_class = "<?php\nfinal class Order {\n public function submit() {}\n}";
1899 let src_caller = "<?php\n$order = new Order();\n$order->submit();";
1901 let src_ignored = "<?php\n$unknown->submit();";
1903
1904 let docs = vec![
1905 doc("/a.php", src_class),
1906 doc("/caller.php", src_caller),
1907 doc("/ignored.php", src_ignored),
1908 ];
1909
1910 let result = find_references_codebase(
1911 "submit",
1912 &docs,
1913 false,
1914 Some(SymbolKind::Method),
1915 &cb,
1916 &|k: &str| cb.get_reference_locations(k),
1917 );
1918
1919 assert!(result.is_some(), "fast path must activate for final class");
1920 let locs = result.unwrap();
1921
1922 let uris: Vec<&str> = locs.iter().map(|l| l.uri.as_str()).collect();
1923 assert!(
1924 uris.iter().any(|u| u.ends_with("/caller.php")),
1925 "caller.php (tracked) must appear in results, got: {:?}",
1926 locs
1927 );
1928 assert!(
1929 !uris.iter().any(|u| u.ends_with("/ignored.php")),
1930 "ignored.php (not tracked) must be excluded, got: {:?}",
1931 locs
1932 );
1933 }
1934
1935 #[test]
1936 fn codebase_method_fast_path_empty_codebase_falls_back() {
1937 let cb = mir_codebase::Codebase::new();
1939 let src = "<?php\n$obj->doWork();";
1940 let docs = vec![doc("/a.php", src)];
1941 let result = find_references_codebase(
1942 "doWork",
1943 &docs,
1944 false,
1945 Some(SymbolKind::Method),
1946 &cb,
1947 &|k: &str| cb.get_reference_locations(k),
1948 );
1949 assert!(
1950 result.is_none(),
1951 "empty codebase must return None for Method kind, got: {:?}",
1952 result
1953 );
1954 }
1955}