use crate::Backend;
use tower_lsp::LanguageServer;
use tower_lsp::lsp_types::*;
async fn open_file(backend: &Backend, uri: &Url, text: &str) {
let open_params = DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: uri.clone(),
language_id: "php".to_string(),
version: 1,
text: text.to_string(),
},
};
backend.did_open(open_params).await;
}
async fn find_references(
backend: &Backend,
uri: &Url,
line: u32,
character: u32,
include_declaration: bool,
) -> Vec<Location> {
let params = ReferenceParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position { line, character },
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: ReferenceContext {
include_declaration,
},
};
backend
.references(params)
.await
.unwrap()
.unwrap_or_default()
}
#[tokio::test]
async fn test_variable_references_same_scope() {
let backend = Backend::new_test();
let uri = Url::parse("file:///test.php").unwrap();
let text = concat!(
"<?php\n", "function demo(): void {\n", " $user = new User();\n", " $user->name = 'Alice';\n", " echo $user->name;\n", "}\n", );
open_file(&backend, &uri, text).await;
let locs = find_references(&backend, &uri, 3, 5, true).await;
assert!(
locs.len() >= 3,
"Expected at least 3 references to $user, got {}",
locs.len()
);
for loc in &locs {
assert_eq!(loc.uri, uri);
}
}
#[tokio::test]
async fn test_variable_references_excludes_other_scope() {
let backend = Backend::new_test();
let uri = Url::parse("file:///test.php").unwrap();
let text = concat!(
"<?php\n", "function alpha(): void {\n", " $x = 1;\n", " echo $x;\n", "}\n", "function beta(): void {\n", " $x = 2;\n", " echo $x;\n", "}\n", );
open_file(&backend, &uri, text).await;
let locs = find_references(&backend, &uri, 2, 5, true).await;
for loc in &locs {
assert!(
loc.range.start.line <= 4,
"Reference to $x in alpha() should not appear in beta() (line {})",
loc.range.start.line
);
}
}
#[tokio::test]
async fn test_variable_references_exclude_declaration() {
let backend = Backend::new_test();
let uri = Url::parse("file:///test.php").unwrap();
let text = concat!(
"<?php\n", "function demo(): void {\n", " $val = 42;\n", " echo $val;\n", " $val = 99;\n", "}\n", );
open_file(&backend, &uri, text).await;
let locs_no_decl = find_references(&backend, &uri, 3, 10, false).await;
let locs_with_decl = find_references(&backend, &uri, 3, 10, true).await;
assert!(
locs_with_decl.len() >= locs_no_decl.len(),
"with_decl ({}) should be >= no_decl ({})",
locs_with_decl.len(),
locs_no_decl.len()
);
}
#[tokio::test]
async fn test_class_references_same_file() {
let backend = Backend::new_test();
let uri = Url::parse("file:///test.php").unwrap();
let text = concat!(
"<?php\n", "class Logger {\n", " public function info(): void {}\n", "}\n", "class Service {\n", " public function run(Logger $l): void {\n", " $x = new Logger();\n", " }\n", "}\n", );
open_file(&backend, &uri, text).await;
let locs = find_references(&backend, &uri, 5, 27, true).await;
assert!(
locs.len() >= 2,
"Expected at least 2 references to Logger, got {}",
locs.len()
);
}
#[tokio::test]
async fn test_class_references_exclude_declaration() {
let backend = Backend::new_test();
let uri = Url::parse("file:///test.php").unwrap();
let text = concat!(
"<?php\n", "class Foo {}\n", "class Bar {\n", " public function test(Foo $f): Foo {\n", " return new Foo();\n", " }\n", "}\n", );
open_file(&backend, &uri, text).await;
let locs = find_references(&backend, &uri, 3, 25, false).await;
for loc in &locs {
assert_ne!(
loc.range.start.line, 1,
"Should not include declaration site when include_declaration=false"
);
}
let locs_decl = find_references(&backend, &uri, 3, 25, true).await;
let has_decl = locs_decl.iter().any(|l| l.range.start.line == 1);
assert!(
has_decl,
"Should include declaration site when include_declaration=true"
);
}
#[tokio::test]
async fn test_class_declaration_finds_references() {
let backend = Backend::new_test();
let uri = Url::parse("file:///test.php").unwrap();
let text = concat!(
"<?php\n", "class Widget {}\n", "function make(): Widget {\n", " return new Widget();\n", "}\n", );
open_file(&backend, &uri, text).await;
let locs = find_references(&backend, &uri, 1, 7, true).await;
assert!(
locs.len() >= 3,
"Expected at least 3 references (decl + 2 usages), got {}",
locs.len()
);
}
#[tokio::test]
async fn test_method_references_same_file() {
let backend = Backend::new_test();
let uri = Url::parse("file:///test.php").unwrap();
let text = concat!(
"<?php\n", "class Repo {\n", " public function find(int $id): void {}\n", "}\n", "class Controller {\n", " public function index(Repo $r): void {\n", " $r->find(1);\n", " $r->find(2);\n", " }\n", "}\n", );
open_file(&backend, &uri, text).await;
let locs = find_references(&backend, &uri, 6, 14, false).await;
assert!(
locs.len() >= 2,
"Expected at least 2 references to find(), got {}",
locs.len()
);
}
#[tokio::test]
async fn test_method_references_include_declaration() {
let backend = Backend::new_test();
let uri = Url::parse("file:///test.php").unwrap();
let text = concat!(
"<?php\n", "class Repo {\n", " public function save(): void {}\n", "}\n", "function demo(Repo $r): void {\n", " $r->save();\n", "}\n", );
open_file(&backend, &uri, text).await;
let locs = find_references(&backend, &uri, 5, 10, true).await;
let has_def = locs.iter().any(|l| l.range.start.line == 2);
assert!(
has_def,
"Should include method declaration when include_declaration=true"
);
}
#[tokio::test]
async fn test_static_method_references() {
let backend = Backend::new_test();
let uri = Url::parse("file:///test.php").unwrap();
let text = concat!(
"<?php\n", "class Factory {\n", " public static function create(): void {}\n", "}\n", "function demo(): void {\n", " Factory::create();\n", " Factory::create();\n", "}\n", );
open_file(&backend, &uri, text).await;
let locs = find_references(&backend, &uri, 5, 15, false).await;
assert!(
locs.len() >= 2,
"Expected at least 2 references to create(), got {}",
locs.len()
);
}
#[tokio::test]
async fn test_property_references() {
let backend = Backend::new_test();
let uri = Url::parse("file:///test.php").unwrap();
let text = concat!(
"<?php\n", "class Config {\n", " public string $name = '';\n", "}\n", "function demo(Config $c): void {\n", " echo $c->name;\n", " $c->name = 'test';\n", "}\n", );
open_file(&backend, &uri, text).await;
let locs = find_references(&backend, &uri, 5, 15, false).await;
assert!(
locs.len() >= 2,
"Expected at least 2 references to ->name, got {}",
locs.len()
);
}
#[tokio::test]
async fn test_function_call_references() {
let backend = Backend::new_test();
let uri = Url::parse("file:///test.php").unwrap();
let text = concat!(
"<?php\n", "function helper(): void {}\n", "function main(): void {\n", " helper();\n", " helper();\n", "}\n", );
open_file(&backend, &uri, text).await;
let locs = find_references(&backend, &uri, 3, 6, false).await;
assert!(
locs.len() >= 2,
"Expected at least 2 references to helper(), got {}",
locs.len()
);
}
#[tokio::test]
async fn test_function_references_include_declaration() {
let backend = Backend::new_test();
let uri = Url::parse("file:///test.php").unwrap();
let text = concat!(
"<?php\n", "function myFunc(): int { return 1; }\n", "function demo(): void {\n", " $x = myFunc();\n", "}\n", );
open_file(&backend, &uri, text).await;
let locs = find_references(&backend, &uri, 3, 11, true).await;
let has_def = locs.iter().any(|l| l.range.start.line == 1);
assert!(
has_def,
"Should include function declaration when include_declaration=true"
);
}
#[tokio::test]
async fn test_constant_references() {
let backend = Backend::new_test();
let uri = Url::parse("file:///test.php").unwrap();
let text = concat!(
"<?php\n", "class Status {\n", " const ACTIVE = 1;\n", "}\n", "function demo(): void {\n", " echo Status::ACTIVE;\n", " $x = Status::ACTIVE;\n", "}\n", );
open_file(&backend, &uri, text).await;
let locs = find_references(&backend, &uri, 5, 20, false).await;
assert!(
locs.len() >= 2,
"Expected at least 2 references to ACTIVE, got {}",
locs.len()
);
}
#[tokio::test]
async fn test_self_references() {
let backend = Backend::new_test();
let uri = Url::parse("file:///test.php").unwrap();
let text = concat!(
"<?php\n", "class Item {\n", " public static function create(): self {\n", " return new self();\n", " }\n", "}\n", "function demo(): void {\n", " $x = new Item();\n", "}\n", );
open_file(&backend, &uri, text).await;
let locs = find_references(&backend, &uri, 3, 20, true).await;
assert!(
!locs.is_empty(),
"Expected references when clicking on self"
);
}
#[tokio::test]
async fn test_class_references_cross_file() {
let backend = Backend::new_test();
let uri_a = Url::parse("file:///a.php").unwrap();
let uri_b = Url::parse("file:///b.php").unwrap();
let text_a = concat!(
"<?php\n", "class Animal {}\n", );
let text_b = concat!(
"<?php\n", "class Zoo {\n", " public function add(Animal $a): void {}\n", " public function get(): Animal {\n", " return new Animal();\n", " }\n", "}\n", );
open_file(&backend, &uri_a, text_a).await;
open_file(&backend, &uri_b, text_b).await;
let locs = find_references(&backend, &uri_a, 1, 7, true).await;
let in_a = locs.iter().filter(|l| l.uri == uri_a).count();
let in_b = locs.iter().filter(|l| l.uri == uri_b).count();
assert!(
in_a >= 1,
"Expected at least 1 reference in a.php, got {}",
in_a
);
assert!(
in_b >= 1,
"Expected at least 1 reference in b.php, got {}",
in_b
);
}
#[tokio::test]
async fn test_member_references_cross_file() {
let backend = Backend::new_test();
let uri_a = Url::parse("file:///a.php").unwrap();
let uri_b = Url::parse("file:///b.php").unwrap();
let text_a = concat!(
"<?php\n", "class Printer {\n", " public function print(): void {}\n", "}\n", "function useA(Printer $p): void {\n", " $p->print();\n", "}\n", );
let text_b = concat!(
"<?php\n", "function useB(Printer $p): void {\n", " $p->print();\n", "}\n", );
open_file(&backend, &uri_a, text_a).await;
open_file(&backend, &uri_b, text_b).await;
let locs = find_references(&backend, &uri_a, 5, 10, false).await;
let in_a = locs.iter().filter(|l| l.uri == uri_a).count();
let in_b = locs.iter().filter(|l| l.uri == uri_b).count();
assert!(
in_a >= 1,
"Expected at least 1 reference in a.php, got {}",
in_a
);
assert!(
in_b >= 1,
"Expected at least 1 reference in b.php, got {}",
in_b
);
}
#[tokio::test]
async fn test_namespaced_class_references() {
let backend = Backend::new_test();
let uri_a = Url::parse("file:///a.php").unwrap();
let uri_b = Url::parse("file:///b.php").unwrap();
let text_a = concat!(
"<?php\n", "namespace App\\Models;\n", "class User {}\n", );
let text_b = concat!(
"<?php\n", "namespace App\\Services;\n", "use App\\Models\\User;\n", "class UserService {\n", " public function find(): User {\n", " return new User();\n", " }\n", "}\n", );
open_file(&backend, &uri_a, text_a).await;
open_file(&backend, &uri_b, text_b).await;
let locs = find_references(&backend, &uri_a, 2, 7, true).await;
let in_b = locs.iter().filter(|l| l.uri == uri_b).count();
assert!(
in_b >= 1,
"Expected at least 1 cross-file namespaced reference in b.php, got {}",
in_b
);
}
#[tokio::test]
async fn test_no_references_on_whitespace() {
let backend = Backend::new_test();
let uri = Url::parse("file:///test.php").unwrap();
let text = concat!(
"<?php\n", "\n", "class Foo {}\n", );
open_file(&backend, &uri, text).await;
let params = ReferenceParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 1,
character: 0,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: ReferenceContext {
include_declaration: true,
},
};
let result = backend.references(params).await.unwrap();
assert!(
result.is_none() || result.as_ref().unwrap().is_empty(),
"Expected no references on whitespace"
);
}
#[tokio::test]
async fn test_variable_parameter_reference() {
let backend = Backend::new_test();
let uri = Url::parse("file:///test.php").unwrap();
let text = concat!(
"<?php\n", "function greet(string $name): string {\n", " return 'Hello ' . $name;\n", "}\n", );
open_file(&backend, &uri, text).await;
let locs = find_references(&backend, &uri, 2, 25, true).await;
assert!(
locs.len() >= 2,
"Expected at least 2 references (param + usage), got {}",
locs.len()
);
}
#[tokio::test]
async fn test_results_sorted_by_position() {
let backend = Backend::new_test();
let uri = Url::parse("file:///test.php").unwrap();
let text = concat!(
"<?php\n", "class X {}\n", "function a(X $x): X { return new X(); }\n", "function b(X $x): X { return new X(); }\n", );
open_file(&backend, &uri, text).await;
let locs = find_references(&backend, &uri, 2, 12, true).await;
for window in locs.windows(2) {
let a = &window[0];
let b = &window[1];
let a_before_b = (a.uri.as_str(), a.range.start.line, a.range.start.character)
<= (b.uri.as_str(), b.range.start.line, b.range.start.character);
assert!(
a_before_b,
"Results should be sorted: {:?} should come before {:?}",
a.range.start, b.range.start
);
}
}
#[tokio::test]
async fn test_class_extends_references() {
let backend = Backend::new_test();
let uri = Url::parse("file:///test.php").unwrap();
let text = concat!(
"<?php\n", "class Base {}\n", "class Child extends Base {}\n", "function demo(Base $b): void {}\n", );
open_file(&backend, &uri, text).await;
let locs = find_references(&backend, &uri, 1, 7, true).await;
assert!(
locs.len() >= 3,
"Expected at least 3 references (decl + extends + param), got {}",
locs.len()
);
}
#[tokio::test]
async fn test_interface_implements_references() {
let backend = Backend::new_test();
let uri = Url::parse("file:///test.php").unwrap();
let text = concat!(
"<?php\n", "interface Loggable {}\n", "class FileLogger implements Loggable {}\n", "function log(Loggable $l): void {}\n", );
open_file(&backend, &uri, text).await;
let locs = find_references(&backend, &uri, 1, 12, true).await;
assert!(
locs.len() >= 3,
"Expected at least 3 references (decl + implements + param), got {}",
locs.len()
);
}
#[tokio::test]
async fn test_foreach_variable_references() {
let backend = Backend::new_test();
let uri = Url::parse("file:///test.php").unwrap();
let text = concat!(
"<?php\n", "function demo(): void {\n", " $items = [1, 2, 3];\n", " foreach ($items as $item) {\n", " echo $item;\n", " echo $item + 1;\n", " }\n", "}\n", );
open_file(&backend, &uri, text).await;
let locs = find_references(&backend, &uri, 4, 14, true).await;
assert!(
locs.len() >= 2,
"Expected at least 2 references to $item (foreach var + usages), got {}",
locs.len()
);
}
#[tokio::test]
async fn test_static_property_references() {
let backend = Backend::new_test();
let uri = Url::parse("file:///test.php").unwrap();
let text = concat!(
"<?php\n", "class Counter {\n", " public static int $count = 0;\n", " public static function increment(): void {\n", " self::$count++;\n", " }\n", "}\n", "function demo(): void {\n", " Counter::$count = 5;\n", "}\n", );
open_file(&backend, &uri, text).await;
let locs = find_references(&backend, &uri, 4, 16, false).await;
assert!(
locs.len() >= 2,
"Expected at least 2 references to static $count, got {}",
locs.len()
);
}
#[tokio::test]
async fn test_this_property_references() {
let backend = Backend::new_test();
let uri = Url::parse("file:///test.php").unwrap();
let text = concat!(
"<?php\n", "class Person {\n", " public string $email = '';\n", " public function setEmail(string $email): void {\n", " $this->email = $email;\n", " }\n", " public function getEmail(): string {\n", " return $this->email;\n", " }\n", "}\n", );
open_file(&backend, &uri, text).await;
let locs = find_references(&backend, &uri, 4, 17, false).await;
assert!(
locs.len() >= 2,
"Expected at least 2 references to ->email, got {}",
locs.len()
);
}
#[tokio::test]
async fn test_multiple_files_function_references() {
let backend = Backend::new_test();
let uri_a = Url::parse("file:///helpers.php").unwrap();
let uri_b = Url::parse("file:///main.php").unwrap();
let text_a = concat!(
"<?php\n", "function format_name(string $name): string {\n", " return ucfirst($name);\n", "}\n", );
let text_b = concat!(
"<?php\n", "function demo(): void {\n", " $x = format_name('alice');\n", " $y = format_name('bob');\n", "}\n", );
open_file(&backend, &uri_a, text_a).await;
open_file(&backend, &uri_b, text_b).await;
let locs = find_references(&backend, &uri_b, 2, 11, false).await;
assert!(
locs.len() >= 2,
"Expected at least 2 call-site references across files, got {}",
locs.len()
);
}
#[tokio::test]
async fn test_this_is_file_local_not_cross_file() {
let backend = Backend::new_test();
let uri_a = Url::parse("file:///a.php").unwrap();
let uri_b = Url::parse("file:///b.php").unwrap();
let text_a = concat!(
"<?php\n", "class Foo {\n", " public function bar(): void {\n", " $this->baz();\n", " }\n", "}\n", );
let text_b = concat!(
"<?php\n", "function demo(Foo $f): void {\n", " $f->baz();\n", "}\n", );
open_file(&backend, &uri_a, text_a).await;
open_file(&backend, &uri_b, text_b).await;
let locs = find_references(&backend, &uri_a, 3, 9, true).await;
for loc in &locs {
assert_eq!(
loc.uri, uri_a,
"$this references should stay within the current file, but found one in {}",
loc.uri
);
}
}
#[tokio::test]
async fn test_this_references_within_class() {
let backend = Backend::new_test();
let uri = Url::parse("file:///test.php").unwrap();
let text = concat!(
"<?php\n", "class Account {\n", " public string $name = '';\n", " public function getName(): string {\n", " return $this->name;\n", " }\n", " public function setName(string $n): void {\n", " $this->name = $n;\n", " }\n", " public function self_ref(): self {\n", " return $this;\n", " }\n", "}\n", );
open_file(&backend, &uri, text).await;
let locs = find_references(&backend, &uri, 4, 16, true).await;
assert!(
locs.len() >= 3,
"Expected at least 3 $this references in Account, got {}",
locs.len()
);
for loc in &locs {
assert_eq!(loc.uri, uri);
}
}
#[tokio::test]
async fn test_this_scoped_to_enclosing_class() {
let backend = Backend::new_test();
let uri = Url::parse("file:///test.php").unwrap();
let text = concat!(
"<?php\n", "class Alpha {\n", " public function go(): void {\n", " $this->run();\n", " }\n", "}\n", "class Beta {\n", " public function go(): void {\n", " $this->run();\n", " }\n", "}\n", );
open_file(&backend, &uri, text).await;
let locs = find_references(&backend, &uri, 3, 9, true).await;
for loc in &locs {
assert!(
loc.range.start.line < 5,
"$this in Alpha should not include Beta's $this on line {}",
loc.range.start.line
);
}
assert!(!locs.is_empty(), "Should find at least one $this in Alpha");
}
#[tokio::test]
async fn test_method_declaration_triggers_references() {
let backend = Backend::new_test();
let uri = Url::parse("file:///test.php").unwrap();
let text = concat!(
"<?php\n", "class Converter {\n", " public static function toListOfString(iterable $values): array\n", " {\n", " self::toListOfString($values);\n", " }\n", "}\n", );
open_file(&backend, &uri, text).await;
let locs = find_references(&backend, &uri, 2, 30, true).await;
assert!(
locs.len() >= 2,
"Clicking on method declaration should find references; got {} locations",
locs.len()
);
let has_call = locs.iter().any(|l| l.range.start.line == 4);
assert!(
has_call,
"Should include the self::toListOfString call on line 4"
);
}
#[tokio::test]
async fn test_property_declaration_triggers_references() {
let backend = Backend::new_test();
let uri = Url::parse("file:///test.php").unwrap();
let text = concat!(
"<?php\n", "class Box {\n", " public int $size = 0;\n", " public function grow(): void {\n", " $this->size++;\n", " }\n", "}\n", );
open_file(&backend, &uri, text).await;
let locs = find_references(&backend, &uri, 2, 16, true).await;
assert!(
locs.len() >= 2,
"Clicking on property declaration should find references; got {} locations",
locs.len()
);
let has_usage = locs.iter().any(|l| l.range.start.line == 4);
assert!(has_usage, "Should include the $this->size usage on line 4");
}
#[tokio::test]
async fn test_constant_declaration_triggers_references() {
let backend = Backend::new_test();
let uri = Url::parse("file:///test.php").unwrap();
let text = concat!(
"<?php\n", "class Limit {\n", " const MAX = 100;\n", " public function check(): bool {\n", " return self::MAX > 0;\n", " }\n", "}\n", );
open_file(&backend, &uri, text).await;
let locs = find_references(&backend, &uri, 2, 11, true).await;
assert!(
locs.len() >= 2,
"Clicking on constant declaration should find references; got {} locations",
locs.len()
);
let has_usage = locs.iter().any(|l| l.range.start.line == 4);
assert!(has_usage, "Should include the self::MAX usage on line 4");
}
#[tokio::test]
async fn test_method_declaration_cross_file() {
let backend = Backend::new_test();
let uri_a = Url::parse("file:///a.php").unwrap();
let uri_b = Url::parse("file:///b.php").unwrap();
let text_a = concat!(
"<?php\n", "class Formatter {\n", " public function format(string $s): string\n", " {\n", " return $s;\n", " }\n", "}\n", );
let text_b = concat!(
"<?php\n", "function demo(Formatter $f): void {\n", " $f->format('hello');\n", "}\n", );
open_file(&backend, &uri_a, text_a).await;
open_file(&backend, &uri_b, text_b).await;
let locs = find_references(&backend, &uri_a, 2, 23, true).await;
let in_b = locs.iter().filter(|l| l.uri == uri_b).count();
assert!(
in_b >= 1,
"Method declaration should find cross-file call site; got {} in b.php",
in_b
);
}
#[tokio::test]
async fn test_unrelated_class_same_method_excluded() {
let backend = Backend::new_test();
let uri = Url::parse("file:///test.php").unwrap();
let text = concat!(
"<?php\n", "class MyClass {\n", " public function save(): void {}\n", "}\n", "class OtherClass {\n", " public function save(): void {}\n", "}\n", "function demo(MyClass $a, OtherClass $b): void {\n", " $a->save();\n", " $b->save();\n", "}\n", );
open_file(&backend, &uri, text).await;
let locs = find_references(&backend, &uri, 8, 10, false).await;
let lines: Vec<u32> = locs.iter().map(|l| l.range.start.line).collect();
assert!(
lines.contains(&8),
"Should find $a->save() on L8; got lines: {:?}",
lines
);
assert!(
!lines.contains(&9),
"Should NOT find $b->save() on L9 (unrelated class); got lines: {:?}",
lines
);
}
#[tokio::test]
async fn test_unrelated_class_same_method_excluded_cross_file() {
let backend = Backend::new_test();
let uri_a = Url::parse("file:///a.php").unwrap();
let uri_b = Url::parse("file:///b.php").unwrap();
let text_a = concat!(
"<?php\n", "class MyClass {\n", " public function save(): void {}\n", "}\n", "class OtherClass {\n", " public function save(): void {}\n", "}\n", );
let text_b = concat!(
"<?php\n", "function useMyClass(MyClass $m): void {\n", " $m->save();\n", "}\n", "function useOtherClass(OtherClass $o): void {\n", " $o->save();\n", "}\n", );
open_file(&backend, &uri_a, text_a).await;
open_file(&backend, &uri_b, text_b).await;
let locs = find_references(&backend, &uri_a, 2, 21, true).await;
let b_lines: Vec<u32> = locs
.iter()
.filter(|l| l.uri == uri_b)
.map(|l| l.range.start.line)
.collect();
assert!(
b_lines.contains(&2),
"Should find $m->save() on L2 of b.php; got lines: {:?}",
b_lines
);
assert!(
!b_lines.contains(&5),
"Should NOT find $o->save() on L5 of b.php (unrelated class); got lines: {:?}",
b_lines
);
let a_lines: Vec<u32> = locs
.iter()
.filter(|l| l.uri == uri_a)
.map(|l| l.range.start.line)
.collect();
assert!(
a_lines.contains(&2),
"Should include MyClass::save() declaration on L2 of a.php; got: {:?}",
a_lines
);
assert!(
!a_lines.contains(&5),
"Should NOT include OtherClass::save() declaration on L5 of a.php; got: {:?}",
a_lines
);
}
#[tokio::test]
async fn test_inherited_method_references_included() {
let backend = Backend::new_test();
let uri = Url::parse("file:///test.php").unwrap();
let text = concat!(
"<?php\n", "class Base {\n", " public function save(): void {}\n", "}\n", "class Child extends Base {}\n", "function demo(Base $a, Child $b): void {\n", " $a->save();\n", " $b->save();\n", "}\n", );
open_file(&backend, &uri, text).await;
let locs = find_references(&backend, &uri, 6, 10, false).await;
let lines: Vec<u32> = locs.iter().map(|l| l.range.start.line).collect();
assert!(
lines.contains(&6),
"Should find $a->save() on L6; got lines: {:?}",
lines
);
assert!(
lines.contains(&7),
"Should find $b->save() on L7 (Child extends Base); got lines: {:?}",
lines
);
}
#[tokio::test]
async fn test_interface_method_references_included() {
let backend = Backend::new_test();
let uri = Url::parse("file:///test.php").unwrap();
let text = concat!(
"<?php\n", "interface Saveable {\n", " public function save(): void;\n", "}\n", "class Record implements Saveable {\n", " public function save(): void {}\n", "}\n", "function demo(Saveable $s, Record $r): void {\n", " $s->save();\n", " $r->save();\n", "}\n", );
open_file(&backend, &uri, text).await;
let locs = find_references(&backend, &uri, 8, 10, false).await;
let lines: Vec<u32> = locs.iter().map(|l| l.range.start.line).collect();
assert!(
lines.contains(&8),
"Should find $s->save() on L8; got lines: {:?}",
lines
);
assert!(
lines.contains(&9),
"Should find $r->save() on L9 (Record implements Saveable); got lines: {:?}",
lines
);
}
#[tokio::test]
async fn test_static_method_unrelated_class_excluded() {
let backend = Backend::new_test();
let uri = Url::parse("file:///test.php").unwrap();
let text = concat!(
"<?php\n", "class Alpha {\n", " public static function create(): void {}\n", "}\n", "class Beta {\n", " public static function create(): void {}\n", "}\n", "function demo(): void {\n", " Alpha::create();\n", " Beta::create();\n", "}\n", );
open_file(&backend, &uri, text).await;
let locs = find_references(&backend, &uri, 8, 14, false).await;
let lines: Vec<u32> = locs.iter().map(|l| l.range.start.line).collect();
assert!(
lines.contains(&8),
"Should find Alpha::create() on L8; got lines: {:?}",
lines
);
assert!(
!lines.contains(&9),
"Should NOT find Beta::create() on L9 (unrelated class); got lines: {:?}",
lines
);
}
#[tokio::test]
async fn test_self_static_method_references_scoped() {
let backend = Backend::new_test();
let uri = Url::parse("file:///test.php").unwrap();
let text = concat!(
"<?php\n", "class Foo {\n", " public static function build(): void {}\n", " public function demo(): void {\n", " self::build();\n", " }\n", "}\n", "class Bar {\n", " public static function build(): void {}\n", " public function demo(): void {\n", " self::build();\n", " }\n", "}\n", );
open_file(&backend, &uri, text).await;
let locs = find_references(&backend, &uri, 4, 16, false).await;
let lines: Vec<u32> = locs.iter().map(|l| l.range.start.line).collect();
assert!(
lines.contains(&4),
"Should find self::build() on L4 (inside Foo); got lines: {:?}",
lines
);
assert!(
!lines.contains(&10),
"Should NOT find self::build() on L10 (inside Bar, unrelated); got lines: {:?}",
lines
);
}
#[tokio::test]
async fn test_unresolvable_variable_included_conservatively() {
let backend = Backend::new_test();
let uri = Url::parse("file:///test.php").unwrap();
let text = concat!(
"<?php\n", "class MyClass {\n", " public function save(): void {}\n", "}\n", "function demo(MyClass $a, $unknown): void {\n", " $a->save();\n", " $unknown->save();\n", "}\n", );
open_file(&backend, &uri, text).await;
let locs = find_references(&backend, &uri, 5, 10, false).await;
let lines: Vec<u32> = locs.iter().map(|l| l.range.start.line).collect();
assert!(
lines.contains(&5),
"Should find $a->save() on L5; got lines: {:?}",
lines
);
assert!(
lines.contains(&6),
"Should conservatively include $unknown->save() on L6 (unresolvable type); got lines: {:?}",
lines
);
}
#[tokio::test]
async fn test_this_method_references_excludes_unrelated() {
let backend = Backend::new_test();
let uri = Url::parse("file:///test.php").unwrap();
let text = concat!(
"<?php\n", "class Dog {\n", " public function speak(): void {}\n", " public function demo(): void {\n", " $this->speak();\n", " }\n", "}\n", "class Cat {\n", " public function speak(): void {}\n", " public function demo(): void {\n", " $this->speak();\n", " }\n", "}\n", "function outside(Dog $d, Cat $c): void {\n", " $d->speak();\n", " $c->speak();\n", "}\n", );
open_file(&backend, &uri, text).await;
let locs = find_references(&backend, &uri, 14, 10, true).await;
let lines: Vec<u32> = locs.iter().map(|l| l.range.start.line).collect();
assert!(
lines.contains(&14),
"Should find $d->speak() on L14; got lines: {:?}",
lines
);
assert!(
lines.contains(&2),
"Should include Dog::speak() declaration on L2; got lines: {:?}",
lines
);
assert!(
lines.contains(&4),
"Should include $this->speak() inside Dog on L4; got lines: {:?}",
lines
);
assert!(
!lines.contains(&8),
"Should NOT include Cat::speak() declaration on L8; got lines: {:?}",
lines
);
assert!(
!lines.contains(&10),
"Should NOT include $this->speak() inside Cat on L10; got lines: {:?}",
lines
);
assert!(
!lines.contains(&15),
"Should NOT include $c->speak() on L15 (unrelated class); got lines: {:?}",
lines
);
}