use crate::common::{create_psr4_workspace, create_test_backend};
use tower_lsp::LanguageServer;
use tower_lsp::lsp_types::*;
#[tokio::test]
async fn test_goto_definition_ambiguous_variable_if_block() {
let backend = create_test_backend();
let uri = Url::parse("file:///test.php").unwrap();
let text = concat!(
"<?php\n", "class SessionManager {\n", " public function callCustomCreator2(): void {}\n", " public function start(): void {}\n", "}\n", "\n", "class Manager {\n", " public function doWork(): void {}\n", "}\n", "\n", "class App {\n", " public function run(): void {\n", " $thing = new SessionManager();\n", " if ($thing->callCustomCreator2()) {\n", " $thing = new Manager();\n", " }\n", " $thing->callCustomCreator2();\n", " }\n", "}\n", );
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;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 16,
character: 20,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result = backend.goto_definition(params).await.unwrap();
assert!(
result.is_some(),
"Should resolve $thing->callCustomCreator2() via SessionManager even though Manager was assigned in if-block"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 2,
"callCustomCreator2 is declared on line 2 in SessionManager"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_ambiguous_variable_both_have_method() {
let backend = create_test_backend();
let uri = Url::parse("file:///test.php").unwrap();
let text = concat!(
"<?php\n", "class Alpha {\n", " public function greet(): void {}\n", "}\n", "\n", "class Beta {\n", " public function greet(): void {}\n", "}\n", "\n", "class App {\n", " public function run(): void {\n", " $obj = new Alpha();\n", " if (true) {\n", " $obj = new Beta();\n", " }\n", " $obj->greet();\n", " }\n", "}\n", );
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;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 15,
character: 16,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result = backend.goto_definition(params).await.unwrap();
assert!(
result.is_some(),
"Should resolve $obj->greet() when both Alpha and Beta have greet()"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 2,
"greet() should resolve to Alpha (line 2) as the first candidate"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_unconditional_reassignment_replaces_type() {
let backend = create_test_backend();
let uri = Url::parse("file:///test.php").unwrap();
let text = concat!(
"<?php\n", "class Foo {\n", " public function fooOnly(): void {}\n", "}\n", "\n", "class Bar {\n", " public function barOnly(): void {}\n", "}\n", "\n", "class App {\n", " public function run(): void {\n", " $x = new Foo();\n", " $x = new Bar();\n", " $x->barOnly();\n", " }\n", "}\n", );
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;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 13,
character: 16,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result = backend.goto_definition(params).await.unwrap();
assert!(
result.is_some(),
"Should resolve $x->barOnly() to Bar::barOnly"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 6,
"barOnly is declared on line 6 in Bar"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
let params2 = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 13,
character: 16,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result2 = backend.goto_definition(params2).await.unwrap();
assert!(result2.is_some());
match result2.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_ne!(
location.range.start.line, 2,
"fooOnly on line 2 (Foo) should NOT be reachable after unconditional reassignment"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_ambiguous_variable_try_catch() {
let backend = create_test_backend();
let uri = Url::parse("file:///test.php").unwrap();
let text = concat!(
"<?php\n", "class Logger {\n", " public function log(string $msg): void {}\n", "}\n", "\n", "class NullLogger {\n", " public function silence(): void {}\n", "}\n", "\n", "class App {\n", " public function run(): void {\n", " $logger = new Logger();\n", " try {\n", " $logger = new NullLogger();\n", " } catch (\\Exception $e) {\n", " }\n", " $logger->log('hello');\n", " }\n", "}\n", );
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;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 16,
character: 20,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result = backend.goto_definition(params).await.unwrap();
assert!(
result.is_some(),
"Should resolve $logger->log() via Logger even though NullLogger was assigned in try block"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 2,
"log() is declared on line 2 in Logger"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_ambiguous_variable_if_else_branches() {
let backend = create_test_backend();
let uri = Url::parse("file:///test.php").unwrap();
let text = concat!(
"<?php\n", "class Writer {\n", " public function write(): void {}\n", "}\n", "\n", "class Printer {\n", " public function print(): void {}\n", "}\n", "\n", "class Sender {\n", " public function send(): void {}\n", "}\n", "\n", "class App {\n", " public function run(): void {\n", " $out = new Writer();\n", " if (true) {\n", " $out = new Printer();\n", " } else {\n", " $out = new Sender();\n", " }\n", " $out->write();\n", " }\n", "}\n", );
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;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 21,
character: 16,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result = backend.goto_definition(params).await.unwrap();
assert!(
result.is_some(),
"Should resolve $out->write() via Writer even with if/else reassignments"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 2,
"write() is declared on line 2 in Writer"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_ambiguous_variable_loop() {
let backend = create_test_backend();
let uri = Url::parse("file:///test.php").unwrap();
let text = concat!(
"<?php\n", "class Handler {\n", " public function handle(): void {}\n", "}\n", "\n", "class Fallback {\n", " public function fallback(): void {}\n", "}\n", "\n", "class App {\n", " public function run(): void {\n", " $h = new Handler();\n", " while (true) {\n", " $h = new Fallback();\n", " }\n", " $h->handle();\n", " }\n", "}\n", );
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;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 15,
character: 14,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result = backend.goto_definition(params).await.unwrap();
assert!(
result.is_some(),
"Should resolve $h->handle() via Handler even though Fallback was assigned in while loop"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 2,
"handle() is declared on line 2 in Handler"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_ambiguous_variable_cross_file() {
let (backend, _dir) = create_psr4_workspace(
r#"{"autoload":{"psr-4":{"App\\":"src/"}}}"#,
&[
(
"src/Cache.php",
concat!(
"<?php\n",
"namespace App;\n",
"class Cache {\n",
" public function get(string $key): mixed { return null; }\n",
"}\n",
),
),
(
"src/NullCache.php",
concat!(
"<?php\n",
"namespace App;\n",
"class NullCache {\n",
" public function clear(): void {}\n",
"}\n",
),
),
],
);
let uri = Url::parse("file:///test_main.php").unwrap();
let text = concat!(
"<?php\n",
"use App\\Cache;\n",
"use App\\NullCache;\n",
"\n",
"class Service {\n",
" public function run(): void {\n",
" $store = new Cache();\n",
" if (getenv('DISABLE_CACHE')) {\n",
" $store = new NullCache();\n",
" }\n",
" $store->get('key');\n",
" }\n",
"}\n",
);
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;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 10,
character: 18,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result = backend.goto_definition(params).await.unwrap();
assert!(
result.is_some(),
"Should resolve $store->get() via Cache (PSR-4) even with NullCache in if-block"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(
location.range.start.line, 3,
"get() should be on line 3 of Cache.php"
);
let loc_path = location.uri.to_file_path().unwrap();
assert!(
loc_path.ends_with("src/Cache.php"),
"Should resolve to Cache.php, got: {:?}",
loc_path
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_inline_var_docblock_simple() {
let backend = create_test_backend();
let uri = Url::parse("file:///varoverride.php").unwrap();
let text = concat!(
"<?php\n", "class Session {\n", " public function getId(): string {}\n", " public function flash(): void {}\n", "}\n", "class Controller {\n", " public function handle() {\n", " /** @var Session */\n", " $sess = mystery();\n", " $sess->getId();\n", " }\n", "}\n", );
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;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 9,
character: 16,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result = backend.goto_definition(params).await.unwrap();
assert!(
result.is_some(),
"Should resolve $sess->getId() via @var Session annotation"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 2,
"getId is declared on line 2 in Session"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_inline_var_docblock_no_variable_name() {
let backend = create_test_backend();
let uri = Url::parse("file:///varoverride_noname.php").unwrap();
let text = concat!(
"<?php\n", "class Session {\n", " public function getId(): string {}\n", " public function flash(): void {}\n", "}\n", "class Controller {\n", " public function handle() {\n", " /** @var Session */\n", " $sess = mystery();\n", " $sess->flash();\n", " }\n", "}\n", );
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;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 9,
character: 16,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result = backend.goto_definition(params).await.unwrap();
assert!(
result.is_some(),
"Should resolve $sess->flash() via @var Session annotation (no variable name)"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 3,
"flash() is declared on line 3 in Session"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_inline_var_docblock_with_variable_name() {
let backend = create_test_backend();
let uri = Url::parse("file:///varoverride_named.php").unwrap();
let text = concat!(
"<?php\n", "class Logger {\n", " public function info(): void {}\n", " public function error(): void {}\n", "}\n", "class App {\n", " public function run() {\n", " /** @var Logger $log */\n", " $log = getLogger();\n", " $log->error();\n", " }\n", "}\n", );
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;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 9,
character: 15,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result = backend.goto_definition(params).await.unwrap();
assert!(
result.is_some(),
"Should resolve $log->error() via @var Logger $log annotation"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 3,
"error() is declared on line 3 in Logger"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_inline_var_docblock_wrong_variable_name() {
let backend = create_test_backend();
let uri = Url::parse("file:///varoverride_wrong.php").unwrap();
let text = concat!(
"<?php\n", "class Logger {\n", " public function info(): void {}\n", "}\n", "class App {\n", " public function run() {\n", " /** @var Logger $other */\n", " $log = something();\n", " $log->info();\n", " }\n", "}\n", );
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;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 8,
character: 15,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result = backend.goto_definition(params).await.unwrap();
assert!(
result.is_none(),
"Should NOT resolve $log->info() when @var names $other"
);
}
#[tokio::test]
async fn test_goto_definition_inline_var_docblock_blocked_by_scalar() {
let backend = create_test_backend();
let uri = Url::parse("file:///varoverride_scalar.php").unwrap();
let text = concat!(
"<?php\n", "class Session {\n", " public function getId(): string {}\n", "}\n", "class App {\n", " public function getName(): string {}\n", " public function run() {\n", " /** @var Session */\n", " $s = $this->getName();\n", " $s->getId();\n", " }\n", "}\n", );
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;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 9,
character: 13,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result = backend.goto_definition(params).await.unwrap();
assert!(
result.is_none(),
"Should NOT resolve $s->getId() when native type is scalar string"
);
}
#[tokio::test]
async fn test_goto_definition_inline_var_docblock_override_allowed_for_object() {
let backend = create_test_backend();
let uri = Url::parse("file:///varoverride_obj.php").unwrap();
let text = concat!(
"<?php\n", "class BaseService {\n", " public function base(): void {}\n", "}\n", "class Session extends BaseService {\n", " public function getId(): string {}\n", " public function flash(): void {}\n", "}\n", "class App {\n", " public function getService(): BaseService {}\n", " public function run() {\n", " /** @var Session */\n", " $s = $this->getService();\n", " $s->flash();\n", " }\n", "}\n", );
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;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 13,
character: 13,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result = backend.goto_definition(params).await.unwrap();
assert!(
result.is_some(),
"Should resolve $s->flash() via @var Session override"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 6,
"flash() is declared on line 6 in Session"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_variable_jumps_to_assignment() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_goto_assign.php").unwrap();
let text = concat!(
"<?php\n", "function demo(): mixed {\n", " $typed = getUnknownValue();\n", " return $typed;\n", "}\n", );
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;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 3,
character: 12,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result = backend.goto_definition(params).await.unwrap();
assert!(
result.is_some(),
"Should resolve $typed to its assignment on the previous line"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(location.range.start.line, 2, "$typed is assigned on line 2");
assert_eq!(
location.range.start.character, 4,
"$typed starts at column 4"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_variable_on_definition_returns_self_location() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_goto_on_def.php").unwrap();
let text = concat!(
"<?php\n", "function demo(): void {\n", " $typed = getUnknownValue();\n", "}\n", );
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;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 2,
character: 5,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result = backend.goto_definition(params).await.unwrap();
match result {
Some(GotoDefinitionResponse::Scalar(location)) => {
assert_eq!(location.uri, uri);
assert_eq!(location.range.start.line, 2, "should point back to line 2");
}
other => panic!("Expected self-location Scalar, got: {other:?}"),
}
}
#[tokio::test]
async fn test_goto_definition_variable_jumps_to_parameter() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_goto_param.php").unwrap();
let text = concat!(
"<?php\n", "class App {\n", " public function handle(int $id): void {\n", " echo $id;\n", " }\n", "}\n", );
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;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 3,
character: 14,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result = backend.goto_definition(params).await.unwrap();
assert!(
result.is_some(),
"Should resolve $id to the parameter declaration"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 2,
"$id is declared as a parameter on line 2"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_variable_jumps_to_foreach() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_goto_foreach.php").unwrap();
let text = concat!(
"<?php\n", "function demo(): void {\n", " $items = [1, 2, 3];\n", " foreach ($items as $item) {\n", " echo $item;\n", " }\n", "}\n", );
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;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 4,
character: 14,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result = backend.goto_definition(params).await.unwrap();
assert!(
result.is_some(),
"Should resolve $item to the foreach declaration"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 3,
"$item is declared in the foreach on line 3"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_variable_jumps_to_most_recent_reassignment() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_goto_reassign.php").unwrap();
let text = concat!(
"<?php\n", "function demo(): void {\n", " $val = 1;\n", " $val = 2;\n", " $val = 3;\n", " echo $val;\n", "}\n", );
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;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 5,
character: 10,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result = backend.goto_definition(params).await.unwrap();
assert!(
result.is_some(),
"Should resolve $val to the most recent assignment"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 4,
"$val's most recent assignment is on line 4"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_variable_jumps_to_catch() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_goto_catch.php").unwrap();
let text = concat!(
"<?php\n", "function demo(): void {\n", " try {\n", " riskyOperation();\n", " } catch (\\Exception $e) {\n", " echo $e;\n", " }\n", "}\n", );
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;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 5,
character: 14,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result = backend.goto_definition(params).await.unwrap();
assert!(
result.is_some(),
"Should resolve $e to the catch declaration"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 4,
"$e is declared in the catch on line 4"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_variable_top_level() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_goto_toplevel.php").unwrap();
let text = concat!(
"<?php\n", "$typed = getUnknownValue();\n", "return $typed;\n", );
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;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 2,
character: 8,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result = backend.goto_definition(params).await.unwrap();
assert!(
result.is_some(),
"Should resolve $typed to assignment on line 1"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(location.range.start.line, 1, "$typed is assigned on line 1");
assert_eq!(
location.range.start.character, 0,
"$typed starts at column 0"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_variable_foreach_key_value() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_goto_foreach_kv.php").unwrap();
let text = concat!(
"<?php\n", "function demo(): void {\n", " $map = ['a' => 1];\n", " foreach ($map as $key => $val) {\n", " echo $val;\n", " }\n", "}\n", );
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;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 4,
character: 14,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result = backend.goto_definition(params).await.unwrap();
assert!(
result.is_some(),
"Should resolve $val to the foreach key-value declaration"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 3,
"$val is declared in the foreach on line 3"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_variable_at_definition_returns_self_location() {
let backend = create_test_backend();
let uri = Url::parse("file:///accordion.php").unwrap();
let text = concat!(
"<?php\n", "class HtmlString {}\n", "class AccordionData {\n", " public function __construct(\n", " public readonly HtmlString|string $content,\n", " ) {}\n", "}\n", );
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;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 4,
character: 45,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result = backend.goto_definition(params).await.unwrap();
match result {
Some(GotoDefinitionResponse::Scalar(location)) => {
assert_eq!(location.uri, uri);
assert_eq!(location.range.start.line, 4, "should point back to line 4");
}
other => panic!("Expected self-location Scalar, got: {other:?}"),
}
}
#[tokio::test]
async fn test_goto_definition_parameter_at_definition_returns_self_location() {
let backend = create_test_backend();
let uri = Url::parse("file:///param_type.php").unwrap();
let text = concat!(
"<?php\n", "class Request {\n", " public function input(): string {}\n", "}\n", "class Controller {\n", " public function handle(Request $req) {\n", " }\n", "}\n", );
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;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 5,
character: 39,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result = backend.goto_definition(params).await.unwrap();
match result {
Some(GotoDefinitionResponse::Scalar(location)) => {
assert_eq!(location.uri, uri);
assert_eq!(location.range.start.line, 5, "should point back to line 5");
}
other => panic!("Expected self-location Scalar, got: {other:?}"),
}
}
#[tokio::test]
async fn test_goto_definition_variable_at_definition_nullable_type_hint_returns_self_location() {
let backend = create_test_backend();
let uri = Url::parse("file:///nullable_type.php").unwrap();
let text = concat!(
"<?php\n", "class Logger {\n", " public function info(): void {}\n", "}\n", "class App {\n", " public function handle(?Logger $log) {\n", " }\n", "}\n", );
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;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 5,
character: 37,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result = backend.goto_definition(params).await.unwrap();
match result {
Some(GotoDefinitionResponse::Scalar(location)) => {
assert_eq!(location.uri, uri);
assert_eq!(location.range.start.line, 5, "should point back to line 5");
}
other => panic!("Expected self-location Scalar, got: {other:?}"),
}
}
#[tokio::test]
async fn test_goto_definition_variable_at_definition_scalar_type_returns_self_location() {
let backend = create_test_backend();
let uri = Url::parse("file:///scalar_type.php").unwrap();
let text = concat!(
"<?php\n", "class App {\n", " public function handle(string $name) {\n", " }\n", "}\n", );
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;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 2,
character: 37,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result = backend.goto_definition(params).await.unwrap();
match result {
Some(GotoDefinitionResponse::Scalar(location)) => {
assert_eq!(location.uri, uri);
assert_eq!(location.range.start.line, 2, "should point back to line 2");
}
other => panic!("Expected self-location Scalar, got: {other:?}"),
}
}
#[tokio::test]
async fn test_goto_definition_variable_at_definition_union_class_second_returns_self_location() {
let backend = create_test_backend();
let uri = Url::parse("file:///union_second.php").unwrap();
let text = concat!(
"<?php\n", "class HtmlString {\n", " public function toHtml(): string {}\n", "}\n", "class Widget {\n", " public function render(string|HtmlString $out) {\n", " }\n", "}\n", );
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;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 5,
character: 47,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result = backend.goto_definition(params).await.unwrap();
match result {
Some(GotoDefinitionResponse::Scalar(location)) => {
assert_eq!(location.uri, uri);
assert_eq!(location.range.start.line, 5, "should point back to line 5");
}
other => panic!("Expected self-location Scalar, got: {other:?}"),
}
}
#[tokio::test]
async fn test_goto_definition_property_at_definition_returns_self_location() {
let backend = create_test_backend();
let uri = Url::parse("file:///prop_type.php").unwrap();
let text = concat!(
"<?php\n", "class Logger {\n", " public function info() {}\n", "}\n", "class App {\n", " private Logger $logger;\n", "}\n", );
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;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 5,
character: 21,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result = backend.goto_definition(params).await.unwrap();
match result {
Some(GotoDefinitionResponse::Scalar(location)) => {
assert_eq!(location.uri, uri);
assert_eq!(location.range.start.line, 5, "should point back to line 5");
}
other => panic!("Expected self-location Scalar, got: {other:?}"),
}
}
#[tokio::test]
async fn test_goto_definition_foreach_consecutive_loops_same_var() {
let backend = create_test_backend();
let uri = Url::parse("file:///foreach_consecutive.php").unwrap();
let text = concat!(
"<?php\n", "function demo(): void {\n", " foreach ($a as $b) {\n", " echo $b;\n", " }\n", " foreach ($c as $b) {\n", " echo $b;\n", " }\n", "}\n", );
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;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 6,
character: 14,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result = backend.goto_definition(params).await.unwrap();
assert!(
result.is_some(),
"Should resolve $b to the second foreach declaration"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 5,
"$b should jump to line 5 (second foreach), not line 2 (first foreach)"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_foreach_on_as_variable_returns_none() {
let backend = create_test_backend();
let uri = Url::parse("file:///foreach_on_as.php").unwrap();
let text = concat!(
"<?php\n", "function demo(): void {\n", " foreach ($a as $b) {\n", " }\n", " foreach ($c as $b) {\n", " }\n", "}\n", );
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;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 4,
character: 24,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result = backend.goto_definition(params).await.unwrap();
assert!(
result.is_none(),
"Should return None when cursor is on `as $b` (already at definition site), \
but got: {:?}. This means it jumped to the first foreach incorrectly.",
result,
);
}
#[tokio::test]
async fn test_goto_definition_foreach_reassignment_inside_loop() {
let backend = create_test_backend();
let uri = Url::parse("file:///foreach_reassign.php").unwrap();
let text = concat!(
"<?php\n", "function demo(): void {\n", " foreach ($a as $b) {\n", " $b = 'overwritten';\n", " echo $b;\n", " }\n", "}\n", );
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;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 4,
character: 14,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result = backend.goto_definition(params).await.unwrap();
assert!(
result.is_some(),
"Should resolve $b to the reassignment on line 3"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 3,
"$b should jump to the reassignment on line 3"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_rhs_variable_same_line_as_assignment() {
let backend = create_test_backend();
let uri = Url::parse("file:///rhs_same_line.php").unwrap();
let text = concat!(
"<?php\n", "class Converter {\n", " public static function toInt(mixed $value): int {\n", " if ($value instanceof BackedEnum) {\n", " $value = $value->value;\n", " }\n", " return (int) $value;\n", " }\n", "}\n", );
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;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 4,
character: 22, },
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result = backend.goto_definition(params).await.unwrap();
assert!(
result.is_some(),
"RHS $value should resolve to the parameter declaration"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 2,
"RHS $value should jump to the parameter on line 2"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_lhs_variable_same_line_returns_self_location() {
let backend = create_test_backend();
let uri = Url::parse("file:///lhs_same_line.php").unwrap();
let text = concat!(
"<?php\n", "class Converter {\n", " public static function toInt(mixed $value): int {\n", " if ($value instanceof BackedEnum) {\n", " $value = $value->value;\n", " }\n", " }\n", "}\n", );
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;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 4,
character: 13, },
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result = backend.goto_definition(params).await.unwrap();
match result {
Some(GotoDefinitionResponse::Scalar(location)) => {
assert_eq!(location.uri, uri);
assert_eq!(location.range.start.line, 4, "should point back to line 4");
}
other => panic!("Expected self-location Scalar, got: {other:?}"),
}
}
#[tokio::test]
async fn test_goto_definition_arrow_fn_rhs_param_jumps_to_same_line_param() {
let backend = create_test_backend();
let uri = Url::parse("file:///arrow_fn_rhs.php").unwrap();
let text = concat!(
"<?php\n", "class Order {\n", " public function getItems(): array {}\n", "}\n", "class Demo {\n", " public Order $o;\n", " public function run(): void {\n", " $list = array_map(fn(Order $o) => $o->getItems(), []);\n", " }\n", "}\n", );
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;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 7,
character: 43, },
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result = backend.goto_definition(params).await.unwrap();
assert!(
result.is_some(),
"RHS $o should resolve to the arrow function parameter"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 7,
"RHS $o should jump to the parameter on the same line 7"
);
assert_eq!(
location.range.start.character, 35,
"Should point to the parameter $o at column 35"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_arrow_fn_lhs_param_returns_self_location() {
let backend = create_test_backend();
let uri = Url::parse("file:///arrow_fn_lhs.php").unwrap();
let text = concat!(
"<?php\n", "class Order {\n", " public function getItems(): array {}\n", "}\n", "class Demo {\n", " public function run(): void {\n", " $list = array_map(fn(Order $o) => $o->getItems(), []);\n", " }\n", "}\n", );
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;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 6,
character: 36,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result = backend.goto_definition(params).await.unwrap();
match result {
Some(GotoDefinitionResponse::Scalar(location)) => {
assert_eq!(location.uri, uri);
assert_eq!(location.range.start.line, 6, "should point back to line 6");
}
other => panic!("Expected self-location Scalar, got: {other:?}"),
}
}
#[tokio::test]
async fn test_goto_definition_arrow_fn_untyped_param_rhs_jumps_to_param() {
let backend = create_test_backend();
let uri = Url::parse("file:///arrow_fn_untyped.php").unwrap();
let text = concat!(
"<?php\n", "class Demo {\n", " public function run(): void {\n", " $list = array_map(fn($x) => $x + 1, []);\n", " }\n", "}\n", );
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;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 3,
character: 37, },
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result = backend.goto_definition(params).await.unwrap();
assert!(
result.is_some(),
"RHS $x should resolve to the arrow function parameter"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 3,
"RHS $x should jump to the parameter on the same line"
);
assert_eq!(
location.range.start.character, 29,
"Should point to the parameter $x at column 29"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_closure_use_clause_body_to_use() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_closure_use.php").unwrap();
let text = concat!(
"<?php\n",
"function demo(): void {\n",
" $search = 'hi';\n",
" $fn = function () use ($search): void {\n",
" echo $search;\n",
" };\n",
"}\n",
);
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;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 4,
character: 13,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result = backend.goto_definition(params).await.unwrap();
assert!(
result.is_some(),
"Should resolve $search inside closure body to the use() clause"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 3,
"$search should jump to the use() clause on line 3"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_closure_use_clause_to_outer_assignment() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_closure_use_outer.php").unwrap();
let text = concat!(
"<?php\n",
"function demo(): void {\n",
" $search = 'hi';\n",
" $fn = function () use ($search): void {\n",
" echo $search;\n",
" };\n",
"}\n",
);
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;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 3,
character: 28,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result = backend.goto_definition(params).await.unwrap();
assert!(
result.is_some(),
"Should resolve $search in use() clause to the outer assignment"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 2,
"$search in use() should jump to the assignment on line 2"
);
assert_eq!(
location.range.start.character, 4,
"$search assignment starts at column 4"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_closure_use_clause_multiple_captures() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_closure_use_multi.php").unwrap();
let text = concat!(
"<?php\n",
"function demo(): void {\n",
" $a = 1;\n",
" $b = 2;\n",
" $fn = function () use ($a, $b): void {\n",
" echo $a;\n",
" echo $b;\n",
" };\n",
"}\n",
);
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;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 6,
character: 13,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result = backend.goto_definition(params).await.unwrap();
assert!(
result.is_some(),
"Should resolve $b inside closure body to the use() clause"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 4,
"$b should jump to the use() clause on line 4"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_closure_use_clause_reassigned_inside() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_closure_use_reassign.php").unwrap();
let text = concat!(
"<?php\n",
"function demo(): void {\n",
" $x = 'outer';\n",
" $fn = function () use ($x): void {\n",
" $x = 'inner';\n",
" echo $x;\n",
" };\n",
"}\n",
);
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;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 5,
character: 13,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result = backend.goto_definition(params).await.unwrap();
assert!(
result.is_some(),
"Should resolve $x to the reassignment inside the closure"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 4,
"$x should jump to the inner reassignment on line 4"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_switch_variable_after_switch() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_switch_after.php").unwrap();
let text = concat!(
"<?php\n",
"function demo(): void {\n",
" switch (true) {\n",
" case 1:\n",
" $x = 'one';\n",
" break;\n",
" }\n",
" echo $x;\n",
"}\n",
);
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;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 7,
character: 9,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result = backend.goto_definition(params).await.unwrap();
assert!(
result.is_some(),
"Should resolve $x to the assignment inside the switch case"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 4,
"$x is assigned on line 4 inside the case"
);
assert_eq!(location.range.start.character, 12);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_switch_cursor_inside_case() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_switch_inside_case.php").unwrap();
let text = concat!(
"<?php\n",
"function demo(): void {\n",
" $x = 'before';\n",
" switch (true) {\n",
" case 1:\n",
" $x = 'one';\n",
" $y = $x;\n",
" break;\n",
" }\n",
"}\n",
);
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;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 6,
character: 17,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result = backend.goto_definition(params).await.unwrap();
assert!(
result.is_some(),
"Should resolve $x to the assignment inside the same case"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 5,
"$x should jump to the case-local assignment on line 5"
);
assert_eq!(location.range.start.character, 12);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_do_while_variable() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_do_while.php").unwrap();
let text = concat!(
"<?php\n",
"function demo(): void {\n",
" do {\n",
" $x = 'hello';\n",
" $y = $x;\n",
" } while (true);\n",
"}\n",
);
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;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 4,
character: 13,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result = backend.goto_definition(params).await.unwrap();
assert!(
result.is_some(),
"Should resolve $x to the assignment inside the do-while"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 3,
"$x is assigned on line 3 inside the do-while"
);
assert_eq!(location.range.start.character, 8);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_for_loop_initializer() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_for_init.php").unwrap();
let text = concat!(
"<?php\n",
"function demo(): void {\n",
" for ($i = 0; $i < 10; $i++) {\n",
" $y = $i;\n",
" }\n",
"}\n",
);
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;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 3,
character: 13,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result = backend.goto_definition(params).await.unwrap();
assert!(
result.is_some(),
"Should resolve $i to the for-loop initializer"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 2,
"$i is initialized on line 2 in the for statement"
);
assert_eq!(location.range.start.character, 9);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_for_loop_initializer_at_definition_returns_self_location() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_for_init_atdef.php").unwrap();
let text = concat!(
"<?php\n",
"function demo(): void {\n",
" for ($i = 0; $i < 10; $i++) {\n",
" $y = $i;\n",
" }\n",
"}\n",
);
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;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 2,
character: 9,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result = backend.goto_definition(params).await.unwrap();
match result {
Some(GotoDefinitionResponse::Scalar(location)) => {
assert_eq!(location.uri, uri);
assert_eq!(location.range.start.line, 2, "should point back to line 2");
}
other => panic!("Expected self-location Scalar, got: {other:?}"),
}
}
#[tokio::test]
async fn test_goto_definition_while_loop_variable() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_while.php").unwrap();
let text = concat!(
"<?php\n",
"function demo(): void {\n",
" $x = 'hello';\n",
" while (true) {\n",
" $y = $x;\n",
" }\n",
"}\n",
);
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;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 4,
character: 13,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result = backend.goto_definition(params).await.unwrap();
assert!(
result.is_some(),
"Should resolve $x to the assignment before the while loop"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 2,
"$x is assigned on line 2 before the while loop"
);
assert_eq!(location.range.start.character, 4);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_global_statement() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_global.php").unwrap();
let text = concat!(
"<?php\n",
"function demo(): void {\n",
" global $gvar;\n",
" $y = $gvar;\n",
"}\n",
);
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;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 3,
character: 9,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result = backend.goto_definition(params).await.unwrap();
assert!(
result.is_some(),
"Should resolve $gvar to the global declaration"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 2,
"$gvar is declared on line 2 in the global statement"
);
assert_eq!(location.range.start.character, 11);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_global_statement_at_definition_returns_self_location() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_global_atdef.php").unwrap();
let text = concat!(
"<?php\n",
"function demo(): void {\n",
" global $gvar;\n",
" $y = $gvar;\n",
"}\n",
);
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;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 2,
character: 11,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result = backend.goto_definition(params).await.unwrap();
match result {
Some(GotoDefinitionResponse::Scalar(location)) => {
assert_eq!(location.uri, uri);
assert_eq!(location.range.start.line, 2, "should point back to line 2");
}
other => panic!("Expected self-location Scalar, got: {other:?}"),
}
}
#[tokio::test]
async fn test_goto_definition_static_statement() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_static.php").unwrap();
let text = concat!(
"<?php\n",
"function demo(): void {\n",
" static $count = 0;\n",
" $y = $count;\n",
"}\n",
);
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;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 3,
character: 9,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result = backend.goto_definition(params).await.unwrap();
assert!(
result.is_some(),
"Should resolve $count to the static declaration"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 2,
"$count is declared on line 2 in the static statement"
);
assert_eq!(location.range.start.character, 11);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_static_statement_at_definition_returns_self_location() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_static_atdef.php").unwrap();
let text = concat!(
"<?php\n",
"function demo(): void {\n",
" static $count = 0;\n",
" $y = $count;\n",
"}\n",
);
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;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 2,
character: 11,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result = backend.goto_definition(params).await.unwrap();
match result {
Some(GotoDefinitionResponse::Scalar(location)) => {
assert_eq!(location.uri, uri);
assert_eq!(location.range.start.line, 2, "should point back to line 2");
}
other => panic!("Expected self-location Scalar, got: {other:?}"),
}
}
#[tokio::test]
async fn test_goto_definition_array_destructuring() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_array_destruct.php").unwrap();
let text = concat!(
"<?php\n",
"function demo(): void {\n",
" [$a, $b] = getValues();\n",
" $y = $a;\n",
"}\n",
);
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;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 3,
character: 9,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result = backend.goto_definition(params).await.unwrap();
assert!(
result.is_some(),
"Should resolve $a to the array destructuring on line 2"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(location.range.start.line, 2, "$a is destructured on line 2");
assert_eq!(location.range.start.character, 5);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_array_destructuring_at_definition_returns_self_location() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_array_destruct_atdef.php").unwrap();
let text = concat!(
"<?php\n",
"function demo(): void {\n",
" [$a, $b] = getValues();\n",
" $y = $a;\n",
"}\n",
);
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;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 2,
character: 5,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result = backend.goto_definition(params).await.unwrap();
match result {
Some(GotoDefinitionResponse::Scalar(location)) => {
assert_eq!(location.uri, uri);
assert_eq!(location.range.start.line, 2, "should point back to line 2");
}
other => panic!("Expected self-location Scalar, got: {other:?}"),
}
}
#[tokio::test]
async fn test_goto_definition_list_destructuring() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_list_destruct.php").unwrap();
let text = concat!(
"<?php\n",
"function demo(): void {\n",
" list($x, $y) = getValues();\n",
" $z = $x;\n",
"}\n",
);
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;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 3,
character: 9,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result = backend.goto_definition(params).await.unwrap();
assert!(
result.is_some(),
"Should resolve $x to the list() destructuring on line 2"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(location.range.start.line, 2, "$x is destructured on line 2");
assert_eq!(location.range.start.character, 9);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_nested_destructuring() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_nested_destruct.php").unwrap();
let text = concat!(
"<?php\n",
"function demo(): void {\n",
" [[$a, $b], $c] = getValues();\n",
" $y = $b;\n",
"}\n",
);
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;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 3,
character: 9,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result = backend.goto_definition(params).await.unwrap();
assert!(
result.is_some(),
"Should resolve $b to the nested destructuring on line 2"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(location.range.start.line, 2, "$b is destructured on line 2");
assert_eq!(location.range.start.character, 10);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_foreach_destructuring() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_foreach_destruct.php").unwrap();
let text = concat!(
"<?php\n",
"function demo(): void {\n",
" $items = [];\n",
" foreach ($items as [$name, $value]) {\n",
" $y = $name;\n",
" }\n",
"}\n",
);
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;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 4,
character: 13,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result = backend.goto_definition(params).await.unwrap();
assert!(
result.is_some(),
"Should resolve $name to the foreach destructuring on line 3"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 3,
"$name is destructured in foreach on line 3"
);
assert_eq!(location.range.start.character, 24);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_foreach_key_variable() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_foreach_key.php").unwrap();
let text = concat!(
"<?php\n",
"function demo(): void {\n",
" $map = ['a' => 1];\n",
" foreach ($map as $key => $val) {\n",
" $y = $key;\n",
" }\n",
"}\n",
);
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;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 4,
character: 13,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result = backend.goto_definition(params).await.unwrap();
assert!(
result.is_some(),
"Should resolve $key to the foreach key declaration on line 3"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 3,
"$key is declared in the foreach on line 3"
);
assert_eq!(location.range.start.character, 21);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_block_statement() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_block.php").unwrap();
let text = concat!(
"<?php\n",
"function demo(): void {\n",
" {\n",
" $x = 'hello';\n",
" }\n",
" $y = $x;\n",
"}\n",
);
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;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 5,
character: 9,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result = backend.goto_definition(params).await.unwrap();
assert!(
result.is_some(),
"Should resolve $x to the assignment inside the block"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 3,
"$x is assigned on line 3 inside the block"
);
assert_eq!(location.range.start.character, 8);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_return_with_assignment() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_return_assign.php").unwrap();
let text = concat!(
"<?php\n",
"function demo(): mixed {\n",
" return $x = getValue();\n",
" $y = $x;\n",
"}\n",
);
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;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 3,
character: 9,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result = backend.goto_definition(params).await.unwrap();
assert!(
result.is_some(),
"Should resolve $x to the assignment in the return statement"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 2,
"$x is assigned on line 2 in the return statement"
);
assert_eq!(location.range.start.character, 11);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_closure_inner_definition_wins() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_closure_inner.php").unwrap();
let text = concat!(
"<?php\n",
"function demo(): void {\n",
" $x = 'outer';\n",
" $fn = function () {\n",
" $x = 'inner';\n",
" $y = $x;\n",
" };\n",
"}\n",
);
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;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 5,
character: 13,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result = backend.goto_definition(params).await.unwrap();
assert!(
result.is_some(),
"Should resolve $x to the inner closure assignment, not the outer one"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 4,
"$x should jump to the inner assignment on line 4"
);
assert_eq!(location.range.start.character, 8);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_arrow_function_body_variable() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_arrow_fn_body.php").unwrap();
let text = concat!(
"<?php\n",
"function demo(): void {\n",
" $x = 'outer';\n",
" $fn = fn($x) => $x;\n",
"}\n",
);
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;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 3,
character: 20,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result = backend.goto_definition(params).await.unwrap();
assert!(
result.is_some(),
"Should resolve $x in arrow fn body to the arrow fn parameter"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 3,
"$x should jump to the arrow fn parameter on line 3"
);
assert_eq!(location.range.start.character, 13);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_rhs_same_assignment_jumps_to_original() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_rhs_same_assign.php").unwrap();
let text = concat!(
"<?php\n",
"function demo(mixed $value): mixed {\n",
" $value = $value->something;\n",
" return $value;\n",
"}\n",
);
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;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 2,
character: 13,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result = backend.goto_definition(params).await.unwrap();
assert!(
result.is_some(),
"RHS $value should resolve to the parameter, not the LHS"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 1,
"RHS $value should jump to the parameter on line 1"
);
assert_eq!(location.range.start.character, 20);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}