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, u16, u16)> + '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 = sv.position_of(span.end);
115 Location {
116 uri: uri.clone(),
117 range: Range { start, end },
118 }
119 })
120 .collect::<Vec<_>>()
121 })
122 .collect()
123}
124
125pub fn find_references_codebase(
144 word: &str,
145 all_docs: &[(Url, Arc<ParsedDoc>)],
146 include_declaration: bool,
147 kind: Option<SymbolKind>,
148 codebase: &mir_codebase::Codebase,
149 lookup_refs: &RefLookup<'_>,
150) -> Option<Vec<Location>> {
151 find_references_codebase_with_target(
152 word,
153 all_docs,
154 include_declaration,
155 kind,
156 None,
157 codebase,
158 lookup_refs,
159 )
160}
161
162pub fn find_references_codebase_with_target(
167 word: &str,
168 all_docs: &[(Url, Arc<ParsedDoc>)],
169 include_declaration: bool,
170 kind: Option<SymbolKind>,
171 target_fqn: Option<&str>,
172 codebase: &mir_codebase::Codebase,
173 lookup_refs: &RefLookup<'_>,
174) -> Option<Vec<Location>> {
175 let doc_map: std::collections::HashMap<&str, (&Url, &Arc<ParsedDoc>)> = all_docs
177 .iter()
178 .map(|(url, doc)| (url.as_str(), (url, doc)))
179 .collect();
180
181 let spans_to_location =
182 |file: &str, line: u32, col_start: u16, col_end: u16| -> Option<Location> {
183 let (url, _doc) = doc_map.get(file)?;
184 Some(Location {
185 uri: (*url).clone(),
186 range: Range {
187 start: Position::new(line.saturating_sub(1), col_start as u32),
188 end: Position::new(line.saturating_sub(1), col_end as u32),
189 },
190 })
191 };
192
193 let target_fqn = target_fqn.map(|t| t.trim_start_matches('\\'));
195
196 match kind {
197 Some(SymbolKind::Function) => {
198 let fqns: Vec<Arc<str>> = if let Some(t) = target_fqn.filter(|t| t.contains('\\')) {
201 match codebase.functions.get(t) {
205 Some(entry) => vec![entry.key().clone()],
206 None => return None,
207 }
208 } else {
209 codebase
210 .functions
211 .iter()
212 .filter_map(|e| {
213 let fqn = e.key();
214 let short = fqn.rsplit('\\').next().unwrap_or(fqn.as_ref());
215 if short == word {
216 Some(fqn.clone())
217 } else {
218 None
219 }
220 })
221 .collect()
222 };
223
224 if fqns.is_empty() {
225 return None;
226 }
227
228 let mut call_site_count = 0usize;
229 let mut locations: Vec<Location> = Vec::new();
230 for fqn in &fqns {
231 for (file, line, col_start, col_end) in lookup_refs(fqn) {
232 if let Some(loc) = spans_to_location(&file, line, col_start, col_end) {
233 locations.push(loc);
234 call_site_count += 1;
235 }
236 }
237 if include_declaration
238 && let Some(func) = codebase.functions.get(fqn.as_ref())
239 && let Some(decl) = &func.location
240 && let Ok(url) = Url::parse(&decl.file)
241 {
242 locations.push(Location {
243 uri: url,
244 range: Range {
245 start: Position::new(
246 decl.line.saturating_sub(1),
247 decl.col_start as u32,
248 ),
249 end: Position::new(
250 decl.line_end.saturating_sub(1),
251 decl.col_end as u32,
252 ),
253 },
254 });
255 }
256 }
257 if call_site_count == 0 {
261 return None;
262 }
263 Some(locations)
264 }
265
266 Some(SymbolKind::Class) => None,
271
272 Some(SymbolKind::Method) => {
273 let word_lower = word.to_lowercase();
274
275 let user_code_uris: HashSet<&str> =
279 all_docs.iter().map(|(url, _)| url.as_str()).collect();
280 let is_user_code = |loc: &Option<mir_codebase::storage::Location>| -> bool {
281 loc.as_ref()
282 .is_some_and(|l| user_code_uris.contains(l.file.as_ref()))
283 };
284
285 let mut method_keys: Vec<String> = Vec::new();
286 let mut candidate_arcs: Vec<Arc<str>> = Vec::new();
287
288 if let Some(owner_fqcn) = target_fqn {
289 let mut owners: Vec<Arc<str>> = Vec::new();
295
296 if let Some(entry) = codebase.classes.get(owner_fqcn) {
297 owners.push(entry.key().clone());
298 for e in codebase.classes.iter() {
299 if e.value()
300 .all_parents
301 .iter()
302 .any(|p| p.as_ref() == owner_fqcn)
303 {
304 owners.push(e.key().clone());
305 }
306 }
307 } else if let Some(entry) = codebase.enums.get(owner_fqcn) {
308 owners.push(entry.key().clone());
309 } else if let Some(entry) = codebase.interfaces.get(owner_fqcn) {
310 owners.push(entry.key().clone());
311 for e in codebase.classes.iter() {
312 if e.value()
313 .interfaces
314 .iter()
315 .any(|i| i.as_ref() == owner_fqcn)
316 {
317 owners.push(e.key().clone());
318 }
319 }
320 } else if let Some(entry) = codebase.traits.get(owner_fqcn) {
321 owners.push(entry.key().clone());
322 for e in codebase.classes.iter() {
323 if e.value().traits.iter().any(|t| t.as_ref() == owner_fqcn) {
324 owners.push(e.key().clone());
325 }
326 }
327 } else {
328 return None;
329 }
330
331 let mut call_site_count = 0usize;
335 let mut locations: Vec<Location> = Vec::new();
336 for owner in &owners {
337 let key = format!("{}::{}", owner, word_lower);
338 for (file, line, col_start, col_end) in lookup_refs(&key) {
339 if let Some(loc) = spans_to_location(&file, line, col_start, col_end) {
340 locations.push(loc);
341 call_site_count += 1;
342 }
343 }
344 }
345 if call_site_count == 0 {
350 return None;
351 }
352
353 if include_declaration {
354 for owner in &owners {
359 let decl_file =
360 codebase
361 .classes
362 .get(owner.as_ref())
363 .and_then(|e| {
364 e.value()
365 .own_methods
366 .get(word_lower.as_str())
367 .and_then(|m| m.location.as_ref().map(|l| l.file.clone()))
368 })
369 .or_else(|| {
370 codebase.enums.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.interfaces.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 .or_else(|| {
384 codebase.traits.get(owner.as_ref()).and_then(|e| {
385 e.value().own_methods.get(word_lower.as_str()).and_then(
386 |m| m.location.as_ref().map(|l| l.file.clone()),
387 )
388 })
389 });
390 let Some(decl_file) = decl_file else { continue };
391 let Some((url, doc)) = all_docs
392 .iter()
393 .find(|(u, _)| u.as_str() == decl_file.as_ref())
394 else {
395 continue;
396 };
397 let short = owner.rsplit('\\').next().unwrap_or(owner.as_ref());
401 let mut spans: Vec<Span> = Vec::new();
402 collect_method_decls_in_class(
403 doc.source(),
404 &doc.program().stmts,
405 short,
406 word,
407 &mut spans,
408 );
409 let sv = doc.view();
410 let word_utf16_len: u32 = word.chars().map(|c| c.len_utf16() as u32).sum();
411 for span in spans {
412 let start = sv.position_of(span.start);
413 let end = Position {
414 line: start.line,
415 character: start.character + word_utf16_len,
416 };
417 locations.push(Location {
418 uri: (*url).clone(),
419 range: Range { start, end },
420 });
421 }
422 }
423 }
424
425 return if locations.is_empty() {
426 None
427 } else {
428 Some(locations)
429 };
430 } else {
431 for entry in codebase.classes.iter() {
434 let cls = entry.value();
435 if !is_user_code(&cls.location) {
436 continue;
437 }
438 if let Some(method) = cls.own_methods.get(word_lower.as_str())
439 && (cls.is_final || method.visibility == mir_codebase::Visibility::Private)
440 {
441 method_keys.push(format!("{}::{}", entry.key(), word_lower));
442 if include_declaration && let Some(loc) = &method.location {
443 candidate_arcs.push(loc.file.clone());
444 }
445 }
446 }
447 for entry in codebase.enums.iter() {
448 let enm = entry.value();
449 if !is_user_code(&enm.location) {
450 continue;
451 }
452 if let Some(method) = enm.own_methods.get(word_lower.as_str())
453 && method.visibility == mir_codebase::Visibility::Private
454 {
455 method_keys.push(format!("{}::{}", entry.key(), word_lower));
456 if include_declaration && let Some(loc) = &method.location {
457 candidate_arcs.push(loc.file.clone());
458 }
459 }
460 }
461
462 if method_keys.is_empty() {
463 return None;
464 }
465 }
466
467 for key in &method_keys {
469 for (file, _, _, _) in lookup_refs(key) {
470 candidate_arcs.push(file);
471 }
472 }
473 let candidate_uris: HashSet<&str> = candidate_arcs.iter().map(|a| a.as_ref()).collect();
474
475 let candidate_docs: Vec<(Url, Arc<ParsedDoc>)> = all_docs
477 .iter()
478 .filter(|(url, _)| candidate_uris.contains(url.as_str()))
479 .cloned()
480 .collect();
481
482 let locations = find_references_inner(
483 word,
484 &candidate_docs,
485 include_declaration,
486 false,
487 Some(SymbolKind::Method),
488 None,
489 );
490 Some(locations)
491 }
492
493 None => None,
495
496 Some(SymbolKind::Property) => None,
499 }
500}
501
502fn find_references_inner(
503 word: &str,
504 all_docs: &[(Url, Arc<ParsedDoc>)],
505 include_declaration: bool,
506 include_use: bool,
507 kind: Option<SymbolKind>,
508 target_fqn: Option<&str>,
509) -> Vec<Location> {
510 let namespace_filter_active =
519 matches!(kind, Some(SymbolKind::Function) | Some(SymbolKind::Class));
520 all_docs
521 .par_iter()
522 .flat_map_iter(|(uri, doc)| {
523 if namespace_filter_active
524 && let Some(target) = target_fqn
525 && !doc_can_reference_target(doc, word, target)
526 {
527 return Vec::new();
528 }
529 scan_doc(word, uri, doc, include_declaration, include_use, kind)
530 })
531 .collect()
532}
533
534fn doc_can_reference_target(doc: &ParsedDoc, word: &str, target_fqn: &str) -> bool {
539 let target = target_fqn.trim_start_matches('\\');
540 let imports = collect_file_imports(doc);
541 let resolved = crate::moniker::resolve_fqn(doc, word, &imports);
542 resolved == target
547 || (resolved == word && !target.contains('\\'))
548 || (resolved == word && target == format!("\\{word}"))
549}
550
551pub(crate) fn collect_file_imports(doc: &ParsedDoc) -> std::collections::HashMap<String, String> {
555 let mut out = std::collections::HashMap::new();
556 fn walk(stmts: &[Stmt<'_, '_>], out: &mut std::collections::HashMap<String, String>) {
557 for stmt in stmts {
558 match &stmt.kind {
559 StmtKind::Use(u) => {
560 for item in u.uses.iter() {
561 let fqn = item.name.to_string_repr().into_owned();
562 let short = item
563 .alias
564 .map(|a| a.to_string())
565 .unwrap_or_else(|| fqn.rsplit('\\').next().unwrap_or(&fqn).to_string());
566 out.insert(short, fqn);
567 }
568 }
569 StmtKind::Namespace(ns) => {
570 if let NamespaceBody::Braced(inner) = &ns.body {
571 walk(inner, out);
572 }
573 }
574 _ => {}
575 }
576 }
577 }
578 walk(&doc.program().stmts, &mut out);
579 out
580}
581
582fn scan_doc(
583 word: &str,
584 uri: &Url,
585 doc: &Arc<ParsedDoc>,
586 include_declaration: bool,
587 include_use: bool,
588 kind: Option<SymbolKind>,
589) -> Vec<Location> {
590 let source = doc.source();
591 if !source.contains(word) {
596 return Vec::new();
597 }
598 let stmts = &doc.program().stmts;
599 let mut spans = Vec::new();
600
601 if include_use {
602 refs_in_stmts_with_use(source, stmts, word, &mut spans);
604 if !include_declaration {
605 let mut decl_spans = Vec::new();
606 collect_declaration_spans(source, stmts, word, None, &mut decl_spans);
607 let decl_set: HashSet<(u32, u32)> =
608 decl_spans.iter().map(|s| (s.start, s.end)).collect();
609 spans.retain(|span| !decl_set.contains(&(span.start, span.end)));
610 }
611 } else {
612 match kind {
613 Some(SymbolKind::Function) => function_refs_in_stmts(stmts, word, &mut spans),
614 Some(SymbolKind::Method) => method_refs_in_stmts(stmts, word, &mut spans),
615 Some(SymbolKind::Class) => class_refs_in_stmts(stmts, word, &mut spans),
616 Some(SymbolKind::Property) => {
619 property_refs_in_stmts(source, stmts, word, &mut spans);
620 if !include_declaration {
621 let mut decl_spans = Vec::new();
622 collect_declaration_spans(
623 source,
624 stmts,
625 word,
626 Some(SymbolKind::Property),
627 &mut decl_spans,
628 );
629 let decl_set: HashSet<(u32, u32)> =
630 decl_spans.iter().map(|s| (s.start, s.end)).collect();
631 spans.retain(|span| !decl_set.contains(&(span.start, span.end)));
632 }
633 }
634 None => {
636 refs_in_stmts(source, stmts, word, &mut spans);
637 if !include_declaration {
638 let mut decl_spans = Vec::new();
639 collect_declaration_spans(source, stmts, word, None, &mut decl_spans);
640 let decl_set: HashSet<(u32, u32)> =
641 decl_spans.iter().map(|s| (s.start, s.end)).collect();
642 spans.retain(|span| !decl_set.contains(&(span.start, span.end)));
643 }
644 }
645 }
646 if include_declaration
651 && matches!(
652 kind,
653 Some(SymbolKind::Function) | Some(SymbolKind::Method) | Some(SymbolKind::Class)
654 )
655 {
656 collect_declaration_spans(source, stmts, word, kind, &mut spans);
657 }
658 }
659
660 let sv = doc.view();
661 let word_utf16_len: u32 = word.chars().map(|c| c.len_utf16() as u32).sum();
662 spans
663 .into_iter()
664 .map(|span| {
665 let start = sv.position_of(span.start);
666 let end = Position {
667 line: start.line,
668 character: start.character + word_utf16_len,
669 };
670 Location {
671 uri: uri.clone(),
672 range: Range { start, end },
673 }
674 })
675 .collect()
676}
677
678fn declaration_name_span(source: &str, name: &str) -> Span {
680 let start = str_offset(source, name);
681 Span {
682 start,
683 end: start + name.len() as u32,
684 }
685}
686
687fn collect_method_decls_in_class(
693 source: &str,
694 stmts: &[Stmt<'_, '_>],
695 class_short: &str,
696 method_word: &str,
697 out: &mut Vec<Span>,
698) {
699 for stmt in stmts {
700 match &stmt.kind {
701 StmtKind::Class(c) if c.name == Some(class_short) => {
702 for member in c.members.iter() {
703 if let ClassMemberKind::Method(m) = &member.kind
704 && m.name == method_word
705 {
706 out.push(declaration_name_span(source, m.name));
707 }
708 }
709 }
710 StmtKind::Interface(i) if i.name == class_short => {
711 for member in i.members.iter() {
712 if let ClassMemberKind::Method(m) = &member.kind
713 && m.name == method_word
714 {
715 out.push(declaration_name_span(source, m.name));
716 }
717 }
718 }
719 StmtKind::Trait(t) if t.name == class_short => {
720 for member in t.members.iter() {
721 if let ClassMemberKind::Method(m) = &member.kind
722 && m.name == method_word
723 {
724 out.push(declaration_name_span(source, m.name));
725 }
726 }
727 }
728 StmtKind::Enum(e) if e.name == class_short => {
729 for member in e.members.iter() {
730 if let EnumMemberKind::Method(m) = &member.kind
731 && m.name == method_word
732 {
733 out.push(declaration_name_span(source, m.name));
734 }
735 }
736 }
737 StmtKind::Namespace(ns) => {
738 if let NamespaceBody::Braced(inner) = &ns.body {
739 collect_method_decls_in_class(source, inner, class_short, method_word, out);
740 }
741 }
742 _ => {}
743 }
744 }
745}
746
747fn collect_declaration_spans(
756 source: &str,
757 stmts: &[Stmt<'_, '_>],
758 word: &str,
759 kind: Option<SymbolKind>,
760 out: &mut Vec<Span>,
761) {
762 let want_free = matches!(kind, None | Some(SymbolKind::Function));
763 let want_method = matches!(kind, None | Some(SymbolKind::Method));
764 let want_type = matches!(kind, None | Some(SymbolKind::Class));
765 let want_property = matches!(kind, None | Some(SymbolKind::Property));
766
767 for stmt in stmts {
768 match &stmt.kind {
769 StmtKind::Function(f) => {
770 if want_free && f.name == word {
771 out.push(declaration_name_span(source, f.name));
772 }
773 }
774 StmtKind::Class(c) => {
775 if want_type
776 && let Some(name) = c.name
777 && name == word
778 {
779 out.push(declaration_name_span(source, name));
780 }
781 if want_method || want_property {
782 for member in c.members.iter() {
783 match &member.kind {
784 ClassMemberKind::Method(m) if want_method && m.name == word => {
785 out.push(declaration_name_span(source, m.name));
786 }
787 ClassMemberKind::Method(m)
788 if want_property && m.name == "__construct" =>
789 {
790 for p in m.params.iter() {
792 if p.visibility.is_some() && p.name == word {
793 out.push(declaration_name_span(source, p.name));
794 }
795 }
796 }
797 ClassMemberKind::Property(p) if want_property && p.name == word => {
798 out.push(declaration_name_span(source, p.name));
799 }
800 _ => {}
801 }
802 }
803 }
804 }
805 StmtKind::Interface(i) => {
806 if want_type && i.name == word {
807 out.push(declaration_name_span(source, i.name));
808 }
809 if want_method {
810 for member in i.members.iter() {
811 if let ClassMemberKind::Method(m) = &member.kind
812 && m.name == word
813 {
814 out.push(declaration_name_span(source, m.name));
815 }
816 }
817 }
818 }
819 StmtKind::Trait(t) => {
820 if want_type && t.name == word {
821 out.push(declaration_name_span(source, t.name));
822 }
823 if want_method || want_property {
824 for member in t.members.iter() {
825 match &member.kind {
826 ClassMemberKind::Method(m) if want_method && m.name == word => {
827 out.push(declaration_name_span(source, m.name));
828 }
829 ClassMemberKind::Property(p) if want_property && p.name == word => {
830 out.push(declaration_name_span(source, p.name));
831 }
832 _ => {}
833 }
834 }
835 }
836 }
837 StmtKind::Enum(e) => {
838 if want_type && e.name == word {
839 out.push(declaration_name_span(source, e.name));
840 }
841 for member in e.members.iter() {
842 match &member.kind {
843 EnumMemberKind::Method(m) if want_method && m.name == word => {
844 out.push(declaration_name_span(source, m.name));
845 }
846 EnumMemberKind::Case(c) if want_type && c.name == word => {
847 out.push(declaration_name_span(source, c.name));
848 }
849 _ => {}
850 }
851 }
852 }
853 StmtKind::Namespace(ns) => {
854 if let NamespaceBody::Braced(inner) = &ns.body {
855 collect_declaration_spans(source, inner, word, kind, out);
856 }
857 }
858 _ => {}
859 }
860 }
861}
862
863#[cfg(test)]
864mod tests {
865 use super::*;
866
867 fn uri(path: &str) -> Url {
868 Url::parse(&format!("file://{path}")).unwrap()
869 }
870
871 fn doc(path: &str, source: &str) -> (Url, Arc<ParsedDoc>) {
872 (uri(path), Arc::new(ParsedDoc::parse(source.to_string())))
873 }
874
875 #[test]
876 fn finds_function_call_reference() {
877 let src = "<?php\nfunction greet() {}\ngreet();\ngreet();";
878 let docs = vec![doc("/a.php", src)];
879 let refs = find_references("greet", &docs, false, None);
880 assert_eq!(refs.len(), 2, "expected 2 call-site refs, got {:?}", refs);
881 }
882
883 #[test]
884 fn include_declaration_adds_def_site() {
885 let src = "<?php\nfunction greet() {}\ngreet();";
886 let docs = vec![doc("/a.php", src)];
887 let with_decl = find_references("greet", &docs, true, None);
888 let without_decl = find_references("greet", &docs, false, None);
889 assert_eq!(
891 without_decl.len(),
892 1,
893 "expected 1 call-site ref without declaration"
894 );
895 assert_eq!(
896 without_decl[0].range.start.line, 2,
897 "call site should be on line 2"
898 );
899 assert_eq!(
901 with_decl.len(),
902 2,
903 "expected 2 refs with declaration included"
904 );
905 }
906
907 #[test]
908 fn finds_new_expression_reference() {
909 let src = "<?php\nclass Foo {}\n$x = new Foo();";
910 let docs = vec![doc("/a.php", src)];
911 let refs = find_references("Foo", &docs, false, None);
912 assert_eq!(
913 refs.len(),
914 1,
915 "expected exactly 1 reference to Foo in new expr"
916 );
917 assert_eq!(
918 refs[0].range.start.line, 2,
919 "new Foo() reference should be on line 2"
920 );
921 }
922
923 #[test]
924 fn finds_reference_in_nested_function_call() {
925 let src = "<?php\nfunction greet() {}\necho(greet());";
926 let docs = vec![doc("/a.php", src)];
927 let refs = find_references("greet", &docs, false, None);
928 assert_eq!(
929 refs.len(),
930 1,
931 "expected exactly 1 nested function call reference"
932 );
933 assert_eq!(
934 refs[0].range.start.line, 2,
935 "nested greet() call should be on line 2"
936 );
937 }
938
939 #[test]
940 fn finds_references_across_multiple_docs() {
941 let a = doc("/a.php", "<?php\nfunction helper() {}");
942 let b = doc("/b.php", "<?php\nhelper();\nhelper();");
943 let refs = find_references("helper", &[a, b], false, None);
944 assert_eq!(refs.len(), 2, "expected 2 cross-file references");
945 assert!(refs.iter().all(|r| r.uri.path().ends_with("/b.php")));
946 }
947
948 #[test]
949 fn finds_method_call_reference() {
950 let src = "<?php\nclass Calc { public function add() {} }\n$c = new Calc();\n$c->add();";
951 let docs = vec![doc("/a.php", src)];
952 let refs = find_references("add", &docs, false, None);
953 assert_eq!(
954 refs.len(),
955 1,
956 "expected exactly 1 method call reference to 'add'"
957 );
958 assert_eq!(
959 refs[0].range.start.line, 3,
960 "add() call should be on line 3"
961 );
962 }
963
964 #[test]
965 fn finds_reference_inside_if_body() {
966 let src = "<?php\nfunction check() {}\nif (true) { check(); }";
967 let docs = vec![doc("/a.php", src)];
968 let refs = find_references("check", &docs, false, None);
969 assert_eq!(refs.len(), 1, "expected exactly 1 reference inside if body");
970 assert_eq!(
971 refs[0].range.start.line, 2,
972 "check() inside if should be on line 2"
973 );
974 }
975
976 #[test]
977 fn finds_use_statement_reference() {
978 let src = "<?php\nuse MyClass;\n$x = new MyClass();";
981 let docs = vec![doc("/a.php", src)];
982 let refs = find_references_with_use("MyClass", &docs, false);
983 assert_eq!(
985 refs.len(),
986 2,
987 "expected exactly 2 references, got: {:?}",
988 refs
989 );
990 let mut lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
991 lines.sort_unstable();
992 assert_eq!(
993 lines,
994 vec![1, 2],
995 "references should be on lines 1 (use) and 2 (new)"
996 );
997 }
998
999 #[test]
1000 fn find_references_returns_correct_lines() {
1001 let src = "<?php\nhelper();\nhelper();\nfunction helper() {}";
1003 let docs = vec![doc("/a.php", src)];
1004 let refs = find_references("helper", &docs, false, None);
1005 assert_eq!(refs.len(), 2, "expected exactly 2 call-site references");
1006 let mut lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
1007 lines.sort_unstable();
1008 assert_eq!(lines, vec![1, 2], "references should be on lines 1 and 2");
1009 }
1010
1011 #[test]
1012 fn declaration_excluded_when_flag_false() {
1013 let src = "<?php\nfunction doWork() {}\ndoWork();\ndoWork();";
1015 let docs = vec![doc("/a.php", src)];
1016 let refs = find_references("doWork", &docs, false, None);
1017 let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
1019 assert!(
1020 !lines.contains(&1),
1021 "declaration line (1) must not appear when include_declaration=false, got: {:?}",
1022 lines
1023 );
1024 assert_eq!(refs.len(), 2, "expected 2 call-site references only");
1025 }
1026
1027 #[test]
1028 fn partial_match_not_included() {
1029 let src = "<?php\nfunction greet() {}\nfunction greeting() {}\ngreet();\ngreeting();";
1031 let docs = vec![doc("/a.php", src)];
1032 let refs = find_references("greet", &docs, false, None);
1033 for r in &refs {
1035 let span_len = r.range.end.character - r.range.start.character;
1038 assert_eq!(
1039 span_len, 5,
1040 "reference span length should equal len('greet')=5, got {} at {:?}",
1041 span_len, r
1042 );
1043 }
1044 assert_eq!(
1046 refs.len(),
1047 1,
1048 "expected exactly 1 reference to 'greet' (not 'greeting'), got: {:?}",
1049 refs
1050 );
1051 }
1052
1053 #[test]
1054 fn finds_reference_in_class_property_default() {
1055 let src = "<?php\nclass Foo {\n public string $status = Status::ACTIVE;\n}";
1057 let docs = vec![doc("/a.php", src)];
1058 let refs = find_references("Status", &docs, false, None);
1059 assert_eq!(
1060 refs.len(),
1061 1,
1062 "expected exactly 1 reference to Status in property default, got: {:?}",
1063 refs
1064 );
1065 assert_eq!(refs[0].range.start.line, 2, "reference should be on line 2");
1066 }
1067
1068 #[test]
1069 fn class_const_access_span_covers_only_member_name() {
1070 let src = "<?php\n$x = Status::ACTIVE;";
1076 let docs = vec![doc("/a.php", src)];
1077 let refs = find_references("ACTIVE", &docs, false, None);
1078 assert_eq!(refs.len(), 1, "expected 1 reference, got: {:?}", refs);
1079 let r = &refs[0].range;
1080 assert_eq!(r.start.line, 1, "reference must be on line 1");
1081 assert_eq!(
1084 r.start.character, 13,
1085 "range must start at 'ACTIVE' (char 13), not at 'Status' (char 5); got {:?}",
1086 r
1087 );
1088 }
1089
1090 #[test]
1091 fn class_const_access_no_duplicate_when_name_equals_class() {
1092 let src = "<?php\n$x = Status::Status;";
1102 let docs = vec![doc("/a.php", src)];
1103 let refs = find_references("Status", &docs, false, None);
1104 assert_eq!(
1105 refs.len(),
1106 2,
1107 "expected exactly 2 refs (class side + member side), got: {:?}",
1108 refs
1109 );
1110 let mut chars: Vec<u32> = refs.iter().map(|r| r.range.start.character).collect();
1111 chars.sort_unstable();
1112 assert_eq!(
1113 chars,
1114 vec![5, 13],
1115 "class-side ref must be at char 5 and member-side at char 13, got: {:?}",
1116 refs
1117 );
1118 }
1119
1120 #[test]
1121 fn finds_reference_inside_enum_method_body() {
1122 let src = "<?php\nfunction helper() {}\nenum Status {\n public function label(): string { return helper(); }\n}";
1124 let docs = vec![doc("/a.php", src)];
1125 let refs = find_references("helper", &docs, false, None);
1126 assert_eq!(
1127 refs.len(),
1128 1,
1129 "expected exactly 1 reference to helper() inside enum method, got: {:?}",
1130 refs
1131 );
1132 assert_eq!(refs[0].range.start.line, 3, "reference should be on line 3");
1133 }
1134
1135 #[test]
1136 fn finds_reference_in_for_init_and_update() {
1137 let src = "<?php\nfunction tick() {}\nfor (tick(); $i < 10; tick()) {}";
1139 let docs = vec![doc("/a.php", src)];
1140 let refs = find_references("tick", &docs, false, None);
1141 assert_eq!(
1142 refs.len(),
1143 2,
1144 "expected exactly 2 references to tick() (init + update), got: {:?}",
1145 refs
1146 );
1147 assert!(refs.iter().all(|r| r.range.start.line == 2));
1149 }
1150
1151 #[test]
1154 fn function_kind_skips_method_call_with_same_name() {
1155 let src = "<?php\nfunction get() {}\nget();\n$obj->get();";
1157 let docs = vec![doc("/a.php", src)];
1158 let refs = find_references("get", &docs, false, Some(SymbolKind::Function));
1159 assert_eq!(
1161 refs.len(),
1162 1,
1163 "expected 1 free-function ref, got: {:?}",
1164 refs
1165 );
1166 assert_eq!(refs[0].range.start.line, 2);
1167 }
1168
1169 #[test]
1170 fn method_kind_skips_free_function_call_with_same_name() {
1171 let src = "<?php\nfunction add() {}\nadd();\n$calc->add();";
1173 let docs = vec![doc("/a.php", src)];
1174 let refs = find_references("add", &docs, false, Some(SymbolKind::Method));
1175 assert_eq!(refs.len(), 1, "expected 1 method ref, got: {:?}", refs);
1177 assert_eq!(refs[0].range.start.line, 3);
1178 }
1179
1180 #[test]
1181 fn class_kind_finds_new_expression() {
1182 let src = "<?php\nclass Foo {}\n$x = new Foo();\nFoo();";
1184 let docs = vec![doc("/a.php", src)];
1185 let refs = find_references("Foo", &docs, false, Some(SymbolKind::Class));
1186 let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
1188 assert!(
1189 lines.contains(&2),
1190 "expected new Foo() on line 2, got: {:?}",
1191 refs
1192 );
1193 assert!(
1194 !lines.contains(&3),
1195 "free call Foo() should not appear as class ref, got: {:?}",
1196 refs
1197 );
1198 }
1199
1200 #[test]
1201 fn class_kind_finds_extends_and_implements() {
1202 let src = "<?php\nclass Base {}\ninterface Iface {}\nclass Child extends Base implements Iface {}";
1203 let docs = vec![doc("/a.php", src)];
1204
1205 let base_refs = find_references("Base", &docs, false, Some(SymbolKind::Class));
1206 let lines_base: Vec<u32> = base_refs.iter().map(|r| r.range.start.line).collect();
1207 assert!(
1208 lines_base.contains(&3),
1209 "expected extends Base on line 3, got: {:?}",
1210 base_refs
1211 );
1212
1213 let iface_refs = find_references("Iface", &docs, false, Some(SymbolKind::Class));
1214 let lines_iface: Vec<u32> = iface_refs.iter().map(|r| r.range.start.line).collect();
1215 assert!(
1216 lines_iface.contains(&3),
1217 "expected implements Iface on line 3, got: {:?}",
1218 iface_refs
1219 );
1220 }
1221
1222 #[test]
1223 fn class_kind_finds_type_hint() {
1224 let src = "<?php\nclass Foo {}\nfunction take(Foo $x): void {}";
1226 let docs = vec![doc("/a.php", src)];
1227 let refs = find_references("Foo", &docs, false, Some(SymbolKind::Class));
1228 let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
1229 assert!(
1230 lines.contains(&2),
1231 "expected type hint Foo on line 2, got: {:?}",
1232 refs
1233 );
1234 }
1235
1236 #[test]
1239 fn function_declaration_span_points_to_name_not_keyword() {
1240 let src = "<?php\nfunction greet() {}";
1243 let docs = vec![doc("/a.php", src)];
1244 let refs = find_references("greet", &docs, true, None);
1245 assert_eq!(refs.len(), 1, "expected exactly 1 ref (the declaration)");
1246 assert_eq!(
1249 refs[0].range.start.line, 1,
1250 "declaration should be on line 1"
1251 );
1252 assert_eq!(
1253 refs[0].range.start.character, 9,
1254 "declaration should start at the function name, not the 'function' keyword"
1255 );
1256 assert_eq!(
1257 refs[0].range.end.character,
1258 refs[0].range.start.character
1259 + "greet".chars().map(|c| c.len_utf16() as u32).sum::<u32>(),
1260 "range should span exactly the function name"
1261 );
1262 }
1263
1264 #[test]
1265 fn class_declaration_span_points_to_name_not_keyword() {
1266 let src = "<?php\nclass MyClass {}";
1267 let docs = vec![doc("/a.php", src)];
1268 let refs = find_references("MyClass", &docs, true, None);
1269 assert_eq!(refs.len(), 1);
1270 assert_eq!(refs[0].range.start.line, 1);
1272 assert_eq!(
1273 refs[0].range.start.character, 6,
1274 "declaration should start at 'MyClass', not 'class'"
1275 );
1276 }
1277
1278 #[test]
1279 fn method_declaration_span_points_to_name_not_keyword() {
1280 let src = "<?php\nclass C {\n public function doThing() {}\n}\n(new C())->doThing();";
1281 let docs = vec![doc("/a.php", src)];
1282 let refs = find_references("doThing", &docs, true, None);
1284 let decl_ref = refs
1286 .iter()
1287 .find(|r| r.range.start.line == 2)
1288 .expect("no declaration ref on line 2");
1289 assert_eq!(
1291 decl_ref.range.start.character, 20,
1292 "method declaration should start at the method name, not 'public function'"
1293 );
1294 }
1295
1296 #[test]
1297 fn method_kind_with_include_declaration_does_not_return_free_function() {
1298 let src =
1308 "<?php\nfunction get() {}\nget();\nclass C { public function get() {} }\n$c->get();";
1309 let docs = vec![doc("/a.php", src)];
1310 let refs = find_references("get", &docs, true, Some(SymbolKind::Method));
1311 let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
1312 assert!(
1313 lines.contains(&3),
1314 "method declaration (line 3) must be present, got: {:?}",
1315 lines
1316 );
1317 assert!(
1318 lines.contains(&4),
1319 "method call (line 4) must be present, got: {:?}",
1320 lines
1321 );
1322 assert!(
1323 !lines.contains(&1),
1324 "free function declaration (line 1) must not appear when kind=Method, got: {:?}",
1325 lines
1326 );
1327 assert!(
1328 !lines.contains(&2),
1329 "free function call (line 2) must not appear when kind=Method, got: {:?}",
1330 lines
1331 );
1332 }
1333
1334 #[test]
1335 fn function_kind_with_include_declaration_does_not_return_method_call() {
1336 let src =
1345 "<?php\nfunction add() {}\nadd();\nclass C { public function add() {} }\n$c->add();";
1346 let docs = vec![doc("/a.php", src)];
1347 let refs = find_references("add", &docs, true, Some(SymbolKind::Function));
1348 let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
1349 assert!(
1350 lines.contains(&1),
1351 "function declaration (line 1) must be present, got: {:?}",
1352 lines
1353 );
1354 assert!(
1355 lines.contains(&2),
1356 "function call (line 2) must be present, got: {:?}",
1357 lines
1358 );
1359 assert!(
1360 !lines.contains(&3),
1361 "method declaration (line 3) must not appear when kind=Function, got: {:?}",
1362 lines
1363 );
1364 assert!(
1365 !lines.contains(&4),
1366 "method call (line 4) must not appear when kind=Function, got: {:?}",
1367 lines
1368 );
1369 }
1370
1371 #[test]
1372 fn interface_method_declaration_included_when_flag_true() {
1373 let src = "<?php\ninterface I {\n public function add(): void;\n}\n$obj->add();";
1383 let docs = vec![doc("/a.php", src)];
1384
1385 let refs = find_references("add", &docs, true, Some(SymbolKind::Method));
1386 let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
1387 assert!(
1388 lines.contains(&2),
1389 "interface method declaration (line 2) must appear with include_declaration=true, got: {:?}",
1390 lines
1391 );
1392 assert!(
1393 lines.contains(&4),
1394 "call site (line 4) must appear, got: {:?}",
1395 lines
1396 );
1397
1398 let refs_no_decl = find_references("add", &docs, false, Some(SymbolKind::Method));
1400 let lines_no_decl: Vec<u32> = refs_no_decl.iter().map(|r| r.range.start.line).collect();
1401 assert!(
1402 !lines_no_decl.contains(&2),
1403 "interface method declaration must be excluded when include_declaration=false, got: {:?}",
1404 lines_no_decl
1405 );
1406 }
1407
1408 #[test]
1409 fn declaration_filter_finds_method_inside_same_named_class() {
1410 let src = "<?php\nclass get { public function get() {} }\n$obj->get();";
1419 let docs = vec![doc("/a.php", src)];
1420
1421 let refs = find_references("get", &docs, false, None);
1424 let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
1425 assert!(
1426 !lines.contains(&1),
1427 "declaration line (1) must not appear when include_declaration=false, got: {:?}",
1428 lines
1429 );
1430 assert!(
1431 lines.contains(&2),
1432 "call site (line 2) must be present, got: {:?}",
1433 lines
1434 );
1435
1436 let refs_with = find_references("get", &docs, true, None);
1439 assert_eq!(
1440 refs_with.len(),
1441 3,
1442 "expected 3 refs (class decl + method decl + call), got: {:?}",
1443 refs_with
1444 );
1445 }
1446
1447 #[test]
1448 fn interface_method_declaration_included_with_kind_none() {
1449 let src = "<?php\ninterface I {\n public function add(): void;\n}\n$obj->add();";
1459 let docs = vec![doc("/a.php", src)];
1460
1461 let refs = find_references("add", &docs, true, None);
1462 let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
1463 assert!(
1464 lines.contains(&2),
1465 "interface method declaration (line 2) must appear with kind=None + include_declaration=true, got: {:?}",
1466 lines
1467 );
1468 }
1469
1470 #[test]
1471 fn interface_method_declaration_excluded_with_kind_none_flag_false() {
1472 let src = "<?php\ninterface I {\n public function add(): void;\n}\n$obj->add();";
1483 let docs = vec![doc("/a.php", src)];
1484
1485 let refs = find_references("add", &docs, false, None);
1486 let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
1487 assert!(
1488 !lines.contains(&2),
1489 "interface method declaration (line 2) must be excluded with kind=None + include_declaration=false, got: {:?}",
1490 lines
1491 );
1492 assert!(
1493 lines.contains(&4),
1494 "call site (line 4) must be present, got: {:?}",
1495 lines
1496 );
1497 }
1498
1499 #[test]
1500 fn function_kind_does_not_include_interface_method_declaration() {
1501 let src =
1512 "<?php\nfunction add() {}\nadd();\ninterface I {\n public function add(): void;\n}";
1513 let docs = vec![doc("/a.php", src)];
1514
1515 let refs = find_references("add", &docs, true, Some(SymbolKind::Function));
1516 let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
1517 assert!(
1518 lines.contains(&1),
1519 "free function declaration (line 1) must be present, got: {:?}",
1520 lines
1521 );
1522 assert!(
1523 lines.contains(&2),
1524 "free function call (line 2) must be present, got: {:?}",
1525 lines
1526 );
1527 assert!(
1528 !lines.contains(&4),
1529 "interface method declaration (line 4) must not appear with kind=Function, got: {:?}",
1530 lines
1531 );
1532 }
1533
1534 #[test]
1537 fn finds_function_call_inside_switch_case() {
1538 let src = "<?php\nfunction tick() {}\nswitch ($x) {\n case 1: tick(); break;\n}";
1541 let docs = vec![doc("/a.php", src)];
1542 let lines: Vec<u32> = find_references("tick", &docs, false, Some(SymbolKind::Function))
1543 .iter()
1544 .map(|r| r.range.start.line)
1545 .collect();
1546 assert!(
1547 lines.contains(&3),
1548 "tick() call inside switch case (line 3) must be present, got: {:?}",
1549 lines
1550 );
1551 }
1552
1553 #[test]
1554 fn finds_method_call_inside_switch_case() {
1555 let src = "<?php\nswitch ($x) {\n case 1: $obj->process(); break;\n}";
1557 let docs = vec![doc("/a.php", src)];
1558 let lines: Vec<u32> = find_references("process", &docs, false, Some(SymbolKind::Method))
1559 .iter()
1560 .map(|r| r.range.start.line)
1561 .collect();
1562 assert!(
1563 lines.contains(&2),
1564 "process() call inside switch case (line 2) must be present, got: {:?}",
1565 lines
1566 );
1567 }
1568
1569 #[test]
1570 fn finds_function_call_inside_switch_condition() {
1571 let src = "<?php\nfunction classify() {}\nswitch (classify()) { default: break; }";
1574 let docs = vec![doc("/a.php", src)];
1575 let lines: Vec<u32> = find_references("classify", &docs, false, Some(SymbolKind::Function))
1576 .iter()
1577 .map(|r| r.range.start.line)
1578 .collect();
1579 assert!(
1580 lines.contains(&2),
1581 "classify() in switch subject (line 2) must be present, got: {:?}",
1582 lines
1583 );
1584 }
1585
1586 #[test]
1587 fn finds_function_call_inside_throw() {
1588 let src = "<?php\nfunction makeException() {}\nthrow makeException();";
1591 let docs = vec![doc("/a.php", src)];
1592 let lines: Vec<u32> =
1593 find_references("makeException", &docs, false, Some(SymbolKind::Function))
1594 .iter()
1595 .map(|r| r.range.start.line)
1596 .collect();
1597 assert!(
1598 lines.contains(&2),
1599 "makeException() inside throw (line 2) must be present, got: {:?}",
1600 lines
1601 );
1602 }
1603
1604 #[test]
1605 fn finds_method_call_inside_throw() {
1606 let src = "<?php\nthrow $factory->create();";
1608 let docs = vec![doc("/a.php", src)];
1609 let lines: Vec<u32> = find_references("create", &docs, false, Some(SymbolKind::Method))
1610 .iter()
1611 .map(|r| r.range.start.line)
1612 .collect();
1613 assert!(
1614 lines.contains(&1),
1615 "create() inside throw (line 1) must be present, got: {:?}",
1616 lines
1617 );
1618 }
1619
1620 #[test]
1621 fn finds_method_call_inside_unset() {
1622 let src = "<?php\nunset($obj->getProp());";
1624 let docs = vec![doc("/a.php", src)];
1625 let lines: Vec<u32> = find_references("getProp", &docs, false, Some(SymbolKind::Method))
1626 .iter()
1627 .map(|r| r.range.start.line)
1628 .collect();
1629 assert!(
1630 lines.contains(&1),
1631 "getProp() inside unset (line 1) must be present, got: {:?}",
1632 lines
1633 );
1634 }
1635
1636 #[test]
1637 fn finds_static_method_call_in_class_property_default() {
1638 let src = "<?php\nclass Config {\n public array $data = self::defaults();\n public static function defaults(): array { return []; }\n}";
1643 let docs = vec![doc("/a.php", src)];
1644 let lines: Vec<u32> = find_references("defaults", &docs, false, Some(SymbolKind::Method))
1645 .iter()
1646 .map(|r| r.range.start.line)
1647 .collect();
1648 assert!(
1649 lines.contains(&2),
1650 "defaults() in class property default (line 2) must be present, got: {:?}",
1651 lines
1652 );
1653 }
1654
1655 #[test]
1656 fn finds_static_method_call_in_trait_property_default() {
1657 let src = "<?php\ntrait T {\n public int $x = self::init();\n public static function init(): int { return 0; }\n}";
1662 let docs = vec![doc("/a.php", src)];
1663 let lines: Vec<u32> = find_references("init", &docs, false, Some(SymbolKind::Method))
1664 .iter()
1665 .map(|r| r.range.start.line)
1666 .collect();
1667 assert!(
1668 lines.contains(&2),
1669 "init() in trait property default (line 2) must be present, got: {:?}",
1670 lines
1671 );
1672 }
1673
1674 fn make_class(
1677 fqcn: &str,
1678 is_final: bool,
1679 method_name: &str,
1680 visibility: mir_codebase::Visibility,
1681 ) -> mir_codebase::ClassStorage {
1682 use indexmap::IndexMap;
1683 let method = mir_codebase::MethodStorage {
1684 name: std::sync::Arc::from(method_name),
1685 fqcn: std::sync::Arc::from(fqcn),
1686 params: vec![],
1687 return_type: None,
1688 inferred_return_type: None,
1689 visibility,
1690 is_static: false,
1691 is_abstract: false,
1692 is_final: false,
1693 is_constructor: false,
1694 template_params: vec![],
1695 assertions: vec![],
1696 throws: vec![],
1697 deprecated: None,
1698 is_internal: false,
1699 is_pure: false,
1700 location: None,
1701 };
1702 let mut methods: IndexMap<
1703 std::sync::Arc<str>,
1704 std::sync::Arc<mir_codebase::MethodStorage>,
1705 > = IndexMap::new();
1706 methods.insert(
1708 std::sync::Arc::from(method_name.to_lowercase().as_str()),
1709 std::sync::Arc::new(method),
1710 );
1711 mir_codebase::ClassStorage {
1712 fqcn: std::sync::Arc::from(fqcn),
1713 short_name: std::sync::Arc::from(fqcn.rsplit('\\').next().unwrap_or(fqcn)),
1714 parent: None,
1715 extends_type_args: vec![],
1716 interfaces: vec![],
1717 traits: vec![],
1718 mixins: vec![],
1719 implements_type_args: vec![],
1720 own_methods: methods,
1721 own_properties: IndexMap::new(),
1722 own_constants: IndexMap::new(),
1723 template_params: vec![],
1724 is_abstract: false,
1725 is_final,
1726 is_readonly: false,
1727 all_parents: vec![],
1728 deprecated: None,
1729 is_internal: false,
1730 type_aliases: std::collections::HashMap::new(),
1731 pending_import_types: vec![],
1732 location: Some(mir_codebase::storage::Location {
1735 file: std::sync::Arc::from("file:///a.php"),
1736 line: 1,
1737 line_end: 1,
1738 col_start: 0,
1739 col_end: 0,
1740 }),
1741 }
1742 }
1743
1744 #[test]
1745 fn codebase_method_falls_back_for_public_method_on_nonfinal_class() {
1746 let cb = mir_codebase::Codebase::new();
1748 cb.classes.insert(
1749 std::sync::Arc::from("Foo"),
1750 make_class("Foo", false, "process", mir_codebase::Visibility::Public),
1751 );
1752 cb.mark_method_referenced_at(
1753 "Foo",
1754 "process",
1755 std::sync::Arc::from("file:///a.php"),
1756 3,
1757 0,
1758 7,
1759 );
1760
1761 let src = "<?php\nclass Foo { public function process() {} }\n$foo->process();";
1762 let docs = vec![doc("/a.php", src)];
1763 let result = find_references_codebase(
1764 "process",
1765 &docs,
1766 false,
1767 Some(SymbolKind::Method),
1768 &cb,
1769 &|k: &str| cb.get_reference_locations(k),
1770 );
1771 assert!(
1772 result.is_none(),
1773 "public method on non-final class must return None (fall back to AST), got: {:?}",
1774 result
1775 );
1776 }
1777
1778 #[test]
1779 fn codebase_method_fast_path_private_method_filters_files() {
1780 let cb = mir_codebase::Codebase::new();
1784 cb.classes.insert(
1785 std::sync::Arc::from("Foo"),
1786 make_class("Foo", false, "execute", mir_codebase::Visibility::Private),
1787 );
1788 cb.mark_method_referenced_at(
1790 "Foo",
1791 "execute",
1792 std::sync::Arc::from("file:///a.php"),
1793 4,
1794 0,
1795 7,
1796 );
1797
1798 let src_a = "<?php\nclass Foo {\n private function execute() {}\n public function run() { $this->execute(); }\n}";
1800 let src_b = "<?php\n$other->execute();";
1802
1803 let docs = vec![doc("/a.php", src_a), doc("/b.php", src_b)];
1804 let result = find_references_codebase(
1805 "execute",
1806 &docs,
1807 false,
1808 Some(SymbolKind::Method),
1809 &cb,
1810 &|k: &str| cb.get_reference_locations(k),
1811 );
1812
1813 assert!(
1814 result.is_some(),
1815 "private method must activate the fast path"
1816 );
1817 let locs = result.unwrap();
1818
1819 let uris: Vec<&str> = locs.iter().map(|l| l.uri.as_str()).collect();
1820 assert!(
1821 uris.iter().all(|u| u.ends_with("/a.php")),
1822 "all results must be from a.php (b.php was not in the codebase index), got: {:?}",
1823 locs
1824 );
1825 assert!(
1826 !locs.is_empty(),
1827 "expected at least the $this->execute() call in a.php, got: {:?}",
1828 locs
1829 );
1830 }
1831
1832 #[test]
1833 fn codebase_method_fast_path_final_class_filters_files() {
1834 let cb = mir_codebase::Codebase::new();
1837 cb.classes.insert(
1838 std::sync::Arc::from("Counter"),
1839 make_class(
1840 "Counter",
1841 true, "increment",
1843 mir_codebase::Visibility::Public,
1844 ),
1845 );
1846 cb.mark_method_referenced_at(
1847 "Counter",
1848 "increment",
1849 std::sync::Arc::from("file:///a.php"),
1850 6,
1851 0,
1852 9,
1853 );
1854
1855 let src_a = "<?php\nfinal class Counter {\n public function increment() {}\n}\n$c = new Counter();\n$c->increment();";
1856 let src_b = "<?php\n$other->increment();";
1857
1858 let docs = vec![doc("/a.php", src_a), doc("/b.php", src_b)];
1859 let result = find_references_codebase(
1860 "increment",
1861 &docs,
1862 false,
1863 Some(SymbolKind::Method),
1864 &cb,
1865 &|k: &str| cb.get_reference_locations(k),
1866 );
1867
1868 assert!(
1869 result.is_some(),
1870 "final class method must activate the fast path"
1871 );
1872 let locs = result.unwrap();
1873
1874 let uris: Vec<&str> = locs.iter().map(|l| l.uri.as_str()).collect();
1875 assert!(
1876 uris.iter().all(|u| u.ends_with("/a.php")),
1877 "all results must be from a.php only, got: {:?}",
1878 locs
1879 );
1880 }
1881
1882 #[test]
1883 fn codebase_method_fast_path_cross_file_reference() {
1884 let cb = mir_codebase::Codebase::new();
1888 cb.classes.insert(
1889 std::sync::Arc::from("Order"),
1890 make_class(
1891 "Order",
1892 true, "submit",
1894 mir_codebase::Visibility::Public,
1895 ),
1896 );
1897 cb.mark_method_referenced_at(
1899 "Order",
1900 "submit",
1901 std::sync::Arc::from("file:///caller.php"),
1902 3,
1903 0,
1904 6,
1905 );
1906
1907 let src_class = "<?php\nfinal class Order {\n public function submit() {}\n}";
1910 let src_caller = "<?php\n$order = new Order();\n$order->submit();";
1912 let src_ignored = "<?php\n$unknown->submit();";
1914
1915 let docs = vec![
1916 doc("/a.php", src_class),
1917 doc("/caller.php", src_caller),
1918 doc("/ignored.php", src_ignored),
1919 ];
1920
1921 let result = find_references_codebase(
1922 "submit",
1923 &docs,
1924 false,
1925 Some(SymbolKind::Method),
1926 &cb,
1927 &|k: &str| cb.get_reference_locations(k),
1928 );
1929
1930 assert!(result.is_some(), "fast path must activate for final class");
1931 let locs = result.unwrap();
1932
1933 let uris: Vec<&str> = locs.iter().map(|l| l.uri.as_str()).collect();
1934 assert!(
1935 uris.iter().any(|u| u.ends_with("/caller.php")),
1936 "caller.php (tracked) must appear in results, got: {:?}",
1937 locs
1938 );
1939 assert!(
1940 !uris.iter().any(|u| u.ends_with("/ignored.php")),
1941 "ignored.php (not tracked) must be excluded, got: {:?}",
1942 locs
1943 );
1944 }
1945
1946 #[test]
1947 fn codebase_method_fast_path_empty_codebase_falls_back() {
1948 let cb = mir_codebase::Codebase::new();
1950 let src = "<?php\n$obj->doWork();";
1951 let docs = vec![doc("/a.php", src)];
1952 let result = find_references_codebase(
1953 "doWork",
1954 &docs,
1955 false,
1956 Some(SymbolKind::Method),
1957 &cb,
1958 &|k: &str| cb.get_reference_locations(k),
1959 );
1960 assert!(
1961 result.is_none(),
1962 "empty codebase must return None for Method kind, got: {:?}",
1963 result
1964 );
1965 }
1966
1967 #[test]
1970 fn property_kind_finds_instance_property_access() {
1971 let src = "<?php\nclass Order {\n public string $status = '';\n}\nfunction status() {}\n$o->status;\nstatus();";
1973 let docs = vec![doc("/a.php", src)];
1974 let refs = find_references("status", &docs, false, Some(SymbolKind::Property));
1975 let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
1976 assert!(
1977 lines.contains(&5),
1978 "$o->status access (line 5) must be present, got: {:?}",
1979 lines
1980 );
1981 assert!(
1982 !lines.contains(&6),
1983 "free function call status() (line 6) must not appear with kind=Property, got: {:?}",
1984 lines
1985 );
1986 }
1987
1988 #[test]
1989 fn property_kind_with_include_declaration_finds_decl() {
1990 let src = "<?php\nclass Foo {\n public int $count = 0;\n}\n$f->count;\n$f->count;";
1992 let docs = vec![doc("/a.php", src)];
1993 let refs_with = find_references("count", &docs, true, Some(SymbolKind::Property));
1994 let lines_with: Vec<u32> = refs_with.iter().map(|r| r.range.start.line).collect();
1995 assert!(
1996 lines_with.contains(&2),
1997 "property declaration (line 2) must be included with include_declaration=true, got: {:?}",
1998 lines_with
1999 );
2000 assert!(
2001 lines_with.contains(&4),
2002 "first access (line 4) must be included, got: {:?}",
2003 lines_with
2004 );
2005 assert!(
2006 lines_with.contains(&5),
2007 "second access (line 5) must be included, got: {:?}",
2008 lines_with
2009 );
2010 }
2011
2012 #[test]
2013 fn property_kind_excludes_declaration_when_flag_false() {
2014 let src = "<?php\nclass Foo {\n public int $count = 0;\n}\n$f->count;";
2016 let docs = vec![doc("/a.php", src)];
2017 let refs = find_references("count", &docs, false, Some(SymbolKind::Property));
2018 let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
2019 assert!(
2020 !lines.contains(&2),
2021 "property declaration (line 2) must be excluded when include_declaration=false, got: {:?}",
2022 lines
2023 );
2024 assert!(
2025 lines.contains(&4),
2026 "access (line 4) must be included, got: {:?}",
2027 lines
2028 );
2029 }
2030
2031 #[test]
2032 fn property_kind_does_not_match_method_with_same_name() {
2033 let src = "<?php\nclass Task {\n public bool $run = false;\n public function run(): void {}\n}\n$t->run;\n$t->run();";
2036 let docs = vec![doc("/a.php", src)];
2037 let refs = find_references("run", &docs, false, Some(SymbolKind::Property));
2038 let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
2039 assert!(
2040 lines.contains(&5),
2041 "property access $t->run (line 5) must be present, got: {:?}",
2042 lines
2043 );
2044 assert!(
2047 !lines.contains(&6),
2048 "method call $t->run() (line 6) must not appear with kind=Property, got: {:?}",
2049 lines
2050 );
2051 }
2052
2053 #[test]
2056 fn method_kind_finds_static_method_call() {
2057 let src = "<?php\nclass Builder {\n public static function create(): self { return new self(); }\n}\nBuilder::create();\n$b->create();";
2059 let docs = vec![doc("/a.php", src)];
2060 let refs = find_references("create", &docs, false, Some(SymbolKind::Method));
2061 let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
2062 assert!(
2063 lines.contains(&4),
2064 "Builder::create() static call (line 4) must be present, got: {:?}",
2065 lines
2066 );
2067 assert!(
2068 lines.contains(&5),
2069 "$b->create() instance call (line 5) must be present, got: {:?}",
2070 lines
2071 );
2072 }
2073
2074 #[test]
2077 fn find_references_with_target_includes_file_whose_namespace_resolves_to_target() {
2078 let src_a = "<?php\nnamespace Alpha;\nfunction make(): void { $w = new Widget(); }";
2081 let docs = vec![doc("/a.php", src_a)];
2082 let refs = find_references_with_target(
2083 "Widget",
2084 &docs,
2085 false,
2086 Some(SymbolKind::Class),
2087 "Alpha\\Widget",
2088 );
2089 let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
2090 assert!(
2091 lines.contains(&2),
2092 "new Widget() in Alpha namespace (line 2) must be included, got: {:?}",
2093 lines
2094 );
2095 }
2096
2097 #[test]
2098 fn find_references_with_target_excludes_file_with_different_namespace() {
2099 let src_a = "<?php\nnamespace Alpha;\n$w = new Widget();";
2102 let src_b = "<?php\nnamespace Beta;\n$w = new Widget();";
2103 let docs = vec![doc("/a.php", src_a), doc("/b.php", src_b)];
2104 let refs = find_references_with_target(
2105 "Widget",
2106 &docs,
2107 false,
2108 Some(SymbolKind::Class),
2109 "Alpha\\Widget",
2110 );
2111 let uris: Vec<&str> = refs.iter().map(|r| r.uri.as_str()).collect();
2112 assert!(
2113 uris.iter().any(|u| u.ends_with("/a.php")),
2114 "Alpha\\Widget in a.php must be included, got: {:?}",
2115 refs
2116 );
2117 assert!(
2118 !uris.iter().any(|u| u.ends_with("/b.php")),
2119 "Beta\\Widget in b.php must be excluded, got: {:?}",
2120 refs
2121 );
2122 }
2123
2124 #[test]
2125 fn find_references_with_target_global_function_fallback() {
2126 let src = "<?php\n$n = strlen('hello');";
2129 let docs = vec![doc("/a.php", src)];
2130 let refs = find_references_with_target(
2131 "strlen",
2132 &docs,
2133 false,
2134 Some(SymbolKind::Function),
2135 "strlen",
2136 );
2137 assert!(
2138 !refs.is_empty(),
2139 "strlen() in global-namespace file must be included, got: {:?}",
2140 refs
2141 );
2142 }
2143}