mod common;
use common::TestServer;
use serde_json::Value;
fn lines_of(locs: &[Value]) -> Vec<u32> {
locs.iter()
.map(|l| l["range"]["start"]["line"].as_u64().unwrap() as u32)
.collect()
}
#[tokio::test]
async fn references_with_exclude_declaration() {
let mut server = TestServer::new().await;
let opened = server
.open_fixture(
r#"<?php
function s$0ub(int $a, int $b): int { return $a - $b; }
sub(10, 3);
"#,
)
.await;
let c = opened.cursor();
let resp = server.references(&c.path, c.line, c.character, false).await;
assert!(resp["error"].is_null(), "references error: {resp:?}");
let locs = resp["result"].as_array().expect("expected array").clone();
assert_eq!(locs.len(), 1, "expected one call-site reference: {locs:?}");
assert_eq!(locs[0]["range"]["start"]["line"].as_u64().unwrap(), 2);
assert_eq!(locs[0]["range"]["start"]["character"].as_u64().unwrap(), 0);
}
#[tokio::test]
async fn references_include_declaration_returns_both() {
let mut server = TestServer::new().await;
let opened = server
.open_fixture(
r#"<?php
function a$0dd(int $a, int $b): int { return $a + $b; }
add(1, 2);
"#,
)
.await;
let c = opened.cursor();
let resp = server.references(&c.path, c.line, c.character, true).await;
assert!(resp["error"].is_null());
let locs = resp["result"].as_array().cloned().unwrap_or_default();
assert!(
locs.len() >= 2,
"expected declaration + call site: {locs:?}"
);
}
#[tokio::test]
async fn references_on_method_decl_returns_method_refs_not_function_refs() {
let mut server = TestServer::new().await;
let opened = server
.open_fixture(
r#"<?php
function add() {}
class C {
public function a$0dd() {}
}
add();
$c->add();
"#,
)
.await;
let c = opened.cursor();
let resp = server.references(&c.path, c.line, c.character, true).await;
assert!(resp["error"].is_null(), "references error: {resp:?}");
let lines = lines_of(resp["result"].as_array().expect("array"));
assert!(lines.contains(&3), "method decl line 3 missing: {lines:?}");
assert!(lines.contains(&6), "method call line 6 missing: {lines:?}");
assert!(
!lines.contains(&1),
"free-function decl line 1 must be excluded: {lines:?}"
);
assert!(
!lines.contains(&5),
"free-function call line 5 must be excluded: {lines:?}"
);
let resp2 = server.references(&c.path, c.line, c.character, false).await;
assert!(resp2["error"].is_null(), "references error: {resp2:?}");
let lines2 = lines_of(resp2["result"].as_array().expect("array"));
assert!(
lines2.contains(&6),
"method call line 6 missing: {lines2:?}"
);
assert!(
!lines2.contains(&3),
"method decl must be excluded when includeDeclaration=false: {lines2:?}"
);
}
#[tokio::test]
async fn references_on_method_decl_excludes_cross_file_free_function() {
let mut server = TestServer::new().await;
let opened = server
.open_fixture(
r#"//- /a.php
<?php
class C {
public function a$0dd() {}
}
//- /b.php
<?php
function add() {}
add();
$c->add();
"#,
)
.await;
let c = opened.cursor();
let a_uri = server.uri("a.php");
let b_uri = server.uri("b.php");
let resp = server.references(&c.path, c.line, c.character, true).await;
assert!(resp["error"].is_null(), "references error: {resp:?}");
let hits: Vec<(String, u32)> = resp["result"]
.as_array()
.expect("array")
.iter()
.map(|l| {
(
l["uri"].as_str().unwrap().to_string(),
l["range"]["start"]["line"].as_u64().unwrap() as u32,
)
})
.collect();
assert!(
hits.contains(&(a_uri.clone(), 2)),
"method decl a.php:2 missing: {hits:?}"
);
assert!(
hits.contains(&(b_uri.clone(), 3)),
"method call b.php:3 missing: {hits:?}"
);
assert!(
!hits.contains(&(b_uri.clone(), 1)),
"free-function decl b.php:1 must be excluded: {hits:?}"
);
assert!(
!hits.contains(&(b_uri.clone(), 2)),
"free-function call b.php:2 must be excluded: {hits:?}"
);
}
#[tokio::test]
async fn references_fast_path_final_class_cross_file_e2e() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("class.php"),
"<?php\nfinal class Order {\n public function submit(): void {}\n}\n",
)
.unwrap();
std::fs::write(
dir.path().join("caller.php"),
"<?php\n$order = new Order();\n$order->submit();\n",
)
.unwrap();
std::fs::write(
dir.path().join("ignored.php"),
"<?php\n$unknown->submit();\n",
)
.unwrap();
let mut server = TestServer::with_root(dir.path()).await;
server.wait_for_index_ready().await;
let caller_uri = server.uri("caller.php");
let ignored_uri = server.uri("ignored.php");
server
.open(
"class.php",
"<?php\nfinal class Order {\n public function submit(): void {}\n}\n",
)
.await;
let resp = server.references("class.php", 2, 20, false).await;
assert!(resp["error"].is_null(), "references error: {resp:?}");
let uris: Vec<&str> = resp["result"]
.as_array()
.expect("array")
.iter()
.map(|l| l["uri"].as_str().unwrap())
.collect();
assert!(
uris.iter().any(|u| *u == caller_uri.as_str()),
"caller.php missing: {uris:?}"
);
assert!(
!uris.iter().any(|u| *u == ignored_uri.as_str()),
"ignored.php (untyped) must be excluded by fast path: {uris:?}"
);
}
#[tokio::test]
async fn references_on_constructor_are_scoped_to_owning_class() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("a.php"),
"<?php\nclass Foo {\n public function __construct(int $x) {}\n}\n",
)
.unwrap();
std::fs::write(
dir.path().join("b.php"),
"<?php\nclass Bar {\n public function __construct(string $s) {}\n}\n",
)
.unwrap();
std::fs::write(
dir.path().join("c.php"),
"<?php\n$foo = new Foo(1);\n$bar = new Bar('x');\n",
)
.unwrap();
let mut server = TestServer::with_root(dir.path()).await;
server.wait_for_index_ready().await;
let (text, _, _) = server.locate("a.php", "<?php", 0);
server.open("a.php", &text).await;
let (_, line, col) = server.locate("a.php", "__construct", 0);
let resp = server.references("a.php", line, col + 2, true).await;
assert!(resp["error"].is_null(), "references error: {resp:?}");
let a_uri = server.uri("a.php");
let b_uri = server.uri("b.php");
let c_uri = server.uri("c.php");
let hits: Vec<(String, u32)> = resp["result"]
.as_array()
.unwrap_or_else(|| panic!("expected array of references, got: {resp:?}"))
.iter()
.map(|l| {
(
l["uri"].as_str().unwrap().to_string(),
l["range"]["start"]["line"].as_u64().unwrap() as u32,
)
})
.collect();
assert!(
!hits.contains(&(b_uri.clone(), 2)),
"Bar::__construct decl on b.php:2 must be excluded — got {hits:?}"
);
assert!(
!hits.contains(&(c_uri.clone(), 2)),
"`new Bar('x')` on c.php:2 must be excluded — got {hits:?}"
);
assert!(
hits.iter().any(|(u, _)| u == &a_uri),
"Foo::__construct decl missing — got {hits:?}"
);
assert!(
hits.contains(&(c_uri.clone(), 1)),
"`new Foo(1)` missing from c.php:1 — got {hits:?}"
);
}
#[tokio::test]
async fn references_on_second_constructor_has_correct_decl_span() {
let mut server = TestServer::new().await;
let opened = server
.open_fixture(
r#"<?php
class Alpha {
public function __construct(int $x) {}
}
class Beta {
public function __con$0struct(string $s) {}
}
new Alpha(1);
new Beta('x');
"#,
)
.await;
let c = opened.cursor();
let resp = server.references(&c.path, c.line, c.character, true).await;
assert!(resp["error"].is_null(), "references error: {resp:?}");
let hits: Vec<u32> = resp["result"]
.as_array()
.expect("array")
.iter()
.map(|l| l["range"]["start"]["line"].as_u64().unwrap() as u32)
.collect();
assert!(
hits.contains(&5),
"Beta::__construct decl (line 5) missing: {hits:?}"
);
assert!(
!hits.contains(&2),
"Alpha::__construct decl (line 2) must not appear: {hits:?}"
);
assert!(
hits.contains(&8),
"`new Beta(...)` (line 8) missing: {hits:?}"
);
assert!(
!hits.contains(&7),
"`new Alpha(...)` (line 7) must not appear: {hits:?}"
);
}
#[tokio::test]
async fn references_on_constructor_in_braced_namespace() {
let mut server = TestServer::new().await;
let opened = server
.open_fixture(
r#"<?php
namespace Shop {
class Order {
public function __con$0struct(int $id) {}
}
}
namespace Shop {
$o = new Order(1);
}
"#,
)
.await;
let c = opened.cursor();
let resp = server.references(&c.path, c.line, c.character, true).await;
assert!(resp["error"].is_null(), "references error: {resp:?}");
let hits: Vec<u32> = resp["result"]
.as_array()
.expect("array")
.iter()
.map(|l| l["range"]["start"]["line"].as_u64().unwrap() as u32)
.collect();
assert!(
hits.contains(&3),
"Order::__construct decl (line 3) missing: {hits:?}"
);
assert!(
hits.contains(&7),
"`new Order(1)` (line 7) missing: {hits:?}"
);
}
#[tokio::test]
async fn references_on_constructor_scoped_by_namespace_fqn() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("a.php"),
"<?php\nnamespace Alpha;\nclass Widget {\n public function __construct(int $x) {}\n}\n",
)
.unwrap();
std::fs::write(
dir.path().join("b.php"),
"<?php\nnamespace Beta;\nclass Widget {\n public function __construct(string $s) {}\n}\n",
)
.unwrap();
std::fs::write(
dir.path().join("c.php"),
"<?php\n$a = new \\Alpha\\Widget(1);\n$b = new \\Beta\\Widget('x');\n",
)
.unwrap();
let mut server = TestServer::with_root(dir.path()).await;
server.wait_for_index_ready().await;
let (text, _, _) = server.locate("a.php", "<?php", 0);
server.open("a.php", &text).await;
let (_, line, col) = server.locate("a.php", "__construct", 0);
let resp = server.references("a.php", line, col + 2, true).await;
assert!(resp["error"].is_null(), "references error: {resp:?}");
let c_uri = server.uri("c.php");
let b_uri = server.uri("b.php");
let hits: Vec<(String, u32)> = resp["result"]
.as_array()
.unwrap_or_else(|| panic!("expected array, got: {resp:?}"))
.iter()
.map(|l| {
(
l["uri"].as_str().unwrap().to_string(),
l["range"]["start"]["line"].as_u64().unwrap() as u32,
)
})
.collect();
assert!(
hits.contains(&(c_uri.clone(), 1)),
"`new \\Alpha\\Widget(1)` missing: {hits:?}"
);
assert!(
!hits.contains(&(c_uri.clone(), 2)),
"`new \\Beta\\Widget('x')` must not appear: {hits:?}"
);
assert!(
!hits.iter().any(|(u, _)| u == &b_uri),
"Beta::Widget::__construct must not appear: {hits:?}"
);
}
#[tokio::test]
async fn references_on_constructor_excludes_type_hints_and_instanceof() {
let mut server = TestServer::new().await;
let opened = server
.open_fixture(
r#"<?php
class Order {
public function __con$0struct(int $id) {}
}
// call site — must be included
$o = new Order(1);
// type hint — must NOT be included
function ship(Order $o): void {}
// instanceof — must NOT be included
if ($o instanceof Order) {}
// static call — must NOT be included
Order::class;
"#,
)
.await;
let c = opened.cursor();
let resp = server.references(&c.path, c.line, c.character, true).await;
assert!(resp["error"].is_null(), "references error: {resp:?}");
let hits: Vec<u32> = resp["result"]
.as_array()
.expect("expected array")
.iter()
.map(|l| l["range"]["start"]["line"].as_u64().unwrap() as u32)
.collect();
assert!(
hits.contains(&2),
"__construct decl (line 2) missing: {hits:?}"
);
assert!(
hits.contains(&5),
"`new Order(1)` (line 5) missing: {hits:?}"
);
assert!(
!hits.contains(&7),
"type hint on line 7 must be excluded: {hits:?}"
);
assert!(
!hits.contains(&9),
"`instanceof` on line 9 must be excluded: {hits:?}"
);
assert!(
!hits.contains(&11),
"`Order::class` on line 11 must be excluded: {hits:?}"
);
}
#[tokio::test]
async fn references_on_promoted_property_param_finds_property_accesses() {
let mut server = TestServer::new().await;
let opened = server
.open_fixture(
r#"<?php
class Person {
public function __construct(public readonly string $na$0me) {}
public function greet(): string { return $this->name; }
}
$p = new Person('Alice');
echo $p->name;
"#,
)
.await;
let c = opened.cursor();
let resp = server.references(&c.path, c.line, c.character, true).await;
assert!(resp["error"].is_null(), "references error: {resp:?}");
let hits: Vec<u32> = resp["result"]
.as_array()
.expect("expected array")
.iter()
.map(|l| l["range"]["start"]["line"].as_u64().unwrap() as u32)
.collect();
assert!(
hits.contains(&3),
"`$this->name` (line 3) missing: {hits:?}"
);
assert!(hits.contains(&6), "`$p->name` (line 6) missing: {hits:?}");
}
#[tokio::test]
async fn references_on_constructor_with_include_declaration_false() {
let mut server = TestServer::new().await;
let opened = server
.open_fixture(
r#"<?php
class Invoice {
public function __con$0struct(int $id) {}
}
$a = new Invoice(1);
$b = new Invoice(2);
"#,
)
.await;
let c = opened.cursor();
let resp = server.references(&c.path, c.line, c.character, false).await;
assert!(resp["error"].is_null(), "references error: {resp:?}");
let hits: Vec<u32> = resp["result"]
.as_array()
.expect("expected array")
.iter()
.map(|l| l["range"]["start"]["line"].as_u64().unwrap() as u32)
.collect();
assert!(
hits.contains(&4),
"`new Invoice(1)` (line 4) missing: {hits:?}"
);
assert!(
hits.contains(&5),
"`new Invoice(2)` (line 5) missing: {hits:?}"
);
assert!(
!hits.contains(&2),
"__construct decl (line 2) must be excluded when include_declaration=false: {hits:?}"
);
assert_eq!(hits.len(), 2, "expected exactly 2 call sites: {hits:?}");
}
#[tokio::test]
async fn references_on_promoted_property_cross_file() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("entity.php"),
"<?php\nclass User {\n public function __construct(public readonly string $email) {}\n}\n",
)
.unwrap();
std::fs::write(
dir.path().join("service.php"),
"<?php\nfunction notify(User $u): void {\n echo $u->email;\n echo $u?->email;\n}\n",
)
.unwrap();
let mut server = TestServer::with_root(dir.path()).await;
server.wait_for_index_ready().await;
let (text, _, _) = server.locate("entity.php", "<?php", 0);
server.open("entity.php", &text).await;
let (_, line, col) = server.locate("entity.php", "$email", 0);
let resp = server.references("entity.php", line, col + 1, false).await;
assert!(resp["error"].is_null(), "references error: {resp:?}");
let service_uri = server.uri("service.php");
let hits: Vec<(String, u32)> = resp["result"]
.as_array()
.unwrap_or_else(|| panic!("expected array: {resp:?}"))
.iter()
.map(|l| {
(
l["uri"].as_str().unwrap().to_string(),
l["range"]["start"]["line"].as_u64().unwrap() as u32,
)
})
.collect();
assert!(
hits.contains(&(service_uri.clone(), 2)),
"`$u->email` (service.php:2) missing: {hits:?}"
);
assert!(
hits.contains(&(service_uri.clone(), 3)),
"`$u?->email` (service.php:3) missing: {hits:?}"
);
}
#[tokio::test]
async fn parallel_warm_finds_all_references_across_many_files() {
let dir = tempfile::tempdir().unwrap();
let caller_count = 15usize;
std::fs::write(
dir.path().join("def.php"),
"<?php\nfunction target(): void {}",
)
.unwrap();
for i in 0..caller_count {
std::fs::write(
dir.path().join(format!("caller_{i}.php")),
"<?php\ntarget();",
)
.unwrap();
}
for i in 0..5usize {
std::fs::write(
dir.path().join(format!("other_{i}.php")),
format!("<?php\nfunction other_{i}() {{}}"),
)
.unwrap();
}
let mut server = TestServer::with_root(dir.path()).await;
server.wait_for_index_ready().await;
server
.open("def.php", "<?php\nfunction target(): void {}")
.await;
let resp = server.references("def.php", 1, 9, false).await;
assert!(resp["error"].is_null(), "references error: {resp:?}");
let locs = resp["result"].as_array().expect("expected array");
assert_eq!(
locs.len(),
caller_count,
"expected {caller_count} references, got {}: {locs:?}",
locs.len()
);
}
#[tokio::test]
async fn parallel_warm_gives_consistent_results_on_repeated_references_calls() {
let mut server = TestServer::new().await;
let opened = server
.open_fixture(
r#"//- /a.php
<?php
function fo$0o(): void {}
//- /b.php
<?php
foo();
//- /c.php
<?php
foo(); foo();
"#,
)
.await;
let c = opened.cursor();
let resp1 = server.references(&c.path, c.line, c.character, false).await;
let resp2 = server.references(&c.path, c.line, c.character, false).await;
let locs1 = resp1["result"].as_array().expect("array");
let locs2 = resp2["result"].as_array().expect("array");
assert_eq!(
locs1.len(),
3,
"expected 3 references (1 from b.php, 2 from c.php): {locs1:?}"
);
assert_eq!(
locs1.len(),
locs2.len(),
"repeated references calls returned different counts"
);
}
#[tokio::test]
async fn references_finds_all_usages_of_function() {
let mut server = TestServer::new().await;
let opened = server
.open_fixture(
r#"<?php
function a$0dd(int $a, int $b): int { return $a + $b; }
add(1, 2);
add(3, 4);
"#,
)
.await;
let c = opened.cursor();
let resp = server.references(&c.path, c.line, c.character, true).await;
assert!(resp["error"].is_null(), "references error: {resp:?}");
let locs = resp["result"].as_array().expect("array");
assert_eq!(
locs.len(),
3,
"expected 3 refs (1 decl + 2 calls): {locs:?}"
);
let lines = lines_of(locs);
assert!(lines.contains(&1), "decl line 1 missing");
assert!(lines.contains(&2), "call line 2 missing");
assert!(lines.contains(&3), "call line 3 missing");
}