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) => {
260 let fqcns: Vec<Arc<str>> = if let Some(t) = target_fqn.filter(|t| t.contains('\\')) {
263 let mut v: Vec<Arc<str>> = Vec::new();
264 if let Some(e) = codebase.classes.get(t) {
265 v.push(e.key().clone());
266 } else if let Some(e) = codebase.interfaces.get(t) {
267 v.push(e.key().clone());
268 } else if let Some(e) = codebase.traits.get(t) {
269 v.push(e.key().clone());
270 } else if let Some(e) = codebase.enums.get(t) {
271 v.push(e.key().clone());
272 } else {
273 return None;
274 }
275 v
276 } else {
277 let mut v: Vec<Arc<str>> = Vec::new();
278 let short_matches =
279 |fqcn: &Arc<str>| fqcn.rsplit('\\').next().unwrap_or(fqcn.as_ref()) == word;
280 for e in codebase.classes.iter() {
281 if short_matches(e.key()) {
282 v.push(e.key().clone());
283 }
284 }
285 for e in codebase.interfaces.iter() {
286 if short_matches(e.key()) {
287 v.push(e.key().clone());
288 }
289 }
290 for e in codebase.traits.iter() {
291 if short_matches(e.key()) {
292 v.push(e.key().clone());
293 }
294 }
295 for e in codebase.enums.iter() {
296 if short_matches(e.key()) {
297 v.push(e.key().clone());
298 }
299 }
300 v
301 };
302
303 if fqcns.is_empty() {
304 return None;
305 }
306
307 let mut call_site_count = 0usize;
308 let mut locations: Vec<Location> = Vec::new();
309 for fqcn in &fqcns {
310 for (file, start, end) in lookup_refs(fqcn) {
311 if let Some(loc) = spans_to_location(&file, start, end) {
312 locations.push(loc);
313 call_site_count += 1;
314 }
315 }
316 if include_declaration
317 && let Some(decl) = codebase.get_symbol_location(fqcn)
318 && let Some(loc) = spans_to_location(&decl.file, decl.start, decl.end)
319 {
320 locations.push(loc);
321 }
322 }
323 if call_site_count == 0 {
324 return None;
325 }
326 Some(locations)
327 }
328
329 Some(SymbolKind::Method) => {
330 let word_lower = word.to_lowercase();
331
332 let user_code_uris: HashSet<&str> =
336 all_docs.iter().map(|(url, _)| url.as_str()).collect();
337 let is_user_code = |loc: &Option<mir_codebase::storage::Location>| -> bool {
338 loc.as_ref()
339 .is_some_and(|l| user_code_uris.contains(l.file.as_ref()))
340 };
341
342 let mut method_keys: Vec<String> = Vec::new();
343 let mut candidate_arcs: Vec<Arc<str>> = Vec::new();
344
345 if let Some(owner_fqcn) = target_fqn {
346 let mut owners: Vec<Arc<str>> = Vec::new();
352
353 if let Some(entry) = codebase.classes.get(owner_fqcn) {
354 owners.push(entry.key().clone());
355 for e in codebase.classes.iter() {
356 if e.value()
357 .all_parents
358 .iter()
359 .any(|p| p.as_ref() == owner_fqcn)
360 {
361 owners.push(e.key().clone());
362 }
363 }
364 } else if let Some(entry) = codebase.enums.get(owner_fqcn) {
365 owners.push(entry.key().clone());
366 } else if let Some(entry) = codebase.interfaces.get(owner_fqcn) {
367 owners.push(entry.key().clone());
368 for e in codebase.classes.iter() {
369 if e.value()
370 .interfaces
371 .iter()
372 .any(|i| i.as_ref() == owner_fqcn)
373 {
374 owners.push(e.key().clone());
375 }
376 }
377 } else if let Some(entry) = codebase.traits.get(owner_fqcn) {
378 owners.push(entry.key().clone());
379 for e in codebase.classes.iter() {
380 if e.value().traits.iter().any(|t| t.as_ref() == owner_fqcn) {
381 owners.push(e.key().clone());
382 }
383 }
384 } else {
385 return None;
386 }
387
388 let mut call_site_count = 0usize;
392 let mut locations: Vec<Location> = Vec::new();
393 for owner in &owners {
394 let key = format!("{}::{}", owner, word_lower);
395 for (file, start, end) in lookup_refs(&key) {
396 if let Some(loc) = spans_to_location(&file, start, end) {
397 locations.push(loc);
398 call_site_count += 1;
399 }
400 }
401 }
402 if call_site_count == 0 {
407 return None;
408 }
409
410 if include_declaration {
411 for owner in &owners {
416 let decl_file =
417 codebase
418 .classes
419 .get(owner.as_ref())
420 .and_then(|e| {
421 e.value()
422 .own_methods
423 .get(word_lower.as_str())
424 .and_then(|m| m.location.as_ref().map(|l| l.file.clone()))
425 })
426 .or_else(|| {
427 codebase.enums.get(owner.as_ref()).and_then(|e| {
428 e.value().own_methods.get(word_lower.as_str()).and_then(
429 |m| m.location.as_ref().map(|l| l.file.clone()),
430 )
431 })
432 })
433 .or_else(|| {
434 codebase.interfaces.get(owner.as_ref()).and_then(|e| {
435 e.value().own_methods.get(word_lower.as_str()).and_then(
436 |m| m.location.as_ref().map(|l| l.file.clone()),
437 )
438 })
439 })
440 .or_else(|| {
441 codebase.traits.get(owner.as_ref()).and_then(|e| {
442 e.value().own_methods.get(word_lower.as_str()).and_then(
443 |m| m.location.as_ref().map(|l| l.file.clone()),
444 )
445 })
446 });
447 let Some(decl_file) = decl_file else { continue };
448 let Some((url, doc)) = all_docs
449 .iter()
450 .find(|(u, _)| u.as_str() == decl_file.as_ref())
451 else {
452 continue;
453 };
454 let short = owner.rsplit('\\').next().unwrap_or(owner.as_ref());
458 let mut spans: Vec<Span> = Vec::new();
459 collect_method_decls_in_class(
460 doc.source(),
461 &doc.program().stmts,
462 short,
463 word,
464 &mut spans,
465 );
466 let sv = doc.view();
467 let word_utf16_len: u32 = word.chars().map(|c| c.len_utf16() as u32).sum();
468 for span in spans {
469 let start = sv.position_of(span.start);
470 let end = Position {
471 line: start.line,
472 character: start.character + word_utf16_len,
473 };
474 locations.push(Location {
475 uri: (*url).clone(),
476 range: Range { start, end },
477 });
478 }
479 }
480 }
481
482 return if locations.is_empty() {
483 None
484 } else {
485 Some(locations)
486 };
487 } else {
488 for entry in codebase.classes.iter() {
491 let cls = entry.value();
492 if !is_user_code(&cls.location) {
493 continue;
494 }
495 if let Some(method) = cls.own_methods.get(word_lower.as_str())
496 && (cls.is_final || method.visibility == mir_codebase::Visibility::Private)
497 {
498 method_keys.push(format!("{}::{}", entry.key(), word_lower));
499 if include_declaration && let Some(loc) = &method.location {
500 candidate_arcs.push(loc.file.clone());
501 }
502 }
503 }
504 for entry in codebase.enums.iter() {
505 let enm = entry.value();
506 if !is_user_code(&enm.location) {
507 continue;
508 }
509 if let Some(method) = enm.own_methods.get(word_lower.as_str())
510 && method.visibility == mir_codebase::Visibility::Private
511 {
512 method_keys.push(format!("{}::{}", entry.key(), word_lower));
513 if include_declaration && let Some(loc) = &method.location {
514 candidate_arcs.push(loc.file.clone());
515 }
516 }
517 }
518
519 if method_keys.is_empty() {
520 return None;
521 }
522 }
523
524 for key in &method_keys {
526 for (file, _, _) in lookup_refs(key) {
527 candidate_arcs.push(file);
528 }
529 }
530 let candidate_uris: HashSet<&str> = candidate_arcs.iter().map(|a| a.as_ref()).collect();
531
532 let candidate_docs: Vec<(Url, Arc<ParsedDoc>)> = all_docs
534 .iter()
535 .filter(|(url, _)| candidate_uris.contains(url.as_str()))
536 .cloned()
537 .collect();
538
539 let locations = find_references_inner(
540 word,
541 &candidate_docs,
542 include_declaration,
543 false,
544 Some(SymbolKind::Method),
545 None,
546 );
547 Some(locations)
548 }
549
550 None => None,
552
553 Some(SymbolKind::Property) => None,
556 }
557}
558
559fn find_references_inner(
560 word: &str,
561 all_docs: &[(Url, Arc<ParsedDoc>)],
562 include_declaration: bool,
563 include_use: bool,
564 kind: Option<SymbolKind>,
565 target_fqn: Option<&str>,
566) -> Vec<Location> {
567 let namespace_filter_active =
576 matches!(kind, Some(SymbolKind::Function) | Some(SymbolKind::Class));
577 all_docs
578 .par_iter()
579 .flat_map_iter(|(uri, doc)| {
580 if namespace_filter_active
581 && let Some(target) = target_fqn
582 && !doc_can_reference_target(doc, word, target)
583 {
584 return Vec::new();
585 }
586 scan_doc(word, uri, doc, include_declaration, include_use, kind)
587 })
588 .collect()
589}
590
591fn doc_can_reference_target(doc: &ParsedDoc, word: &str, target_fqn: &str) -> bool {
596 let target = target_fqn.trim_start_matches('\\');
597 let imports = collect_file_imports(doc);
598 let resolved = crate::moniker::resolve_fqn(doc, word, &imports);
599 resolved == target
604 || (resolved == word && !target.contains('\\'))
605 || (resolved == word && target == format!("\\{word}"))
606}
607
608fn collect_file_imports(doc: &ParsedDoc) -> std::collections::HashMap<String, String> {
612 let mut out = std::collections::HashMap::new();
613 fn walk(stmts: &[Stmt<'_, '_>], out: &mut std::collections::HashMap<String, String>) {
614 for stmt in stmts {
615 match &stmt.kind {
616 StmtKind::Use(u) => {
617 for item in u.uses.iter() {
618 let fqn = item.name.to_string_repr().into_owned();
619 let short = item
620 .alias
621 .map(|a| a.to_string())
622 .unwrap_or_else(|| fqn.rsplit('\\').next().unwrap_or(&fqn).to_string());
623 out.insert(short, fqn);
624 }
625 }
626 StmtKind::Namespace(ns) => {
627 if let NamespaceBody::Braced(inner) = &ns.body {
628 walk(inner, out);
629 }
630 }
631 _ => {}
632 }
633 }
634 }
635 walk(&doc.program().stmts, &mut out);
636 out
637}
638
639fn scan_doc(
640 word: &str,
641 uri: &Url,
642 doc: &Arc<ParsedDoc>,
643 include_declaration: bool,
644 include_use: bool,
645 kind: Option<SymbolKind>,
646) -> Vec<Location> {
647 let source = doc.source();
648 if !source.contains(word) {
653 return Vec::new();
654 }
655 let stmts = &doc.program().stmts;
656 let mut spans = Vec::new();
657
658 if include_use {
659 refs_in_stmts_with_use(source, stmts, word, &mut spans);
661 if !include_declaration {
662 let mut decl_spans = Vec::new();
663 collect_declaration_spans(source, stmts, word, None, &mut decl_spans);
664 let decl_set: HashSet<(u32, u32)> =
665 decl_spans.iter().map(|s| (s.start, s.end)).collect();
666 spans.retain(|span| !decl_set.contains(&(span.start, span.end)));
667 }
668 } else {
669 match kind {
670 Some(SymbolKind::Function) => function_refs_in_stmts(stmts, word, &mut spans),
671 Some(SymbolKind::Method) => method_refs_in_stmts(stmts, word, &mut spans),
672 Some(SymbolKind::Class) => class_refs_in_stmts(stmts, word, &mut spans),
673 Some(SymbolKind::Property) => {
676 property_refs_in_stmts(source, stmts, word, &mut spans);
677 if !include_declaration {
678 let mut decl_spans = Vec::new();
679 collect_declaration_spans(
680 source,
681 stmts,
682 word,
683 Some(SymbolKind::Property),
684 &mut decl_spans,
685 );
686 let decl_set: HashSet<(u32, u32)> =
687 decl_spans.iter().map(|s| (s.start, s.end)).collect();
688 spans.retain(|span| !decl_set.contains(&(span.start, span.end)));
689 }
690 }
691 None => {
693 refs_in_stmts(source, stmts, word, &mut spans);
694 if !include_declaration {
695 let mut decl_spans = Vec::new();
696 collect_declaration_spans(source, stmts, word, None, &mut decl_spans);
697 let decl_set: HashSet<(u32, u32)> =
698 decl_spans.iter().map(|s| (s.start, s.end)).collect();
699 spans.retain(|span| !decl_set.contains(&(span.start, span.end)));
700 }
701 }
702 }
703 if include_declaration
708 && matches!(
709 kind,
710 Some(SymbolKind::Function) | Some(SymbolKind::Method) | Some(SymbolKind::Class)
711 )
712 {
713 collect_declaration_spans(source, stmts, word, kind, &mut spans);
714 }
715 }
716
717 let sv = doc.view();
718 let word_utf16_len: u32 = word.chars().map(|c| c.len_utf16() as u32).sum();
719 spans
720 .into_iter()
721 .map(|span| {
722 let start = sv.position_of(span.start);
723 let end = Position {
724 line: start.line,
725 character: start.character + word_utf16_len,
726 };
727 Location {
728 uri: uri.clone(),
729 range: Range { start, end },
730 }
731 })
732 .collect()
733}
734
735fn declaration_name_span(source: &str, name: &str) -> Span {
737 let start = str_offset(source, name);
738 Span {
739 start,
740 end: start + name.len() as u32,
741 }
742}
743
744fn collect_method_decls_in_class(
750 source: &str,
751 stmts: &[Stmt<'_, '_>],
752 class_short: &str,
753 method_word: &str,
754 out: &mut Vec<Span>,
755) {
756 for stmt in stmts {
757 match &stmt.kind {
758 StmtKind::Class(c) if c.name == Some(class_short) => {
759 for member in c.members.iter() {
760 if let ClassMemberKind::Method(m) = &member.kind
761 && m.name == method_word
762 {
763 out.push(declaration_name_span(source, m.name));
764 }
765 }
766 }
767 StmtKind::Interface(i) if i.name == class_short => {
768 for member in i.members.iter() {
769 if let ClassMemberKind::Method(m) = &member.kind
770 && m.name == method_word
771 {
772 out.push(declaration_name_span(source, m.name));
773 }
774 }
775 }
776 StmtKind::Trait(t) if t.name == class_short => {
777 for member in t.members.iter() {
778 if let ClassMemberKind::Method(m) = &member.kind
779 && m.name == method_word
780 {
781 out.push(declaration_name_span(source, m.name));
782 }
783 }
784 }
785 StmtKind::Enum(e) if e.name == class_short => {
786 for member in e.members.iter() {
787 if let EnumMemberKind::Method(m) = &member.kind
788 && m.name == method_word
789 {
790 out.push(declaration_name_span(source, m.name));
791 }
792 }
793 }
794 StmtKind::Namespace(ns) => {
795 if let NamespaceBody::Braced(inner) = &ns.body {
796 collect_method_decls_in_class(source, inner, class_short, method_word, out);
797 }
798 }
799 _ => {}
800 }
801 }
802}
803
804fn collect_declaration_spans(
813 source: &str,
814 stmts: &[Stmt<'_, '_>],
815 word: &str,
816 kind: Option<SymbolKind>,
817 out: &mut Vec<Span>,
818) {
819 let want_free = matches!(kind, None | Some(SymbolKind::Function));
820 let want_method = matches!(kind, None | Some(SymbolKind::Method));
821 let want_type = matches!(kind, None | Some(SymbolKind::Class));
822 let want_property = matches!(kind, None | Some(SymbolKind::Property));
823
824 for stmt in stmts {
825 match &stmt.kind {
826 StmtKind::Function(f) => {
827 if want_free && f.name == word {
828 out.push(declaration_name_span(source, f.name));
829 }
830 }
831 StmtKind::Class(c) => {
832 if want_type
833 && let Some(name) = c.name
834 && name == word
835 {
836 out.push(declaration_name_span(source, name));
837 }
838 if want_method || want_property {
839 for member in c.members.iter() {
840 match &member.kind {
841 ClassMemberKind::Method(m) if want_method && m.name == word => {
842 out.push(declaration_name_span(source, m.name));
843 }
844 ClassMemberKind::Method(m)
845 if want_property && m.name == "__construct" =>
846 {
847 for p in m.params.iter() {
849 if p.visibility.is_some() && p.name == word {
850 out.push(declaration_name_span(source, p.name));
851 }
852 }
853 }
854 ClassMemberKind::Property(p) if want_property && p.name == word => {
855 out.push(declaration_name_span(source, p.name));
856 }
857 _ => {}
858 }
859 }
860 }
861 }
862 StmtKind::Interface(i) => {
863 if want_type && i.name == word {
864 out.push(declaration_name_span(source, i.name));
865 }
866 if want_method {
867 for member in i.members.iter() {
868 if let ClassMemberKind::Method(m) = &member.kind
869 && m.name == word
870 {
871 out.push(declaration_name_span(source, m.name));
872 }
873 }
874 }
875 }
876 StmtKind::Trait(t) => {
877 if want_type && t.name == word {
878 out.push(declaration_name_span(source, t.name));
879 }
880 if want_method || want_property {
881 for member in t.members.iter() {
882 match &member.kind {
883 ClassMemberKind::Method(m) if want_method && m.name == word => {
884 out.push(declaration_name_span(source, m.name));
885 }
886 ClassMemberKind::Property(p) if want_property && p.name == word => {
887 out.push(declaration_name_span(source, p.name));
888 }
889 _ => {}
890 }
891 }
892 }
893 }
894 StmtKind::Enum(e) => {
895 if want_type && e.name == word {
896 out.push(declaration_name_span(source, e.name));
897 }
898 for member in e.members.iter() {
899 match &member.kind {
900 EnumMemberKind::Method(m) if want_method && m.name == word => {
901 out.push(declaration_name_span(source, m.name));
902 }
903 EnumMemberKind::Case(c) if want_type && c.name == word => {
904 out.push(declaration_name_span(source, c.name));
905 }
906 _ => {}
907 }
908 }
909 }
910 StmtKind::Namespace(ns) => {
911 if let NamespaceBody::Braced(inner) = &ns.body {
912 collect_declaration_spans(source, inner, word, kind, out);
913 }
914 }
915 _ => {}
916 }
917 }
918}
919
920#[cfg(test)]
921mod tests {
922 use super::*;
923
924 fn uri(path: &str) -> Url {
925 Url::parse(&format!("file://{path}")).unwrap()
926 }
927
928 fn doc(path: &str, source: &str) -> (Url, Arc<ParsedDoc>) {
929 (uri(path), Arc::new(ParsedDoc::parse(source.to_string())))
930 }
931
932 #[test]
933 fn finds_function_call_reference() {
934 let src = "<?php\nfunction greet() {}\ngreet();\ngreet();";
935 let docs = vec![doc("/a.php", src)];
936 let refs = find_references("greet", &docs, false, None);
937 assert_eq!(refs.len(), 2, "expected 2 call-site refs, got {:?}", refs);
938 }
939
940 #[test]
941 fn include_declaration_adds_def_site() {
942 let src = "<?php\nfunction greet() {}\ngreet();";
943 let docs = vec![doc("/a.php", src)];
944 let with_decl = find_references("greet", &docs, true, None);
945 let without_decl = find_references("greet", &docs, false, None);
946 assert_eq!(
948 without_decl.len(),
949 1,
950 "expected 1 call-site ref without declaration"
951 );
952 assert_eq!(
953 without_decl[0].range.start.line, 2,
954 "call site should be on line 2"
955 );
956 assert_eq!(
958 with_decl.len(),
959 2,
960 "expected 2 refs with declaration included"
961 );
962 }
963
964 #[test]
965 fn finds_new_expression_reference() {
966 let src = "<?php\nclass Foo {}\n$x = new Foo();";
967 let docs = vec![doc("/a.php", src)];
968 let refs = find_references("Foo", &docs, false, None);
969 assert_eq!(
970 refs.len(),
971 1,
972 "expected exactly 1 reference to Foo in new expr"
973 );
974 assert_eq!(
975 refs[0].range.start.line, 2,
976 "new Foo() reference should be on line 2"
977 );
978 }
979
980 #[test]
981 fn finds_reference_in_nested_function_call() {
982 let src = "<?php\nfunction greet() {}\necho(greet());";
983 let docs = vec![doc("/a.php", src)];
984 let refs = find_references("greet", &docs, false, None);
985 assert_eq!(
986 refs.len(),
987 1,
988 "expected exactly 1 nested function call reference"
989 );
990 assert_eq!(
991 refs[0].range.start.line, 2,
992 "nested greet() call should be on line 2"
993 );
994 }
995
996 #[test]
997 fn finds_references_across_multiple_docs() {
998 let a = doc("/a.php", "<?php\nfunction helper() {}");
999 let b = doc("/b.php", "<?php\nhelper();\nhelper();");
1000 let refs = find_references("helper", &[a, b], false, None);
1001 assert_eq!(refs.len(), 2, "expected 2 cross-file references");
1002 assert!(refs.iter().all(|r| r.uri.path().ends_with("/b.php")));
1003 }
1004
1005 #[test]
1006 fn finds_method_call_reference() {
1007 let src = "<?php\nclass Calc { public function add() {} }\n$c = new Calc();\n$c->add();";
1008 let docs = vec![doc("/a.php", src)];
1009 let refs = find_references("add", &docs, false, None);
1010 assert_eq!(
1011 refs.len(),
1012 1,
1013 "expected exactly 1 method call reference to 'add'"
1014 );
1015 assert_eq!(
1016 refs[0].range.start.line, 3,
1017 "add() call should be on line 3"
1018 );
1019 }
1020
1021 #[test]
1022 fn finds_reference_inside_if_body() {
1023 let src = "<?php\nfunction check() {}\nif (true) { check(); }";
1024 let docs = vec![doc("/a.php", src)];
1025 let refs = find_references("check", &docs, false, None);
1026 assert_eq!(refs.len(), 1, "expected exactly 1 reference inside if body");
1027 assert_eq!(
1028 refs[0].range.start.line, 2,
1029 "check() inside if should be on line 2"
1030 );
1031 }
1032
1033 #[test]
1034 fn finds_use_statement_reference() {
1035 let src = "<?php\nuse MyClass;\n$x = new MyClass();";
1038 let docs = vec![doc("/a.php", src)];
1039 let refs = find_references_with_use("MyClass", &docs, false);
1040 assert_eq!(
1042 refs.len(),
1043 2,
1044 "expected exactly 2 references, got: {:?}",
1045 refs
1046 );
1047 let mut lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
1048 lines.sort_unstable();
1049 assert_eq!(
1050 lines,
1051 vec![1, 2],
1052 "references should be on lines 1 (use) and 2 (new)"
1053 );
1054 }
1055
1056 #[test]
1057 fn find_references_returns_correct_lines() {
1058 let src = "<?php\nhelper();\nhelper();\nfunction helper() {}";
1060 let docs = vec![doc("/a.php", src)];
1061 let refs = find_references("helper", &docs, false, None);
1062 assert_eq!(refs.len(), 2, "expected exactly 2 call-site references");
1063 let mut lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
1064 lines.sort_unstable();
1065 assert_eq!(lines, vec![1, 2], "references should be on lines 1 and 2");
1066 }
1067
1068 #[test]
1069 fn declaration_excluded_when_flag_false() {
1070 let src = "<?php\nfunction doWork() {}\ndoWork();\ndoWork();";
1072 let docs = vec![doc("/a.php", src)];
1073 let refs = find_references("doWork", &docs, false, None);
1074 let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
1076 assert!(
1077 !lines.contains(&1),
1078 "declaration line (1) must not appear when include_declaration=false, got: {:?}",
1079 lines
1080 );
1081 assert_eq!(refs.len(), 2, "expected 2 call-site references only");
1082 }
1083
1084 #[test]
1085 fn partial_match_not_included() {
1086 let src = "<?php\nfunction greet() {}\nfunction greeting() {}\ngreet();\ngreeting();";
1088 let docs = vec![doc("/a.php", src)];
1089 let refs = find_references("greet", &docs, false, None);
1090 for r in &refs {
1092 let span_len = r.range.end.character - r.range.start.character;
1095 assert_eq!(
1096 span_len, 5,
1097 "reference span length should equal len('greet')=5, got {} at {:?}",
1098 span_len, r
1099 );
1100 }
1101 assert_eq!(
1103 refs.len(),
1104 1,
1105 "expected exactly 1 reference to 'greet' (not 'greeting'), got: {:?}",
1106 refs
1107 );
1108 }
1109
1110 #[test]
1111 fn finds_reference_in_class_property_default() {
1112 let src = "<?php\nclass Foo {\n public string $status = Status::ACTIVE;\n}";
1114 let docs = vec![doc("/a.php", src)];
1115 let refs = find_references("Status", &docs, false, None);
1116 assert_eq!(
1117 refs.len(),
1118 1,
1119 "expected exactly 1 reference to Status in property default, got: {:?}",
1120 refs
1121 );
1122 assert_eq!(refs[0].range.start.line, 2, "reference should be on line 2");
1123 }
1124
1125 #[test]
1126 fn class_const_access_span_covers_only_member_name() {
1127 let src = "<?php\n$x = Status::ACTIVE;";
1133 let docs = vec![doc("/a.php", src)];
1134 let refs = find_references("ACTIVE", &docs, false, None);
1135 assert_eq!(refs.len(), 1, "expected 1 reference, got: {:?}", refs);
1136 let r = &refs[0].range;
1137 assert_eq!(r.start.line, 1, "reference must be on line 1");
1138 assert_eq!(
1141 r.start.character, 13,
1142 "range must start at 'ACTIVE' (char 13), not at 'Status' (char 5); got {:?}",
1143 r
1144 );
1145 }
1146
1147 #[test]
1148 fn class_const_access_no_duplicate_when_name_equals_class() {
1149 let src = "<?php\n$x = Status::Status;";
1159 let docs = vec![doc("/a.php", src)];
1160 let refs = find_references("Status", &docs, false, None);
1161 assert_eq!(
1162 refs.len(),
1163 2,
1164 "expected exactly 2 refs (class side + member side), got: {:?}",
1165 refs
1166 );
1167 let mut chars: Vec<u32> = refs.iter().map(|r| r.range.start.character).collect();
1168 chars.sort_unstable();
1169 assert_eq!(
1170 chars,
1171 vec![5, 13],
1172 "class-side ref must be at char 5 and member-side at char 13, got: {:?}",
1173 refs
1174 );
1175 }
1176
1177 #[test]
1178 fn finds_reference_inside_enum_method_body() {
1179 let src = "<?php\nfunction helper() {}\nenum Status {\n public function label(): string { return helper(); }\n}";
1181 let docs = vec![doc("/a.php", src)];
1182 let refs = find_references("helper", &docs, false, None);
1183 assert_eq!(
1184 refs.len(),
1185 1,
1186 "expected exactly 1 reference to helper() inside enum method, got: {:?}",
1187 refs
1188 );
1189 assert_eq!(refs[0].range.start.line, 3, "reference should be on line 3");
1190 }
1191
1192 #[test]
1193 fn finds_reference_in_for_init_and_update() {
1194 let src = "<?php\nfunction tick() {}\nfor (tick(); $i < 10; tick()) {}";
1196 let docs = vec![doc("/a.php", src)];
1197 let refs = find_references("tick", &docs, false, None);
1198 assert_eq!(
1199 refs.len(),
1200 2,
1201 "expected exactly 2 references to tick() (init + update), got: {:?}",
1202 refs
1203 );
1204 assert!(refs.iter().all(|r| r.range.start.line == 2));
1206 }
1207
1208 #[test]
1211 fn function_kind_skips_method_call_with_same_name() {
1212 let src = "<?php\nfunction get() {}\nget();\n$obj->get();";
1214 let docs = vec![doc("/a.php", src)];
1215 let refs = find_references("get", &docs, false, Some(SymbolKind::Function));
1216 assert_eq!(
1218 refs.len(),
1219 1,
1220 "expected 1 free-function ref, got: {:?}",
1221 refs
1222 );
1223 assert_eq!(refs[0].range.start.line, 2);
1224 }
1225
1226 #[test]
1227 fn method_kind_skips_free_function_call_with_same_name() {
1228 let src = "<?php\nfunction add() {}\nadd();\n$calc->add();";
1230 let docs = vec![doc("/a.php", src)];
1231 let refs = find_references("add", &docs, false, Some(SymbolKind::Method));
1232 assert_eq!(refs.len(), 1, "expected 1 method ref, got: {:?}", refs);
1234 assert_eq!(refs[0].range.start.line, 3);
1235 }
1236
1237 #[test]
1238 fn class_kind_finds_new_expression() {
1239 let src = "<?php\nclass Foo {}\n$x = new Foo();\nFoo();";
1241 let docs = vec![doc("/a.php", src)];
1242 let refs = find_references("Foo", &docs, false, Some(SymbolKind::Class));
1243 let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
1245 assert!(
1246 lines.contains(&2),
1247 "expected new Foo() on line 2, got: {:?}",
1248 refs
1249 );
1250 assert!(
1251 !lines.contains(&3),
1252 "free call Foo() should not appear as class ref, got: {:?}",
1253 refs
1254 );
1255 }
1256
1257 #[test]
1258 fn class_kind_finds_extends_and_implements() {
1259 let src = "<?php\nclass Base {}\ninterface Iface {}\nclass Child extends Base implements Iface {}";
1260 let docs = vec![doc("/a.php", src)];
1261
1262 let base_refs = find_references("Base", &docs, false, Some(SymbolKind::Class));
1263 let lines_base: Vec<u32> = base_refs.iter().map(|r| r.range.start.line).collect();
1264 assert!(
1265 lines_base.contains(&3),
1266 "expected extends Base on line 3, got: {:?}",
1267 base_refs
1268 );
1269
1270 let iface_refs = find_references("Iface", &docs, false, Some(SymbolKind::Class));
1271 let lines_iface: Vec<u32> = iface_refs.iter().map(|r| r.range.start.line).collect();
1272 assert!(
1273 lines_iface.contains(&3),
1274 "expected implements Iface on line 3, got: {:?}",
1275 iface_refs
1276 );
1277 }
1278
1279 #[test]
1280 fn class_kind_finds_type_hint() {
1281 let src = "<?php\nclass Foo {}\nfunction take(Foo $x): void {}";
1283 let docs = vec![doc("/a.php", src)];
1284 let refs = find_references("Foo", &docs, false, Some(SymbolKind::Class));
1285 let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
1286 assert!(
1287 lines.contains(&2),
1288 "expected type hint Foo on line 2, got: {:?}",
1289 refs
1290 );
1291 }
1292
1293 #[test]
1296 fn function_declaration_span_points_to_name_not_keyword() {
1297 let src = "<?php\nfunction greet() {}";
1300 let docs = vec![doc("/a.php", src)];
1301 let refs = find_references("greet", &docs, true, None);
1302 assert_eq!(refs.len(), 1, "expected exactly 1 ref (the declaration)");
1303 assert_eq!(
1306 refs[0].range.start.line, 1,
1307 "declaration should be on line 1"
1308 );
1309 assert_eq!(
1310 refs[0].range.start.character, 9,
1311 "declaration should start at the function name, not the 'function' keyword"
1312 );
1313 assert_eq!(
1314 refs[0].range.end.character,
1315 refs[0].range.start.character
1316 + "greet".chars().map(|c| c.len_utf16() as u32).sum::<u32>(),
1317 "range should span exactly the function name"
1318 );
1319 }
1320
1321 #[test]
1322 fn class_declaration_span_points_to_name_not_keyword() {
1323 let src = "<?php\nclass MyClass {}";
1324 let docs = vec![doc("/a.php", src)];
1325 let refs = find_references("MyClass", &docs, true, None);
1326 assert_eq!(refs.len(), 1);
1327 assert_eq!(refs[0].range.start.line, 1);
1329 assert_eq!(
1330 refs[0].range.start.character, 6,
1331 "declaration should start at 'MyClass', not 'class'"
1332 );
1333 }
1334
1335 #[test]
1336 fn method_declaration_span_points_to_name_not_keyword() {
1337 let src = "<?php\nclass C {\n public function doThing() {}\n}\n(new C())->doThing();";
1338 let docs = vec![doc("/a.php", src)];
1339 let refs = find_references("doThing", &docs, true, None);
1341 let decl_ref = refs
1343 .iter()
1344 .find(|r| r.range.start.line == 2)
1345 .expect("no declaration ref on line 2");
1346 assert_eq!(
1348 decl_ref.range.start.character, 20,
1349 "method declaration should start at the method name, not 'public function'"
1350 );
1351 }
1352
1353 #[test]
1354 fn method_kind_with_include_declaration_does_not_return_free_function() {
1355 let src =
1365 "<?php\nfunction get() {}\nget();\nclass C { public function get() {} }\n$c->get();";
1366 let docs = vec![doc("/a.php", src)];
1367 let refs = find_references("get", &docs, true, Some(SymbolKind::Method));
1368 let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
1369 assert!(
1370 lines.contains(&3),
1371 "method declaration (line 3) must be present, got: {:?}",
1372 lines
1373 );
1374 assert!(
1375 lines.contains(&4),
1376 "method call (line 4) must be present, got: {:?}",
1377 lines
1378 );
1379 assert!(
1380 !lines.contains(&1),
1381 "free function declaration (line 1) must not appear when kind=Method, got: {:?}",
1382 lines
1383 );
1384 assert!(
1385 !lines.contains(&2),
1386 "free function call (line 2) must not appear when kind=Method, got: {:?}",
1387 lines
1388 );
1389 }
1390
1391 #[test]
1392 fn function_kind_with_include_declaration_does_not_return_method_call() {
1393 let src =
1402 "<?php\nfunction add() {}\nadd();\nclass C { public function add() {} }\n$c->add();";
1403 let docs = vec![doc("/a.php", src)];
1404 let refs = find_references("add", &docs, true, Some(SymbolKind::Function));
1405 let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
1406 assert!(
1407 lines.contains(&1),
1408 "function declaration (line 1) must be present, got: {:?}",
1409 lines
1410 );
1411 assert!(
1412 lines.contains(&2),
1413 "function call (line 2) must be present, got: {:?}",
1414 lines
1415 );
1416 assert!(
1417 !lines.contains(&3),
1418 "method declaration (line 3) must not appear when kind=Function, got: {:?}",
1419 lines
1420 );
1421 assert!(
1422 !lines.contains(&4),
1423 "method call (line 4) must not appear when kind=Function, got: {:?}",
1424 lines
1425 );
1426 }
1427
1428 #[test]
1429 fn interface_method_declaration_included_when_flag_true() {
1430 let src = "<?php\ninterface I {\n public function add(): void;\n}\n$obj->add();";
1440 let docs = vec![doc("/a.php", src)];
1441
1442 let refs = find_references("add", &docs, true, Some(SymbolKind::Method));
1443 let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
1444 assert!(
1445 lines.contains(&2),
1446 "interface method declaration (line 2) must appear with include_declaration=true, got: {:?}",
1447 lines
1448 );
1449 assert!(
1450 lines.contains(&4),
1451 "call site (line 4) must appear, got: {:?}",
1452 lines
1453 );
1454
1455 let refs_no_decl = find_references("add", &docs, false, Some(SymbolKind::Method));
1457 let lines_no_decl: Vec<u32> = refs_no_decl.iter().map(|r| r.range.start.line).collect();
1458 assert!(
1459 !lines_no_decl.contains(&2),
1460 "interface method declaration must be excluded when include_declaration=false, got: {:?}",
1461 lines_no_decl
1462 );
1463 }
1464
1465 #[test]
1466 fn declaration_filter_finds_method_inside_same_named_class() {
1467 let src = "<?php\nclass get { public function get() {} }\n$obj->get();";
1476 let docs = vec![doc("/a.php", src)];
1477
1478 let refs = find_references("get", &docs, false, None);
1481 let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
1482 assert!(
1483 !lines.contains(&1),
1484 "declaration line (1) must not appear when include_declaration=false, got: {:?}",
1485 lines
1486 );
1487 assert!(
1488 lines.contains(&2),
1489 "call site (line 2) must be present, got: {:?}",
1490 lines
1491 );
1492
1493 let refs_with = find_references("get", &docs, true, None);
1496 assert_eq!(
1497 refs_with.len(),
1498 3,
1499 "expected 3 refs (class decl + method decl + call), got: {:?}",
1500 refs_with
1501 );
1502 }
1503
1504 #[test]
1505 fn interface_method_declaration_included_with_kind_none() {
1506 let src = "<?php\ninterface I {\n public function add(): void;\n}\n$obj->add();";
1516 let docs = vec![doc("/a.php", src)];
1517
1518 let refs = find_references("add", &docs, true, None);
1519 let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
1520 assert!(
1521 lines.contains(&2),
1522 "interface method declaration (line 2) must appear with kind=None + include_declaration=true, got: {:?}",
1523 lines
1524 );
1525 }
1526
1527 #[test]
1528 fn interface_method_declaration_excluded_with_kind_none_flag_false() {
1529 let src = "<?php\ninterface I {\n public function add(): void;\n}\n$obj->add();";
1540 let docs = vec![doc("/a.php", src)];
1541
1542 let refs = find_references("add", &docs, false, None);
1543 let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
1544 assert!(
1545 !lines.contains(&2),
1546 "interface method declaration (line 2) must be excluded with kind=None + include_declaration=false, got: {:?}",
1547 lines
1548 );
1549 assert!(
1550 lines.contains(&4),
1551 "call site (line 4) must be present, got: {:?}",
1552 lines
1553 );
1554 }
1555
1556 #[test]
1557 fn function_kind_does_not_include_interface_method_declaration() {
1558 let src =
1569 "<?php\nfunction add() {}\nadd();\ninterface I {\n public function add(): void;\n}";
1570 let docs = vec![doc("/a.php", src)];
1571
1572 let refs = find_references("add", &docs, true, Some(SymbolKind::Function));
1573 let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
1574 assert!(
1575 lines.contains(&1),
1576 "free function declaration (line 1) must be present, got: {:?}",
1577 lines
1578 );
1579 assert!(
1580 lines.contains(&2),
1581 "free function call (line 2) must be present, got: {:?}",
1582 lines
1583 );
1584 assert!(
1585 !lines.contains(&4),
1586 "interface method declaration (line 4) must not appear with kind=Function, got: {:?}",
1587 lines
1588 );
1589 }
1590
1591 #[test]
1594 fn finds_function_call_inside_switch_case() {
1595 let src = "<?php\nfunction tick() {}\nswitch ($x) {\n case 1: tick(); break;\n}";
1598 let docs = vec![doc("/a.php", src)];
1599 let lines: Vec<u32> = find_references("tick", &docs, false, Some(SymbolKind::Function))
1600 .iter()
1601 .map(|r| r.range.start.line)
1602 .collect();
1603 assert!(
1604 lines.contains(&3),
1605 "tick() call inside switch case (line 3) must be present, got: {:?}",
1606 lines
1607 );
1608 }
1609
1610 #[test]
1611 fn finds_method_call_inside_switch_case() {
1612 let src = "<?php\nswitch ($x) {\n case 1: $obj->process(); break;\n}";
1614 let docs = vec![doc("/a.php", src)];
1615 let lines: Vec<u32> = find_references("process", &docs, false, Some(SymbolKind::Method))
1616 .iter()
1617 .map(|r| r.range.start.line)
1618 .collect();
1619 assert!(
1620 lines.contains(&2),
1621 "process() call inside switch case (line 2) must be present, got: {:?}",
1622 lines
1623 );
1624 }
1625
1626 #[test]
1627 fn finds_function_call_inside_switch_condition() {
1628 let src = "<?php\nfunction classify() {}\nswitch (classify()) { default: break; }";
1631 let docs = vec![doc("/a.php", src)];
1632 let lines: Vec<u32> = find_references("classify", &docs, false, Some(SymbolKind::Function))
1633 .iter()
1634 .map(|r| r.range.start.line)
1635 .collect();
1636 assert!(
1637 lines.contains(&2),
1638 "classify() in switch subject (line 2) must be present, got: {:?}",
1639 lines
1640 );
1641 }
1642
1643 #[test]
1644 fn finds_function_call_inside_throw() {
1645 let src = "<?php\nfunction makeException() {}\nthrow makeException();";
1648 let docs = vec![doc("/a.php", src)];
1649 let lines: Vec<u32> =
1650 find_references("makeException", &docs, false, Some(SymbolKind::Function))
1651 .iter()
1652 .map(|r| r.range.start.line)
1653 .collect();
1654 assert!(
1655 lines.contains(&2),
1656 "makeException() inside throw (line 2) must be present, got: {:?}",
1657 lines
1658 );
1659 }
1660
1661 #[test]
1662 fn finds_method_call_inside_throw() {
1663 let src = "<?php\nthrow $factory->create();";
1665 let docs = vec![doc("/a.php", src)];
1666 let lines: Vec<u32> = find_references("create", &docs, false, Some(SymbolKind::Method))
1667 .iter()
1668 .map(|r| r.range.start.line)
1669 .collect();
1670 assert!(
1671 lines.contains(&1),
1672 "create() inside throw (line 1) must be present, got: {:?}",
1673 lines
1674 );
1675 }
1676
1677 #[test]
1678 fn finds_method_call_inside_unset() {
1679 let src = "<?php\nunset($obj->getProp());";
1681 let docs = vec![doc("/a.php", src)];
1682 let lines: Vec<u32> = find_references("getProp", &docs, false, Some(SymbolKind::Method))
1683 .iter()
1684 .map(|r| r.range.start.line)
1685 .collect();
1686 assert!(
1687 lines.contains(&1),
1688 "getProp() inside unset (line 1) must be present, got: {:?}",
1689 lines
1690 );
1691 }
1692
1693 #[test]
1694 fn finds_static_method_call_in_class_property_default() {
1695 let src = "<?php\nclass Config {\n public array $data = self::defaults();\n public static function defaults(): array { return []; }\n}";
1700 let docs = vec![doc("/a.php", src)];
1701 let lines: Vec<u32> = find_references("defaults", &docs, false, Some(SymbolKind::Method))
1702 .iter()
1703 .map(|r| r.range.start.line)
1704 .collect();
1705 assert!(
1706 lines.contains(&2),
1707 "defaults() in class property default (line 2) must be present, got: {:?}",
1708 lines
1709 );
1710 }
1711
1712 #[test]
1713 fn finds_static_method_call_in_trait_property_default() {
1714 let src = "<?php\ntrait T {\n public int $x = self::init();\n public static function init(): int { return 0; }\n}";
1719 let docs = vec![doc("/a.php", src)];
1720 let lines: Vec<u32> = find_references("init", &docs, false, Some(SymbolKind::Method))
1721 .iter()
1722 .map(|r| r.range.start.line)
1723 .collect();
1724 assert!(
1725 lines.contains(&2),
1726 "init() in trait property default (line 2) must be present, got: {:?}",
1727 lines
1728 );
1729 }
1730
1731 fn make_class(
1734 fqcn: &str,
1735 is_final: bool,
1736 method_name: &str,
1737 visibility: mir_codebase::Visibility,
1738 ) -> mir_codebase::ClassStorage {
1739 use indexmap::IndexMap;
1740 let method = mir_codebase::MethodStorage {
1741 name: std::sync::Arc::from(method_name),
1742 fqcn: std::sync::Arc::from(fqcn),
1743 params: vec![],
1744 return_type: None,
1745 inferred_return_type: None,
1746 visibility,
1747 is_static: false,
1748 is_abstract: false,
1749 is_final: false,
1750 is_constructor: false,
1751 template_params: vec![],
1752 assertions: vec![],
1753 throws: vec![],
1754 deprecated: None,
1755 is_internal: false,
1756 is_pure: false,
1757 location: None,
1758 };
1759 let mut methods: IndexMap<
1760 std::sync::Arc<str>,
1761 std::sync::Arc<mir_codebase::MethodStorage>,
1762 > = IndexMap::new();
1763 methods.insert(
1765 std::sync::Arc::from(method_name.to_lowercase().as_str()),
1766 std::sync::Arc::new(method),
1767 );
1768 mir_codebase::ClassStorage {
1769 fqcn: std::sync::Arc::from(fqcn),
1770 short_name: std::sync::Arc::from(fqcn.rsplit('\\').next().unwrap_or(fqcn)),
1771 parent: None,
1772 extends_type_args: vec![],
1773 interfaces: vec![],
1774 traits: vec![],
1775 mixins: vec![],
1776 implements_type_args: vec![],
1777 own_methods: methods,
1778 own_properties: IndexMap::new(),
1779 own_constants: IndexMap::new(),
1780 template_params: vec![],
1781 is_abstract: false,
1782 is_final,
1783 is_readonly: false,
1784 all_parents: vec![],
1785 deprecated: None,
1786 is_internal: false,
1787 location: Some(mir_codebase::storage::Location {
1790 file: std::sync::Arc::from("file:///a.php"),
1791 start: 0,
1792 end: 0,
1793 line: 1,
1794 col: 0,
1795 }),
1796 }
1797 }
1798
1799 #[test]
1800 fn codebase_method_falls_back_for_public_method_on_nonfinal_class() {
1801 let cb = mir_codebase::Codebase::new();
1803 cb.classes.insert(
1804 std::sync::Arc::from("Foo"),
1805 make_class("Foo", false, "process", mir_codebase::Visibility::Public),
1806 );
1807 cb.mark_method_referenced_at(
1808 "Foo",
1809 "process",
1810 std::sync::Arc::from("file:///a.php"),
1811 10,
1812 17,
1813 );
1814
1815 let src = "<?php\nclass Foo { public function process() {} }\n$foo->process();";
1816 let docs = vec![doc("/a.php", src)];
1817 let result = find_references_codebase(
1818 "process",
1819 &docs,
1820 false,
1821 Some(SymbolKind::Method),
1822 &cb,
1823 &|k: &str| cb.get_reference_locations(k),
1824 );
1825 assert!(
1826 result.is_none(),
1827 "public method on non-final class must return None (fall back to AST), got: {:?}",
1828 result
1829 );
1830 }
1831
1832 #[test]
1833 fn codebase_method_fast_path_private_method_filters_files() {
1834 let cb = mir_codebase::Codebase::new();
1838 cb.classes.insert(
1839 std::sync::Arc::from("Foo"),
1840 make_class("Foo", false, "execute", mir_codebase::Visibility::Private),
1841 );
1842 cb.mark_method_referenced_at(
1844 "Foo",
1845 "execute",
1846 std::sync::Arc::from("file:///a.php"),
1847 10,
1848 17,
1849 );
1850
1851 let src_a = "<?php\nclass Foo {\n private function execute() {}\n public function run() { $this->execute(); }\n}";
1853 let src_b = "<?php\n$other->execute();";
1855
1856 let docs = vec![doc("/a.php", src_a), doc("/b.php", src_b)];
1857 let result = find_references_codebase(
1858 "execute",
1859 &docs,
1860 false,
1861 Some(SymbolKind::Method),
1862 &cb,
1863 &|k: &str| cb.get_reference_locations(k),
1864 );
1865
1866 assert!(
1867 result.is_some(),
1868 "private method must activate the fast path"
1869 );
1870 let locs = result.unwrap();
1871
1872 let uris: Vec<&str> = locs.iter().map(|l| l.uri.as_str()).collect();
1873 assert!(
1874 uris.iter().all(|u| u.ends_with("/a.php")),
1875 "all results must be from a.php (b.php was not in the codebase index), got: {:?}",
1876 locs
1877 );
1878 assert!(
1879 !locs.is_empty(),
1880 "expected at least the $this->execute() call in a.php, got: {:?}",
1881 locs
1882 );
1883 }
1884
1885 #[test]
1886 fn codebase_method_fast_path_final_class_filters_files() {
1887 let cb = mir_codebase::Codebase::new();
1890 cb.classes.insert(
1891 std::sync::Arc::from("Counter"),
1892 make_class(
1893 "Counter",
1894 true, "increment",
1896 mir_codebase::Visibility::Public,
1897 ),
1898 );
1899 cb.mark_method_referenced_at(
1900 "Counter",
1901 "increment",
1902 std::sync::Arc::from("file:///a.php"),
1903 10,
1904 19,
1905 );
1906
1907 let src_a = "<?php\nfinal class Counter {\n public function increment() {}\n}\n$c = new Counter();\n$c->increment();";
1908 let src_b = "<?php\n$other->increment();";
1909
1910 let docs = vec![doc("/a.php", src_a), doc("/b.php", src_b)];
1911 let result = find_references_codebase(
1912 "increment",
1913 &docs,
1914 false,
1915 Some(SymbolKind::Method),
1916 &cb,
1917 &|k: &str| cb.get_reference_locations(k),
1918 );
1919
1920 assert!(
1921 result.is_some(),
1922 "final class method must activate the fast path"
1923 );
1924 let locs = result.unwrap();
1925
1926 let uris: Vec<&str> = locs.iter().map(|l| l.uri.as_str()).collect();
1927 assert!(
1928 uris.iter().all(|u| u.ends_with("/a.php")),
1929 "all results must be from a.php only, got: {:?}",
1930 locs
1931 );
1932 }
1933
1934 #[test]
1935 fn codebase_method_fast_path_cross_file_reference() {
1936 let cb = mir_codebase::Codebase::new();
1940 cb.classes.insert(
1941 std::sync::Arc::from("Order"),
1942 make_class(
1943 "Order",
1944 true, "submit",
1946 mir_codebase::Visibility::Public,
1947 ),
1948 );
1949 cb.mark_method_referenced_at(
1951 "Order",
1952 "submit",
1953 std::sync::Arc::from("file:///caller.php"),
1954 50,
1955 56,
1956 );
1957
1958 let src_class = "<?php\nfinal class Order {\n public function submit() {}\n}";
1961 let src_caller = "<?php\n$order = new Order();\n$order->submit();";
1963 let src_ignored = "<?php\n$unknown->submit();";
1965
1966 let docs = vec![
1967 doc("/a.php", src_class),
1968 doc("/caller.php", src_caller),
1969 doc("/ignored.php", src_ignored),
1970 ];
1971
1972 let result = find_references_codebase(
1973 "submit",
1974 &docs,
1975 false,
1976 Some(SymbolKind::Method),
1977 &cb,
1978 &|k: &str| cb.get_reference_locations(k),
1979 );
1980
1981 assert!(result.is_some(), "fast path must activate for final class");
1982 let locs = result.unwrap();
1983
1984 let uris: Vec<&str> = locs.iter().map(|l| l.uri.as_str()).collect();
1985 assert!(
1986 uris.iter().any(|u| u.ends_with("/caller.php")),
1987 "caller.php (tracked) must appear in results, got: {:?}",
1988 locs
1989 );
1990 assert!(
1991 !uris.iter().any(|u| u.ends_with("/ignored.php")),
1992 "ignored.php (not tracked) must be excluded, got: {:?}",
1993 locs
1994 );
1995 }
1996
1997 #[test]
1998 fn codebase_method_fast_path_empty_codebase_falls_back() {
1999 let cb = mir_codebase::Codebase::new();
2001 let src = "<?php\n$obj->doWork();";
2002 let docs = vec![doc("/a.php", src)];
2003 let result = find_references_codebase(
2004 "doWork",
2005 &docs,
2006 false,
2007 Some(SymbolKind::Method),
2008 &cb,
2009 &|k: &str| cb.get_reference_locations(k),
2010 );
2011 assert!(
2012 result.is_none(),
2013 "empty codebase must return None for Method kind, got: {:?}",
2014 result
2015 );
2016 }
2017}