use std::collections::HashSet;
use std::sync::Arc;
use php_ast::{ClassMemberKind, EnumMemberKind, NamespaceBody, Span, Stmt, StmtKind};
use rayon::prelude::*;
use tower_lsp::lsp_types::{Location, Position, Range, Url};
use crate::ast::{ParsedDoc, str_offset_in_range};
use crate::util::utf16_code_units;
use crate::walk::{
class_refs_in_stmts, fqn_new_class_refs_in_stmts, function_refs_in_stmts, method_refs_in_stmts,
new_refs_in_stmts, property_refs_in_stmts, refs_in_stmts, refs_in_stmts_with_use,
};
pub type RefLookup<'a> = dyn Fn(&str) -> Vec<(Arc<str>, u32, u16, u16)> + 'a;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SymbolKind {
Function,
Method,
Class,
Property,
}
fn class_has_ancestor(
codebase: &mir_analyzer::db::MirDb,
class_fqcn: &str,
target_fqcn: &str,
) -> bool {
mir_analyzer::db::extends_or_implements_via_db(codebase, class_fqcn, target_fqcn)
}
pub fn find_references(
word: &str,
all_docs: &[(Url, Arc<ParsedDoc>)],
include_declaration: bool,
kind: Option<SymbolKind>,
) -> Vec<Location> {
find_references_inner(word, all_docs, include_declaration, false, kind, None)
}
pub fn find_references_with_target(
word: &str,
all_docs: &[(Url, Arc<ParsedDoc>)],
include_declaration: bool,
kind: Option<SymbolKind>,
target_fqn: &str,
) -> Vec<Location> {
let include_use = kind.is_none();
find_references_inner(
word,
all_docs,
include_declaration,
include_use,
kind,
Some(target_fqn),
)
}
pub fn find_references_with_use(
word: &str,
all_docs: &[(Url, Arc<ParsedDoc>)],
include_declaration: bool,
) -> Vec<Location> {
find_references_inner(word, all_docs, include_declaration, true, None, None)
}
pub fn find_constructor_references(
short_name: &str,
all_docs: &[(Url, Arc<ParsedDoc>)],
class_fqn: Option<&str>,
) -> Vec<Location> {
all_docs
.par_iter()
.flat_map_iter(|(uri, doc)| {
if let Some(fqn) = class_fqn
&& !doc_can_reference_target(doc, short_name, fqn)
&& !doc.view().source().contains(fqn.trim_start_matches('\\'))
{
return Vec::new();
}
let mut spans = Vec::new();
new_refs_in_stmts(&doc.program().stmts, short_name, class_fqn, &mut spans);
let sv = doc.view();
spans
.into_iter()
.map(|span| {
let start = sv.position_of(span.start);
let end = sv.position_of(span.end);
Location {
uri: uri.clone(),
range: Range { start, end },
}
})
.collect::<Vec<_>>()
})
.collect()
}
pub fn find_references_codebase(
word: &str,
all_docs: &[(Url, Arc<ParsedDoc>)],
include_declaration: bool,
kind: Option<SymbolKind>,
codebase: &mir_analyzer::db::MirDb,
lookup_refs: &RefLookup<'_>,
) -> Option<Vec<Location>> {
find_references_codebase_with_target(
word,
all_docs,
include_declaration,
kind,
None,
codebase,
lookup_refs,
)
}
pub fn find_references_codebase_with_target(
_word: &str,
_all_docs: &[(Url, Arc<ParsedDoc>)],
_include_declaration: bool,
kind: Option<SymbolKind>,
_target_fqn: Option<&str>,
_codebase: &mir_analyzer::db::MirDb,
_lookup_refs: &RefLookup<'_>,
) -> Option<Vec<Location>> {
match kind {
Some(SymbolKind::Function) => {
None
}
Some(SymbolKind::Class) => None,
Some(SymbolKind::Method) => {
None
}
None => None,
Some(SymbolKind::Property) => None,
}
}
fn find_references_inner(
word: &str,
all_docs: &[(Url, Arc<ParsedDoc>)],
include_declaration: bool,
include_use: bool,
kind: Option<SymbolKind>,
target_fqn: Option<&str>,
) -> Vec<Location> {
let namespace_filter_active =
matches!(kind, Some(SymbolKind::Function) | Some(SymbolKind::Class));
all_docs
.par_iter()
.flat_map_iter(|(uri, doc)| {
if namespace_filter_active
&& let Some(target) = target_fqn
&& !doc_can_reference_target(doc, word, target)
{
return Vec::new();
}
scan_doc(word, uri, doc, include_declaration, include_use, kind)
})
.collect()
}
fn doc_can_reference_target(doc: &ParsedDoc, word: &str, target_fqn: &str) -> bool {
let target = target_fqn.trim_start_matches('\\');
let imports = collect_file_imports(doc);
let resolved = crate::moniker::resolve_fqn(doc, word, &imports);
resolved == target
|| (resolved == word && !target.contains('\\'))
|| (resolved == word && target == format!("\\{word}"))
}
pub(crate) fn collect_file_imports(doc: &ParsedDoc) -> std::collections::HashMap<String, String> {
let mut out = std::collections::HashMap::new();
fn walk(stmts: &[Stmt<'_, '_>], out: &mut std::collections::HashMap<String, String>) {
for stmt in stmts {
match &stmt.kind {
StmtKind::Use(u) => {
for item in u.uses.iter() {
let fqn = item.name.to_string_repr().into_owned();
let short = item
.alias
.map(|a| a.to_string())
.unwrap_or_else(|| fqn.rsplit('\\').next().unwrap_or(&fqn).to_string());
out.insert(short, fqn);
}
}
StmtKind::Namespace(ns) => {
if let NamespaceBody::Braced(inner) = &ns.body {
walk(inner, out);
}
}
_ => {}
}
}
}
walk(&doc.program().stmts, &mut out);
out
}
pub(crate) fn collect_fqn_new_class_refs(doc: &ParsedDoc) -> Vec<String> {
fqn_new_class_refs_in_stmts(&doc.program().stmts)
}
fn scan_doc(
word: &str,
uri: &Url,
doc: &Arc<ParsedDoc>,
include_declaration: bool,
include_use: bool,
kind: Option<SymbolKind>,
) -> Vec<Location> {
let source = doc.source();
if !source.contains(word) {
return Vec::new();
}
let stmts = &doc.program().stmts;
let mut spans = Vec::new();
if include_use {
refs_in_stmts_with_use(source, stmts, word, &mut spans);
if !include_declaration {
let mut decl_spans = Vec::new();
collect_declaration_spans(source, stmts, word, None, &mut decl_spans);
let decl_set: HashSet<(u32, u32)> =
decl_spans.iter().map(|s| (s.start, s.end)).collect();
spans.retain(|span| !decl_set.contains(&(span.start, span.end)));
}
} else {
match kind {
Some(SymbolKind::Function) => function_refs_in_stmts(stmts, word, &mut spans),
Some(SymbolKind::Method) => method_refs_in_stmts(stmts, word, &mut spans),
Some(SymbolKind::Class) => class_refs_in_stmts(stmts, word, &mut spans),
Some(SymbolKind::Property) => {
property_refs_in_stmts(source, stmts, word, &mut spans);
if !include_declaration {
let mut decl_spans = Vec::new();
collect_declaration_spans(
source,
stmts,
word,
Some(SymbolKind::Property),
&mut decl_spans,
);
let decl_set: HashSet<(u32, u32)> =
decl_spans.iter().map(|s| (s.start, s.end)).collect();
spans.retain(|span| !decl_set.contains(&(span.start, span.end)));
}
}
None => {
refs_in_stmts(source, stmts, word, &mut spans);
if !include_declaration {
let mut decl_spans = Vec::new();
collect_declaration_spans(source, stmts, word, None, &mut decl_spans);
let decl_set: HashSet<(u32, u32)> =
decl_spans.iter().map(|s| (s.start, s.end)).collect();
spans.retain(|span| !decl_set.contains(&(span.start, span.end)));
}
}
}
if include_declaration
&& matches!(
kind,
Some(SymbolKind::Function) | Some(SymbolKind::Method) | Some(SymbolKind::Class)
)
{
collect_declaration_spans(source, stmts, word, kind, &mut spans);
}
}
let sv = doc.view();
let word_utf16_len: u32 = utf16_code_units(word);
spans
.into_iter()
.map(|span| {
let start = sv.position_of(span.start);
let end = Position {
line: start.line,
character: start.character + word_utf16_len,
};
Location {
uri: uri.clone(),
range: Range { start, end },
}
})
.collect()
}
fn declaration_name_span(source: &str, name: &str, stmt_span: Span) -> Span {
let start = str_offset_in_range(source, stmt_span, name).unwrap_or(stmt_span.start);
Span {
start,
end: start + name.len() as u32,
}
}
fn collect_method_decls_in_class(
source: &str,
stmts: &[Stmt<'_, '_>],
class_short: &str,
method_word: &str,
out: &mut Vec<Span>,
) {
for stmt in stmts {
match &stmt.kind {
StmtKind::Class(c)
if c.name.as_ref().map(|n| n.to_string()) == Some(class_short.to_string()) =>
{
for member in c.members.iter() {
if let ClassMemberKind::Method(m) = &member.kind
&& m.name == method_word
{
out.push(declaration_name_span(
source,
&m.name.to_string(),
stmt.span,
));
}
}
}
StmtKind::Interface(i) if i.name == class_short => {
for member in i.members.iter() {
if let ClassMemberKind::Method(m) = &member.kind
&& m.name == method_word
{
out.push(declaration_name_span(
source,
&m.name.to_string(),
stmt.span,
));
}
}
}
StmtKind::Trait(t) if t.name == class_short => {
for member in t.members.iter() {
if let ClassMemberKind::Method(m) = &member.kind
&& m.name == method_word
{
out.push(declaration_name_span(
source,
&m.name.to_string(),
stmt.span,
));
}
}
}
StmtKind::Enum(e) if e.name == class_short => {
for member in e.members.iter() {
if let EnumMemberKind::Method(m) = &member.kind
&& m.name == method_word
{
out.push(declaration_name_span(
source,
&m.name.to_string(),
stmt.span,
));
}
}
}
StmtKind::Namespace(ns) => {
if let NamespaceBody::Braced(inner) = &ns.body {
collect_method_decls_in_class(source, inner, class_short, method_word, out);
}
}
_ => {}
}
}
}
fn collect_declaration_spans(
source: &str,
stmts: &[Stmt<'_, '_>],
word: &str,
kind: Option<SymbolKind>,
out: &mut Vec<Span>,
) {
let want_free = matches!(kind, None | Some(SymbolKind::Function));
let want_method = matches!(kind, None | Some(SymbolKind::Method));
let want_type = matches!(kind, None | Some(SymbolKind::Class));
let want_property = matches!(kind, None | Some(SymbolKind::Property));
for stmt in stmts {
match &stmt.kind {
StmtKind::Function(f) if want_free && f.name == word => {
out.push(declaration_name_span(
source,
&f.name.to_string(),
stmt.span,
));
}
StmtKind::Class(c) => {
if want_type
&& let Some(name) = c.name
&& name == word
{
out.push(declaration_name_span(source, &name.to_string(), stmt.span));
}
if want_method || want_property {
for member in c.members.iter() {
match &member.kind {
ClassMemberKind::Method(m) if want_method && m.name == word => {
out.push(declaration_name_span(
source,
&m.name.to_string(),
member.span,
));
}
ClassMemberKind::Method(m)
if want_property && m.name == "__construct" =>
{
for p in m.params.iter() {
if p.visibility.is_some() && p.name == word {
out.push(declaration_name_span(
source,
&p.name.to_string(),
p.span,
));
}
}
}
ClassMemberKind::Property(p) if want_property && p.name == word => {
out.push(declaration_name_span(
source,
&p.name.to_string(),
member.span,
));
}
_ => {}
}
}
}
}
StmtKind::Interface(i) => {
if want_type && i.name == word {
out.push(declaration_name_span(
source,
&i.name.to_string(),
stmt.span,
));
}
if want_method {
for member in i.members.iter() {
if let ClassMemberKind::Method(m) = &member.kind
&& m.name == word
{
out.push(declaration_name_span(
source,
&m.name.to_string(),
member.span,
));
}
}
}
}
StmtKind::Trait(t) => {
if want_type && t.name == word {
out.push(declaration_name_span(
source,
&t.name.to_string(),
stmt.span,
));
}
if want_method || want_property {
for member in t.members.iter() {
match &member.kind {
ClassMemberKind::Method(m) if want_method && m.name == word => {
out.push(declaration_name_span(
source,
&m.name.to_string(),
stmt.span,
));
}
ClassMemberKind::Property(p) if want_property && p.name == word => {
out.push(declaration_name_span(
source,
&p.name.to_string(),
stmt.span,
));
}
_ => {}
}
}
}
}
StmtKind::Enum(e) => {
if want_type && e.name == word {
out.push(declaration_name_span(
source,
&e.name.to_string(),
stmt.span,
));
}
for member in e.members.iter() {
match &member.kind {
EnumMemberKind::Method(m) if want_method && m.name == word => {
out.push(declaration_name_span(
source,
&m.name.to_string(),
stmt.span,
));
}
EnumMemberKind::Case(c) if want_type && c.name == word => {
out.push(declaration_name_span(
source,
&c.name.to_string(),
stmt.span,
));
}
_ => {}
}
}
}
StmtKind::Namespace(ns) => {
if let NamespaceBody::Braced(inner) = &ns.body {
collect_declaration_spans(source, inner, word, kind, out);
}
}
_ => {}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn uri(path: &str) -> Url {
Url::parse(&format!("file://{path}")).unwrap()
}
fn doc(path: &str, source: &str) -> (Url, Arc<ParsedDoc>) {
(uri(path), Arc::new(ParsedDoc::parse(source.to_string())))
}
#[test]
fn finds_function_call_reference() {
let src = "<?php\nfunction greet() {}\ngreet();\ngreet();";
let docs = vec![doc("/a.php", src)];
let refs = find_references("greet", &docs, false, None);
assert_eq!(refs.len(), 2, "expected 2 call-site refs, got {:?}", refs);
}
#[test]
fn include_declaration_adds_def_site() {
let src = "<?php\nfunction greet() {}\ngreet();";
let docs = vec![doc("/a.php", src)];
let with_decl = find_references("greet", &docs, true, None);
let without_decl = find_references("greet", &docs, false, None);
assert_eq!(
without_decl.len(),
1,
"expected 1 call-site ref without declaration"
);
assert_eq!(
without_decl[0].range.start.line, 2,
"call site should be on line 2"
);
assert_eq!(
with_decl.len(),
2,
"expected 2 refs with declaration included"
);
}
#[test]
fn finds_new_expression_reference() {
let src = "<?php\nclass Foo {}\n$x = new Foo();";
let docs = vec![doc("/a.php", src)];
let refs = find_references("Foo", &docs, false, None);
assert_eq!(
refs.len(),
1,
"expected exactly 1 reference to Foo in new expr"
);
assert_eq!(
refs[0].range.start.line, 2,
"new Foo() reference should be on line 2"
);
}
#[test]
fn finds_reference_in_nested_function_call() {
let src = "<?php\nfunction greet() {}\necho(greet());";
let docs = vec![doc("/a.php", src)];
let refs = find_references("greet", &docs, false, None);
assert_eq!(
refs.len(),
1,
"expected exactly 1 nested function call reference"
);
assert_eq!(
refs[0].range.start.line, 2,
"nested greet() call should be on line 2"
);
}
#[test]
fn finds_references_across_multiple_docs() {
let a = doc("/a.php", "<?php\nfunction helper() {}");
let b = doc("/b.php", "<?php\nhelper();\nhelper();");
let refs = find_references("helper", &[a, b], false, None);
assert_eq!(refs.len(), 2, "expected 2 cross-file references");
assert!(refs.iter().all(|r| r.uri.path().ends_with("/b.php")));
}
#[test]
fn finds_method_call_reference() {
let src = "<?php\nclass Calc { public function add() {} }\n$c = new Calc();\n$c->add();";
let docs = vec![doc("/a.php", src)];
let refs = find_references("add", &docs, false, None);
assert_eq!(
refs.len(),
1,
"expected exactly 1 method call reference to 'add'"
);
assert_eq!(
refs[0].range.start.line, 3,
"add() call should be on line 3"
);
}
#[test]
fn finds_reference_inside_if_body() {
let src = "<?php\nfunction check() {}\nif (true) { check(); }";
let docs = vec![doc("/a.php", src)];
let refs = find_references("check", &docs, false, None);
assert_eq!(refs.len(), 1, "expected exactly 1 reference inside if body");
assert_eq!(
refs[0].range.start.line, 2,
"check() inside if should be on line 2"
);
}
#[test]
fn finds_use_statement_reference() {
let src = "<?php\nuse MyClass;\n$x = new MyClass();";
let docs = vec![doc("/a.php", src)];
let refs = find_references_with_use("MyClass", &docs, false);
assert_eq!(
refs.len(),
2,
"expected exactly 2 references, got: {:?}",
refs
);
let mut lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
lines.sort_unstable();
assert_eq!(
lines,
vec![1, 2],
"references should be on lines 1 (use) and 2 (new)"
);
}
#[test]
fn find_references_returns_correct_lines() {
let src = "<?php\nhelper();\nhelper();\nfunction helper() {}";
let docs = vec![doc("/a.php", src)];
let refs = find_references("helper", &docs, false, None);
assert_eq!(refs.len(), 2, "expected exactly 2 call-site references");
let mut lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
lines.sort_unstable();
assert_eq!(lines, vec![1, 2], "references should be on lines 1 and 2");
}
#[test]
fn declaration_excluded_when_flag_false() {
let src = "<?php\nfunction doWork() {}\ndoWork();\ndoWork();";
let docs = vec![doc("/a.php", src)];
let refs = find_references("doWork", &docs, false, None);
let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
assert!(
!lines.contains(&1),
"declaration line (1) must not appear when include_declaration=false, got: {:?}",
lines
);
assert_eq!(refs.len(), 2, "expected 2 call-site references only");
}
#[test]
fn partial_match_not_included() {
let src = "<?php\nfunction greet() {}\nfunction greeting() {}\ngreet();\ngreeting();";
let docs = vec![doc("/a.php", src)];
let refs = find_references("greet", &docs, false, None);
for r in &refs {
let span_len = r.range.end.character - r.range.start.character;
assert_eq!(
span_len, 5,
"reference span length should equal len('greet')=5, got {} at {:?}",
span_len, r
);
}
assert_eq!(
refs.len(),
1,
"expected exactly 1 reference to 'greet' (not 'greeting'), got: {:?}",
refs
);
}
#[test]
fn finds_reference_in_class_property_default() {
let src = "<?php\nclass Foo {\n public string $status = Status::ACTIVE;\n}";
let docs = vec![doc("/a.php", src)];
let refs = find_references("Status", &docs, false, None);
assert_eq!(
refs.len(),
1,
"expected exactly 1 reference to Status in property default, got: {:?}",
refs
);
assert_eq!(refs[0].range.start.line, 2, "reference should be on line 2");
}
#[test]
fn class_const_access_span_covers_only_member_name() {
let src = "<?php\n$x = Status::ACTIVE;";
let docs = vec![doc("/a.php", src)];
let refs = find_references("ACTIVE", &docs, false, None);
assert_eq!(refs.len(), 1, "expected 1 reference, got: {:?}", refs);
let r = &refs[0].range;
assert_eq!(r.start.line, 1, "reference must be on line 1");
assert_eq!(
r.start.character, 13,
"range must start at 'ACTIVE' (char 13), not at 'Status' (char 5); got {:?}",
r
);
}
#[test]
fn class_const_access_no_duplicate_when_name_equals_class() {
let src = "<?php\n$x = Status::Status;";
let docs = vec![doc("/a.php", src)];
let refs = find_references("Status", &docs, false, None);
assert_eq!(
refs.len(),
2,
"expected exactly 2 refs (class side + member side), got: {:?}",
refs
);
let mut chars: Vec<u32> = refs.iter().map(|r| r.range.start.character).collect();
chars.sort_unstable();
assert_eq!(
chars,
vec![5, 13],
"class-side ref must be at char 5 and member-side at char 13, got: {:?}",
refs
);
}
#[test]
fn finds_reference_inside_enum_method_body() {
let src = "<?php\nfunction helper() {}\nenum Status {\n public function label(): string { return helper(); }\n}";
let docs = vec![doc("/a.php", src)];
let refs = find_references("helper", &docs, false, None);
assert_eq!(
refs.len(),
1,
"expected exactly 1 reference to helper() inside enum method, got: {:?}",
refs
);
assert_eq!(refs[0].range.start.line, 3, "reference should be on line 3");
}
#[test]
fn finds_reference_in_for_init_and_update() {
let src = "<?php\nfunction tick() {}\nfor (tick(); $i < 10; tick()) {}";
let docs = vec![doc("/a.php", src)];
let refs = find_references("tick", &docs, false, None);
assert_eq!(
refs.len(),
2,
"expected exactly 2 references to tick() (init + update), got: {:?}",
refs
);
assert!(refs.iter().all(|r| r.range.start.line == 2));
}
#[test]
fn function_kind_skips_method_call_with_same_name() {
let src = "<?php\nfunction get() {}\nget();\n$obj->get();";
let docs = vec![doc("/a.php", src)];
let refs = find_references("get", &docs, false, Some(SymbolKind::Function));
assert_eq!(
refs.len(),
1,
"expected 1 free-function ref, got: {:?}",
refs
);
assert_eq!(refs[0].range.start.line, 2);
}
#[test]
fn method_kind_skips_free_function_call_with_same_name() {
let src = "<?php\nfunction add() {}\nadd();\n$calc->add();";
let docs = vec![doc("/a.php", src)];
let refs = find_references("add", &docs, false, Some(SymbolKind::Method));
assert_eq!(refs.len(), 1, "expected 1 method ref, got: {:?}", refs);
assert_eq!(refs[0].range.start.line, 3);
}
#[test]
fn class_kind_finds_new_expression() {
let src = "<?php\nclass Foo {}\n$x = new Foo();\nFoo();";
let docs = vec![doc("/a.php", src)];
let refs = find_references("Foo", &docs, false, Some(SymbolKind::Class));
let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
assert!(
lines.contains(&2),
"expected new Foo() on line 2, got: {:?}",
refs
);
assert!(
!lines.contains(&3),
"free call Foo() should not appear as class ref, got: {:?}",
refs
);
}
#[test]
fn class_kind_finds_extends_and_implements() {
let src = "<?php\nclass Base {}\ninterface Iface {}\nclass Child extends Base implements Iface {}";
let docs = vec![doc("/a.php", src)];
let base_refs = find_references("Base", &docs, false, Some(SymbolKind::Class));
let lines_base: Vec<u32> = base_refs.iter().map(|r| r.range.start.line).collect();
assert!(
lines_base.contains(&3),
"expected extends Base on line 3, got: {:?}",
base_refs
);
let iface_refs = find_references("Iface", &docs, false, Some(SymbolKind::Class));
let lines_iface: Vec<u32> = iface_refs.iter().map(|r| r.range.start.line).collect();
assert!(
lines_iface.contains(&3),
"expected implements Iface on line 3, got: {:?}",
iface_refs
);
}
#[test]
fn class_kind_finds_type_hint() {
let src = "<?php\nclass Foo {}\nfunction take(Foo $x): void {}";
let docs = vec![doc("/a.php", src)];
let refs = find_references("Foo", &docs, false, Some(SymbolKind::Class));
let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
assert!(
lines.contains(&2),
"expected type hint Foo on line 2, got: {:?}",
refs
);
}
#[test]
fn function_declaration_span_points_to_name_not_keyword() {
let src = "<?php\nfunction greet() {}";
let docs = vec![doc("/a.php", src)];
let refs = find_references("greet", &docs, true, None);
assert_eq!(refs.len(), 1, "expected exactly 1 ref (the declaration)");
assert_eq!(
refs[0].range.start.line, 1,
"declaration should be on line 1"
);
assert_eq!(
refs[0].range.start.character, 9,
"declaration should start at the function name, not the 'function' keyword"
);
assert_eq!(
refs[0].range.end.character,
refs[0].range.start.character + utf16_code_units("greet"),
"range should span exactly the function name"
);
}
#[test]
fn class_declaration_span_points_to_name_not_keyword() {
let src = "<?php\nclass MyClass {}";
let docs = vec![doc("/a.php", src)];
let refs = find_references("MyClass", &docs, true, None);
assert_eq!(refs.len(), 1);
assert_eq!(refs[0].range.start.line, 1);
assert_eq!(
refs[0].range.start.character, 6,
"declaration should start at 'MyClass', not 'class'"
);
}
#[test]
fn method_declaration_span_points_to_name_not_keyword() {
let src = "<?php\nclass C {\n public function doThing() {}\n}\n(new C())->doThing();";
let docs = vec![doc("/a.php", src)];
let refs = find_references("doThing", &docs, true, None);
let decl_ref = refs
.iter()
.find(|r| r.range.start.line == 2)
.expect("no declaration ref on line 2");
assert_eq!(
decl_ref.range.start.character, 20,
"method declaration should start at the method name, not 'public function'"
);
}
#[test]
fn method_kind_with_include_declaration_does_not_return_free_function() {
let src =
"<?php\nfunction get() {}\nget();\nclass C { public function get() {} }\n$c->get();";
let docs = vec![doc("/a.php", src)];
let refs = find_references("get", &docs, true, Some(SymbolKind::Method));
let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
assert!(
lines.contains(&3),
"method declaration (line 3) must be present, got: {:?}",
lines
);
assert!(
lines.contains(&4),
"method call (line 4) must be present, got: {:?}",
lines
);
assert!(
!lines.contains(&1),
"free function declaration (line 1) must not appear when kind=Method, got: {:?}",
lines
);
assert!(
!lines.contains(&2),
"free function call (line 2) must not appear when kind=Method, got: {:?}",
lines
);
}
#[test]
fn function_kind_with_include_declaration_does_not_return_method_call() {
let src =
"<?php\nfunction add() {}\nadd();\nclass C { public function add() {} }\n$c->add();";
let docs = vec![doc("/a.php", src)];
let refs = find_references("add", &docs, true, Some(SymbolKind::Function));
let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
assert!(
lines.contains(&1),
"function declaration (line 1) must be present, got: {:?}",
lines
);
assert!(
lines.contains(&2),
"function call (line 2) must be present, got: {:?}",
lines
);
assert!(
!lines.contains(&3),
"method declaration (line 3) must not appear when kind=Function, got: {:?}",
lines
);
assert!(
!lines.contains(&4),
"method call (line 4) must not appear when kind=Function, got: {:?}",
lines
);
}
#[test]
fn interface_method_declaration_included_when_flag_true() {
let src = "<?php\ninterface I {\n public function add(): void;\n}\n$obj->add();";
let docs = vec![doc("/a.php", src)];
let refs = find_references("add", &docs, true, Some(SymbolKind::Method));
let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
assert!(
lines.contains(&2),
"interface method declaration (line 2) must appear with include_declaration=true, got: {:?}",
lines
);
assert!(
lines.contains(&4),
"call site (line 4) must appear, got: {:?}",
lines
);
let refs_no_decl = find_references("add", &docs, false, Some(SymbolKind::Method));
let lines_no_decl: Vec<u32> = refs_no_decl.iter().map(|r| r.range.start.line).collect();
assert!(
!lines_no_decl.contains(&2),
"interface method declaration must be excluded when include_declaration=false, got: {:?}",
lines_no_decl
);
}
#[test]
fn declaration_filter_finds_method_inside_same_named_class() {
let src = "<?php\nclass get { public function get() {} }\n$obj->get();";
let docs = vec![doc("/a.php", src)];
let refs = find_references("get", &docs, false, None);
let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
assert!(
!lines.contains(&1),
"declaration line (1) must not appear when include_declaration=false, got: {:?}",
lines
);
assert!(
lines.contains(&2),
"call site (line 2) must be present, got: {:?}",
lines
);
let refs_with = find_references("get", &docs, true, None);
assert_eq!(
refs_with.len(),
3,
"expected 3 refs (class decl + method decl + call), got: {:?}",
refs_with
);
}
#[test]
fn interface_method_declaration_included_with_kind_none() {
let src = "<?php\ninterface I {\n public function add(): void;\n}\n$obj->add();";
let docs = vec![doc("/a.php", src)];
let refs = find_references("add", &docs, true, None);
let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
assert!(
lines.contains(&2),
"interface method declaration (line 2) must appear with kind=None + include_declaration=true, got: {:?}",
lines
);
}
#[test]
fn interface_method_declaration_excluded_with_kind_none_flag_false() {
let src = "<?php\ninterface I {\n public function add(): void;\n}\n$obj->add();";
let docs = vec![doc("/a.php", src)];
let refs = find_references("add", &docs, false, None);
let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
assert!(
!lines.contains(&2),
"interface method declaration (line 2) must be excluded with kind=None + include_declaration=false, got: {:?}",
lines
);
assert!(
lines.contains(&4),
"call site (line 4) must be present, got: {:?}",
lines
);
}
#[test]
fn function_kind_does_not_include_interface_method_declaration() {
let src =
"<?php\nfunction add() {}\nadd();\ninterface I {\n public function add(): void;\n}";
let docs = vec![doc("/a.php", src)];
let refs = find_references("add", &docs, true, Some(SymbolKind::Function));
let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
assert!(
lines.contains(&1),
"free function declaration (line 1) must be present, got: {:?}",
lines
);
assert!(
lines.contains(&2),
"free function call (line 2) must be present, got: {:?}",
lines
);
assert!(
!lines.contains(&4),
"interface method declaration (line 4) must not appear with kind=Function, got: {:?}",
lines
);
}
#[test]
fn finds_function_call_inside_switch_case() {
let src = "<?php\nfunction tick() {}\nswitch ($x) {\n case 1: tick(); break;\n}";
let docs = vec![doc("/a.php", src)];
let lines: Vec<u32> = find_references("tick", &docs, false, Some(SymbolKind::Function))
.iter()
.map(|r| r.range.start.line)
.collect();
assert!(
lines.contains(&3),
"tick() call inside switch case (line 3) must be present, got: {:?}",
lines
);
}
#[test]
fn finds_method_call_inside_switch_case() {
let src = "<?php\nswitch ($x) {\n case 1: $obj->process(); break;\n}";
let docs = vec![doc("/a.php", src)];
let lines: Vec<u32> = find_references("process", &docs, false, Some(SymbolKind::Method))
.iter()
.map(|r| r.range.start.line)
.collect();
assert!(
lines.contains(&2),
"process() call inside switch case (line 2) must be present, got: {:?}",
lines
);
}
#[test]
fn finds_function_call_inside_switch_condition() {
let src = "<?php\nfunction classify() {}\nswitch (classify()) { default: break; }";
let docs = vec![doc("/a.php", src)];
let lines: Vec<u32> = find_references("classify", &docs, false, Some(SymbolKind::Function))
.iter()
.map(|r| r.range.start.line)
.collect();
assert!(
lines.contains(&2),
"classify() in switch subject (line 2) must be present, got: {:?}",
lines
);
}
#[test]
fn finds_function_call_inside_throw() {
let src = "<?php\nfunction makeException() {}\nthrow makeException();";
let docs = vec![doc("/a.php", src)];
let lines: Vec<u32> =
find_references("makeException", &docs, false, Some(SymbolKind::Function))
.iter()
.map(|r| r.range.start.line)
.collect();
assert!(
lines.contains(&2),
"makeException() inside throw (line 2) must be present, got: {:?}",
lines
);
}
#[test]
fn finds_method_call_inside_throw() {
let src = "<?php\nthrow $factory->create();";
let docs = vec![doc("/a.php", src)];
let lines: Vec<u32> = find_references("create", &docs, false, Some(SymbolKind::Method))
.iter()
.map(|r| r.range.start.line)
.collect();
assert!(
lines.contains(&1),
"create() inside throw (line 1) must be present, got: {:?}",
lines
);
}
#[test]
fn finds_method_call_inside_unset() {
let src = "<?php\nunset($obj->getProp());";
let docs = vec![doc("/a.php", src)];
let lines: Vec<u32> = find_references("getProp", &docs, false, Some(SymbolKind::Method))
.iter()
.map(|r| r.range.start.line)
.collect();
assert!(
lines.contains(&1),
"getProp() inside unset (line 1) must be present, got: {:?}",
lines
);
}
#[test]
fn finds_static_method_call_in_class_property_default() {
let src = "<?php\nclass Config {\n public array $data = self::defaults();\n public static function defaults(): array { return []; }\n}";
let docs = vec![doc("/a.php", src)];
let lines: Vec<u32> = find_references("defaults", &docs, false, Some(SymbolKind::Method))
.iter()
.map(|r| r.range.start.line)
.collect();
assert!(
lines.contains(&2),
"defaults() in class property default (line 2) must be present, got: {:?}",
lines
);
}
#[test]
fn finds_static_method_call_in_trait_property_default() {
let src = "<?php\ntrait T {\n public int $x = self::init();\n public static function init(): int { return 0; }\n}";
let docs = vec![doc("/a.php", src)];
let lines: Vec<u32> = find_references("init", &docs, false, Some(SymbolKind::Method))
.iter()
.map(|r| r.range.start.line)
.collect();
assert!(
lines.contains(&2),
"init() in trait property default (line 2) must be present, got: {:?}",
lines
);
}
#[test]
fn property_kind_finds_instance_property_access() {
let src = "<?php\nclass Order {\n public string $status = '';\n}\nfunction status() {}\n$o->status;\nstatus();";
let docs = vec![doc("/a.php", src)];
let refs = find_references("status", &docs, false, Some(SymbolKind::Property));
let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
assert!(
lines.contains(&5),
"$o->status access (line 5) must be present, got: {:?}",
lines
);
assert!(
!lines.contains(&6),
"free function call status() (line 6) must not appear with kind=Property, got: {:?}",
lines
);
}
#[test]
fn property_kind_with_include_declaration_finds_decl() {
let src = "<?php\nclass Foo {\n public int $count = 0;\n}\n$f->count;\n$f->count;";
let docs = vec![doc("/a.php", src)];
let refs_with = find_references("count", &docs, true, Some(SymbolKind::Property));
let lines_with: Vec<u32> = refs_with.iter().map(|r| r.range.start.line).collect();
assert!(
lines_with.contains(&2),
"property declaration (line 2) must be included with include_declaration=true, got: {:?}",
lines_with
);
assert!(
lines_with.contains(&4),
"first access (line 4) must be included, got: {:?}",
lines_with
);
assert!(
lines_with.contains(&5),
"second access (line 5) must be included, got: {:?}",
lines_with
);
}
#[test]
fn property_kind_excludes_declaration_when_flag_false() {
let src = "<?php\nclass Foo {\n public int $count = 0;\n}\n$f->count;";
let docs = vec![doc("/a.php", src)];
let refs = find_references("count", &docs, false, Some(SymbolKind::Property));
let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
assert!(
!lines.contains(&2),
"property declaration (line 2) must be excluded when include_declaration=false, got: {:?}",
lines
);
assert!(
lines.contains(&4),
"access (line 4) must be included, got: {:?}",
lines
);
}
#[test]
fn property_kind_does_not_match_method_with_same_name() {
let src = "<?php\nclass Task {\n public bool $run = false;\n public function run(): void {}\n}\n$t->run;\n$t->run();";
let docs = vec![doc("/a.php", src)];
let refs = find_references("run", &docs, false, Some(SymbolKind::Property));
let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
assert!(
lines.contains(&5),
"property access $t->run (line 5) must be present, got: {:?}",
lines
);
assert!(
!lines.contains(&6),
"method call $t->run() (line 6) must not appear with kind=Property, got: {:?}",
lines
);
}
#[test]
fn method_kind_finds_static_method_call() {
let src = "<?php\nclass Builder {\n public static function create(): self { return new self(); }\n}\nBuilder::create();\n$b->create();";
let docs = vec![doc("/a.php", src)];
let refs = find_references("create", &docs, false, Some(SymbolKind::Method));
let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
assert!(
lines.contains(&4),
"Builder::create() static call (line 4) must be present, got: {:?}",
lines
);
assert!(
lines.contains(&5),
"$b->create() instance call (line 5) must be present, got: {:?}",
lines
);
}
#[test]
fn find_references_with_target_includes_file_whose_namespace_resolves_to_target() {
let src_a = "<?php\nnamespace Alpha;\nfunction make(): void { $w = new Widget(); }";
let docs = vec![doc("/a.php", src_a)];
let refs = find_references_with_target(
"Widget",
&docs,
false,
Some(SymbolKind::Class),
"Alpha\\Widget",
);
let lines: Vec<u32> = refs.iter().map(|r| r.range.start.line).collect();
assert!(
lines.contains(&2),
"new Widget() in Alpha namespace (line 2) must be included, got: {:?}",
lines
);
}
#[test]
fn find_references_with_target_excludes_file_with_different_namespace() {
let src_a = "<?php\nnamespace Alpha;\n$w = new Widget();";
let src_b = "<?php\nnamespace Beta;\n$w = new Widget();";
let docs = vec![doc("/a.php", src_a), doc("/b.php", src_b)];
let refs = find_references_with_target(
"Widget",
&docs,
false,
Some(SymbolKind::Class),
"Alpha\\Widget",
);
let uris: Vec<&str> = refs.iter().map(|r| r.uri.as_str()).collect();
assert!(
uris.iter().any(|u| u.ends_with("/a.php")),
"Alpha\\Widget in a.php must be included, got: {:?}",
refs
);
assert!(
!uris.iter().any(|u| u.ends_with("/b.php")),
"Beta\\Widget in b.php must be excluded, got: {:?}",
refs
);
}
#[test]
fn find_references_with_target_global_function_fallback() {
let src = "<?php\n$n = strlen('hello');";
let docs = vec![doc("/a.php", src)];
let refs = find_references_with_target(
"strlen",
&docs,
false,
Some(SymbolKind::Function),
"strlen",
);
assert!(
!refs.is_empty(),
"strlen() in global-namespace file must be included, got: {:?}",
refs
);
}
}