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_class_constant_same_file() {
let backend = create_test_backend();
let uri = Url::parse("file:///test.php").unwrap();
let text = concat!(
"<?php\n",
"class MyClass {\n",
" const MY_CONST = 42;\n",
" const OTHER = 'hello';\n",
"\n",
" public function foo(): int {\n",
" return self::MY_CONST;\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: 22,
},
},
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 self::MY_CONST to its declaration"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 2,
"const MY_CONST is declared on line 2"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_class_constant_via_classname() {
let backend = create_test_backend();
let uri = Url::parse("file:///test.php").unwrap();
let text = concat!(
"<?php\n",
"class Status {\n",
" const ACTIVE = 1;\n",
" const INACTIVE = 0;\n",
"}\n",
"\n",
"class Service {\n",
" public function check(): int {\n",
" return Status::ACTIVE;\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: 24,
},
},
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 Status::ACTIVE to its declaration"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 2,
"const ACTIVE is declared on line 2"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_class_constant_cross_file() {
let (backend, _dir) = create_psr4_workspace(
r#"{
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
}"#,
&[(
"src/Status.php",
concat!(
"<?php\n",
"namespace App;\n",
"\n",
"class Status {\n",
" const PENDING = 'pending';\n",
" const APPROVED = 'approved';\n",
"}\n",
),
)],
);
let uri = Url::parse("file:///service.php").unwrap();
let text = concat!(
"<?php\n",
"namespace App;\n",
"\n",
"class OrderService {\n",
" public function getDefault(): string {\n",
" return Status::PENDING;\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 },
position: Position {
line: 5,
character: 25,
},
},
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 cross-file Status::PENDING"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
let path = location.uri.to_file_path().unwrap();
assert!(
path.ends_with("src/Status.php"),
"Should point to Status.php, got: {:?}",
path
);
assert_eq!(location.range.start.line, 4, "const PENDING is on line 4");
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_method_via_this() {
let backend = create_test_backend();
let uri = Url::parse("file:///test.php").unwrap();
let text = concat!(
"<?php\n",
"class Logger {\n",
" public function info(string $msg): void {}\n",
"\n",
" public function warn(string $msg): void {\n",
" $this->info($msg);\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: 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 $this->info to its declaration"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 2,
"function info is declared on line 2"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_static_method_via_classname() {
let backend = create_test_backend();
let uri = Url::parse("file:///test.php").unwrap();
let text = concat!(
"<?php\n",
"class Factory {\n",
" public static function create(): self {\n",
" return new self();\n",
" }\n",
"}\n",
"\n",
"class App {\n",
" public function run(): void {\n",
" Factory::create();\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: 19,
},
},
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 Factory::create to its declaration"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 2,
"function create is declared on line 2"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_method_via_self() {
let backend = create_test_backend();
let uri = Url::parse("file:///test.php").unwrap();
let text = concat!(
"<?php\n",
"class Calculator {\n",
" public static function add(int $a, int $b): int {\n",
" return $a + $b;\n",
" }\n",
"\n",
" public static function sum(array $nums): int {\n",
" return self::add($nums[0], $nums[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: 7,
character: 23,
},
},
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 self::add to its declaration"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 2,
"function add is declared on line 2"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_method_cross_file() {
let (backend, _dir) = create_psr4_workspace(
r#"{
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
}"#,
&[(
"src/Logger.php",
concat!(
"<?php\n",
"namespace App;\n",
"\n",
"class Logger {\n",
" public function info(string $msg): void {}\n",
" public function error(string $msg): void {}\n",
"}\n",
),
)],
);
let uri = Url::parse("file:///service.php").unwrap();
let text = concat!(
"<?php\n",
"namespace App;\n",
"\n",
"class Service {\n",
" public function run(Logger $logger): void {\n",
" $logger->error('failed');\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 },
position: Position {
line: 5,
character: 19,
},
},
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 cross-file $logger->error");
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
let path = location.uri.to_file_path().unwrap();
assert!(
path.ends_with("src/Logger.php"),
"Should point to Logger.php, got: {:?}",
path
);
assert_eq!(location.range.start.line, 5, "function error is on line 5");
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_property_via_this() {
let backend = create_test_backend();
let uri = Url::parse("file:///test.php").unwrap();
let text = concat!(
"<?php\n",
"class User {\n",
" public string $name;\n",
" public int $age;\n",
"\n",
" public function getName(): string {\n",
" return $this->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: 6,
character: 23,
},
},
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 $this->name to its declaration"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 2,
"$name property is declared on line 2"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_property_cross_file() {
let (backend, _dir) = create_psr4_workspace(
r#"{
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
}"#,
&[(
"src/Config.php",
concat!(
"<?php\n",
"namespace App;\n",
"\n",
"class Config {\n",
" public string $dbHost;\n",
" public int $dbPort;\n",
"}\n",
),
)],
);
let uri = Url::parse("file:///service.php").unwrap();
let text = concat!(
"<?php\n",
"namespace App;\n",
"\n",
"class Service {\n",
" public function connect(Config $cfg): void {\n",
" $host = $cfg->dbHost;\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 },
position: Position {
line: 5,
character: 24,
},
},
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 cross-file $cfg->dbHost");
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
let path = location.uri.to_file_path().unwrap();
assert!(
path.ends_with("src/Config.php"),
"Should point to Config.php, got: {:?}",
path
);
assert_eq!(
location.range.start.line, 4,
"$dbHost property is on line 4"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_inherited_method() {
let (backend, _dir) = create_psr4_workspace(
r#"{
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
}"#,
&[(
"src/BaseModel.php",
concat!(
"<?php\n",
"namespace App;\n",
"\n",
"class BaseModel {\n",
" public function save(): void {}\n",
" public function delete(): void {}\n",
"}\n",
),
)],
);
let uri = Url::parse("file:///user.php").unwrap();
let text = concat!(
"<?php\n",
"namespace App;\n",
"\n",
"class User extends BaseModel {\n",
" public string $name;\n",
"\n",
" public function update(): void {\n",
" $this->save();\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 },
position: Position {
line: 7,
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 inherited $this->save() to parent class"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
let path = location.uri.to_file_path().unwrap();
assert!(
path.ends_with("src/BaseModel.php"),
"Should point to BaseModel.php, got: {:?}",
path
);
assert_eq!(
location.range.start.line, 4,
"function save is on line 4 of BaseModel.php"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_inherited_constant_via_parent() {
let backend = create_test_backend();
let uri = Url::parse("file:///test.php").unwrap();
let text = concat!(
"<?php\n",
"class Base {\n",
" const VERSION = '1.0';\n",
"}\n",
"\n",
"class Child extends Base {\n",
" public function getVersion(): string {\n",
" return parent::VERSION;\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: 25,
},
},
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 parent::VERSION to Base class"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 2,
"const VERSION is declared on line 2 in Base"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_method_on_new_variable() {
let backend = create_test_backend();
let uri = Url::parse("file:///test.php").unwrap();
let text = concat!(
"<?php\n",
"class Mailer {\n",
" public function send(string $to): void {}\n",
" public function queue(string $to): void {}\n",
"}\n",
"\n",
"class App {\n",
" public function run(): void {\n",
" $mailer = new Mailer();\n",
" $mailer->send('user@example.com');\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: 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 $mailer->send via new Mailer() assignment"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 2,
"function send is declared on line 2"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_chained_property_method() {
let backend = create_test_backend();
let uri = Url::parse("file:///test.php").unwrap();
let text = concat!(
"<?php\n",
"class Connection {\n",
" public function query(string $sql): void {}\n",
"}\n",
"\n",
"class Database {\n",
" public Connection $conn;\n",
"\n",
" public function run(): void {\n",
" $this->conn->query('SELECT 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: 9,
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(),
"Should resolve $this->conn->query via chained property"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 2,
"function query is declared on line 2"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_promoted_property() {
let backend = create_test_backend();
let uri = Url::parse("file:///test.php").unwrap();
let text = concat!(
"<?php\n",
"class User {\n",
" public function __construct(\n",
" private string $name,\n",
" private int $age,\n",
" ) {}\n",
"\n",
" public function getName(): string {\n",
" return $this->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: 8,
character: 23,
},
},
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 $this->name to promoted property"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(location.range.start.line, 3, "promoted $name is on line 3");
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_constant_via_static() {
let backend = create_test_backend();
let uri = Url::parse("file:///test.php").unwrap();
let text = concat!(
"<?php\n",
"class Config {\n",
" const MAX_RETRIES = 3;\n",
"\n",
" public function getMax(): int {\n",
" return static::MAX_RETRIES;\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: 24,
},
},
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 static::MAX_RETRIES");
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 2,
"const MAX_RETRIES is on line 2"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_method_cross_file_with_use_statement() {
let (backend, _dir) = create_psr4_workspace(
r#"{
"autoload": {
"psr-4": {
"Lib\\": "lib/"
}
}
}"#,
&[(
"lib/Cache.php",
concat!(
"<?php\n",
"namespace Lib;\n",
"\n",
"class Cache {\n",
" public function get(string $key): mixed {}\n",
" public function set(string $key, mixed $val): void {}\n",
"}\n",
),
)],
);
let uri = Url::parse("file:///app.php").unwrap();
let text = concat!(
"<?php\n",
"namespace App;\n",
"\n",
"use Lib\\Cache;\n",
"\n",
"class Service {\n",
" public function load(Cache $cache): void {\n",
" $cache->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 },
position: Position {
line: 7,
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 $cache->get via use statement"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
let path = location.uri.to_file_path().unwrap();
assert!(
path.ends_with("lib/Cache.php"),
"Should point to Cache.php, got: {:?}",
path
);
assert_eq!(location.range.start.line, 4, "function get is on line 4");
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_cursor_on_classname_before_double_colon() {
let backend = create_test_backend();
let uri = Url::parse("file:///test.php").unwrap();
let text = concat!(
"<?php\n",
"class Status {\n",
" const ACTIVE = 1;\n",
"}\n",
"\n",
"class Service {\n",
" public function check(): int {\n",
" return Status::ACTIVE;\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: 18,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result = backend.goto_definition(params).await.unwrap();
assert!(
result.is_some(),
"Cursor on class name before :: should resolve to the class"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(location.range.start.line, 1, "class Status is on line 1");
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_property_preferred_over_method_without_parens() {
let backend = create_test_backend();
let uri = Url::parse("file:///test.php").unwrap();
let text = concat!(
"<?php\n", "class User {\n", " public int $id;\n", " public string $name;\n", "\n", " public function id(): int {\n", " return $this->id;\n", " }\n", "\n", " public function test(): void {\n", " $user = new User();\n", " $val = $user->id;\n", " $val2 = $user->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: 11,
character: 24, },
},
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 $user->id to the property declaration"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 2,
"$user->id (no parens) should go to the $id property on line 2, not the id() method on line 5"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 12,
character: 25, },
},
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 $user->id() to the method declaration"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 5,
"$user->id() (with parens) should go to the id() method on line 5, not the $id property on line 2"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_this_property_vs_method_disambiguation() {
let backend = create_test_backend();
let uri = Url::parse("file:///test.php").unwrap();
let text = concat!(
"<?php\n", "class Order {\n", " public float $total;\n", "\n", " public function total(): float {\n", " return $this->total;\n", " }\n", "\n", " public function display(): void {\n", " echo $this->total;\n", " echo $this->total();\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: 23, },
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result = backend.goto_definition(params).await.unwrap();
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(
location.range.start.line, 2,
"$this->total (no parens) should go to the $total property on line 2"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 10,
character: 23, },
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result = backend.goto_definition(params).await.unwrap();
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(
location.range.start.line, 4,
"$this->total() (with parens) should go to the total() method on line 4"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_method_tag_name_matches_type_keyword() {
let backend = create_test_backend();
let uri = Url::parse("file:///method_string.php").unwrap();
let text = concat!(
"<?php\n", "/**\n", " * @method static string string(string $key, \\Closure|string|null $default = null)\n", " */\n", "class Config {\n", "}\n", "\n", "Config::string('hello');\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: 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 Config::string() to the @method tag declaration"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 2,
"@method string string(...) is declared on line 2"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_method_on_static_call_with_nested_call_arg() {
let backend = create_test_backend();
let uri = Url::parse("file:///test_nested_arg.php").unwrap();
let text = concat!(
"<?php\n", "class Country {}\n", "\n", "class SettingsProvider {\n", " public function get(string $key): string { return ''; }\n", "}\n", "\n", "class Environment {\n", " public static function get(Country $env): self { return new self(); }\n", " public function settings(): SettingsProvider { return new SettingsProvider(); }\n", "}\n", "\n", "class CurrentEnvironment {\n", " public static function country(): Country { return new Country(); }\n", " public static function settings(): SettingsProvider {\n", " return Environment::get(self::country())->settings();\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: 52,
},
},
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 ->settings() after Environment::get(self::country()) to its declaration"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 9,
"Environment::settings() is declared on line 9"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_member_does_not_fallthrough_to_function() {
let backend = create_test_backend();
let uri = Url::parse("file:///fallthrough.php").unwrap();
let text = concat!(
"<?php\n",
"class Collection {\n",
" public function map(callable $cb): static {}\n",
" public function values(): static {}\n",
"}\n",
"\n",
"/** @return Collection */\n",
"function collect($v): Collection { return new Collection(); }\n",
"\n",
"/** Standalone map function — must NOT be the target. */\n",
"function map(array $arr, callable $cb): array { return []; }\n",
"\n",
"class Service {\n",
" public function run(): void {\n",
" $result = collect([])->map(fn ($x) => $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 line_text = " $result = collect([])->map(fn ($x) => $x);";
let map_col = line_text.find("->map(").unwrap() + 2;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 14,
character: map_col as u32,
},
},
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.range.start.line, 2,
"Expected Collection::map on line 2, got line {}",
location.range.start.line,
);
}
None => {
}
other => panic!(
"Expected Scalar location pointing to Collection::map or None, got: {:?}",
other
),
}
}
#[tokio::test]
async fn test_goto_definition_method_on_enum_returned_by_static_call_forward_ref() {
let backend = create_test_backend();
let uri = Url::parse("file:///forward_ref_enum.php").unwrap();
let text = concat!(
"<?php\n", "final class CurrentEnvironment {\n", " public static function country(): Country {\n", " }\n", "}\n", "\n", "enum Country: string {\n", " public function getName(): string {\n", " }\n", "}\n", "\n", "CurrentEnvironment::country()->getName();\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 line_text = "CurrentEnvironment::country()->getName();";
let col = line_text.find("getName").unwrap();
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 11,
character: col as u32,
},
},
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 ->getName() on enum Country even when class is defined before enum (forward reference)"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 7,
"Country::getName() is declared on line 7"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_method_on_enum_returned_by_static_call() {
let backend = create_test_backend();
let uri = Url::parse("file:///static_enum_chain.php").unwrap();
let text = concat!(
"<?php\n", "enum Country: string {\n", " case DK = 'DK';\n", " public function getName(): string { return 'Denmark'; }\n", "}\n", "\n", "final class CurrentEnvironment {\n", " public static function country(): Country {\n", " return Country::DK;\n", " }\n", "}\n", "\n", "CurrentEnvironment::country()->getName();\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 line_text = "CurrentEnvironment::country()->getName();";
let col = line_text.find("getName").unwrap();
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 12,
character: col as u32,
},
},
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 ->getName() on enum Country returned by static call"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 3,
"Country::getName() is declared on line 3"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_method_on_enum_returned_by_static_call_cross_file() {
let composer_json = r#"{
"autoload": {
"psr-4": {
"App\\": "src/",
"Vendor\\Enums\\": "vendor/enums/"
}
}
}"#;
let country_php = r#"<?php
namespace Vendor\Enums;
enum Country: string {
case DK = 'DK';
public function getName(): string {
return 'Denmark';
}
}
"#;
let env_php = r#"<?php
namespace App;
use Vendor\Enums\Country;
final class CurrentEnvironment {
public static function country(): Country {
return Country::DK;
}
}
"#;
let controller_php = r#"<?php
namespace App\Http;
use App\CurrentEnvironment;
class MyController {
public function index(): void {
CurrentEnvironment::country()->getName();
}
}
"#;
let (backend, _dir) = create_psr4_workspace(
composer_json,
&[
("vendor/enums/Country.php", country_php),
("src/CurrentEnvironment.php", env_php),
("src/Http/MyController.php", controller_php),
],
);
let controller_uri = {
let path = _dir.path().join("src/Http/MyController.php");
Url::from_file_path(&path).unwrap()
};
let open_params = DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: controller_uri.clone(),
language_id: "php".to_string(),
version: 1,
text: controller_php.to_string(),
},
};
backend.did_open(open_params).await;
let line_text = " CurrentEnvironment::country()->getName();";
let col = line_text.find("getName").unwrap();
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier {
uri: controller_uri.clone(),
},
position: Position {
line: 7,
character: col as u32,
},
},
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 ->getName() on enum Country returned by cross-file static call \
where Country is in a different namespace (Vendor\\Enums) than the returning \
class (App\\CurrentEnvironment)"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
let country_uri = {
let path = _dir.path().join("vendor/enums/Country.php");
Url::from_file_path(&path).unwrap()
};
assert_eq!(
location.uri, country_uri,
"Should jump to Country.php in the vendor namespace"
);
assert_eq!(
location.range.start.line, 6,
"Country::getName() is declared on line 6 in Country.php"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_class_constant_top_level() {
let backend = create_test_backend();
let uri = Url::parse("file:///top_level_const.php").unwrap();
let text = concat!(
"<?php\n",
"class User {\n",
" public const string TYPE_ADMIN = 'admin';\n",
" public const string TYPE_USER = 'user';\n",
"}\n",
"\n",
"User::TYPE_ADMIN;\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 line_text = "User::TYPE_ADMIN;";
let name_pos = line_text.find("TYPE_ADMIN").unwrap();
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 6,
character: name_pos as u32,
},
},
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 User::TYPE_ADMIN at top level to its declaration"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 2,
"const TYPE_ADMIN is declared on line 2"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_inherited_constant_top_level() {
let backend = create_test_backend();
let uri = Url::parse("file:///inherited_const_top.php").unwrap();
let text = concat!(
"<?php\n",
"class Model {\n",
" public const string CONNECTION = 'default';\n",
"}\n",
"class User extends Model {\n",
"}\n",
"\n",
"User::CONNECTION;\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 line_text = "User::CONNECTION;";
let name_pos = line_text.find("CONNECTION").unwrap();
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 7,
character: name_pos as u32,
},
},
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 User::CONNECTION (inherited) at top level to Model's declaration"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 2,
"const CONNECTION is declared on line 2 in Model"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_static_property_via_classname() {
let backend = create_test_backend();
let uri = Url::parse("file:///static_prop.php").unwrap();
let text = concat!(
"<?php\n",
"class Config {\n",
" public static string $defaultLocale = 'en';\n",
" protected static int $timeout = 30;\n",
"}\n",
"\n",
"class Service {\n",
" public function run(): void {\n",
" $locale = Config::$defaultLocale;\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: 27,
},
},
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 Config::$defaultLocale to its declaration"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 2,
"$defaultLocale is declared on line 2"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_static_property_via_classname_cross_file() {
let (backend, _dir) = create_psr4_workspace(
r#"{
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
}"#,
&[(
"src/Config.php",
concat!(
"<?php\n",
"namespace App;\n",
"class Config {\n",
" public static string $appName = 'PHPantom';\n",
" public static bool $debug = false;\n",
"}\n",
),
)],
);
let uri = Url::parse("file:///consumer.php").unwrap();
let text = concat!(
"<?php\n",
"use App\\Config;\n",
"class Consumer {\n",
" public function run(): void {\n",
" $name = Config::$appName;\n",
" }\n",
"}\n",
);
backend
.did_open(DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: uri.clone(),
language_id: "php".to_string(),
version: 1,
text: text.to_string(),
},
})
.await;
let line_text = " $name = Config::$appName;";
let dollar_pos = line_text.find("::$appName").unwrap() + 3; let name_pos = dollar_pos + 1;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 4,
character: name_pos as u32,
},
},
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 Config::$appName cross-file to its declaration"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert!(
location.uri.as_str().contains("Config.php"),
"Should jump to Config.php, got: {}",
location.uri
);
assert_eq!(
location.range.start.line, 3,
"$appName is declared on line 3 of Config.php"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_static_property_via_self() {
let backend = create_test_backend();
let uri = Url::parse("file:///self_static_prop.php").unwrap();
let text = concat!(
"<?php\n",
"class Registry {\n",
" private static array $items = [];\n",
"\n",
" public static function count(): int {\n",
" return count(self::$items);\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 line_text = " return count(self::$items);";
let dollar_pos = line_text.find("::$items").unwrap() + 3;
let name_pos = dollar_pos + 1;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 5,
character: name_pos as u32,
},
},
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 self::$items to its declaration"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(location.range.start.line, 2, "$items is declared on line 2");
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_unresolvable_member_returns_none() {
let backend = create_test_backend();
let uri = Url::parse("file:///unresolvable.php").unwrap();
let text = concat!(
"<?php\n",
"/** Standalone values function — must NOT be the target. */\n",
"function values(): array { return []; }\n",
"\n",
"class Service {\n",
" public function run(): void {\n",
" unknown_function()->values();\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 line_text = " unknown_function()->values();";
let val_col = line_text.find("->values(").unwrap() + 2;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 6,
character: val_col as u32,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result = backend.goto_definition(params).await.unwrap();
assert!(
result.is_none(),
"Expected None when owning class is unresolvable, but got: {:?}. \
This means the member name fell through to standalone function lookup.",
result,
);
}
#[tokio::test]
async fn test_goto_definition_inherited_method_same_short_name() {
let (backend, _dir) = create_psr4_workspace(
r#"{
"autoload": {
"psr-4": {
"App\\": "src/",
"Framework\\": "vendor/framework/src/"
}
}
}"#,
&[(
"vendor/framework/src/Console/Kernel.php",
concat!(
"<?php\n",
"namespace Framework\\Console;\n",
"\n",
"class Kernel {\n",
" protected function load(string $path): void {}\n",
" protected function commands(): void {}\n",
"}\n",
),
)],
);
let uri = Url::parse("file:///app_kernel.php").unwrap();
let text = concat!(
"<?php\n",
"namespace App\\Console;\n",
"\n",
"use Framework\\Console\\Kernel as ConsoleKernel;\n",
"\n",
"class Kernel extends ConsoleKernel {\n",
" protected function commands(): void {\n",
" $this->load('routes');\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 },
position: Position {
line: 7,
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 inherited $this->load() to parent Framework\\Console\\Kernel"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
let path = location.uri.to_file_path().unwrap();
assert!(
path.ends_with("vendor/framework/src/Console/Kernel.php"),
"Should point to Framework\\Console\\Kernel.php, got: {:?}",
path
);
assert_eq!(
location.range.start.line, 4,
"function load is on line 4 of Framework\\Console\\Kernel.php"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_inherited_method_same_short_name_no_alias() {
let (backend, _dir) = create_psr4_workspace(
r#"{
"autoload": {
"psr-4": {
"App\\": "src/",
"Vendor\\": "vendor/src/"
}
}
}"#,
&[(
"vendor/src/Kernel.php",
concat!(
"<?php\n",
"namespace Vendor;\n",
"\n",
"class Kernel {\n",
" public function bootstrap(): void {}\n",
"}\n",
),
)],
);
let uri = Url::parse("file:///app_kernel2.php").unwrap();
let text = concat!(
"<?php\n",
"namespace App;\n",
"\n",
"class Kernel extends \\Vendor\\Kernel {\n",
" public function run(): void {\n",
" $this->bootstrap();\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 },
position: Position {
line: 5,
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 inherited $this->bootstrap() to parent Vendor\\Kernel"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
let path = location.uri.to_file_path().unwrap();
assert!(
path.ends_with("vendor/src/Kernel.php"),
"Should point to Vendor\\Kernel.php, got: {:?}",
path
);
assert_eq!(
location.range.start.line, 4,
"function bootstrap is on line 4 of Vendor\\Kernel.php"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_own_method_same_short_name_as_parent() {
let (backend, _dir) = create_psr4_workspace(
r#"{
"autoload": {
"psr-4": {
"App\\": "src/",
"Framework\\": "vendor/framework/src/"
}
}
}"#,
&[(
"vendor/framework/src/Console/Kernel.php",
concat!(
"<?php\n",
"namespace Framework\\Console;\n",
"\n",
"class Kernel {\n",
" protected function commands(): void {}\n",
"}\n",
),
)],
);
let uri = Url::parse("file:///app_kernel3.php").unwrap();
let text = concat!(
"<?php\n",
"namespace App\\Console;\n",
"\n",
"use Framework\\Console\\Kernel as ConsoleKernel;\n",
"\n",
"class Kernel extends ConsoleKernel {\n",
" protected function commands(): void {\n",
" // overridden\n",
" }\n",
"\n",
" public function run(): void {\n",
" $this->commands();\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: 11,
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 $this->commands() to the child's own override"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(
location.uri.as_str(),
uri.as_str(),
"Should point to current file (child override)"
);
assert_eq!(
location.range.start.line, 6,
"function commands is on line 6 of child Kernel"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_method_declaration_returns_self_location() {
let backend = create_test_backend();
let uri = Url::parse("file:///self_ref_method.php").unwrap();
let text = concat!(
"<?php\n",
"class GtdSelfRef {\n",
" public function paramTypes(string $item): void {\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: 2,
character: 25,
},
},
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_class_declaration_returns_self_location() {
let backend = create_test_backend();
let uri = Url::parse("file:///self_ref_class.php").unwrap();
let text = concat!(
"<?php\n",
"class MyUniqueTestClass {\n",
" public function foo(): void {}\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: 1,
character: 10,
},
},
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, 1, "should point back to line 1");
}
other => panic!("Expected self-location Scalar, got: {other:?}"),
}
}
#[tokio::test]
async fn test_goto_definition_define_constant_at_definition_returns_none() {
let backend = create_test_backend();
let uri = Url::parse("file:///self_ref_define.php").unwrap();
let text = concat!(
"<?php\n",
"define('APP_VERSION', '1.0.0');\n",
"\n",
"echo APP_VERSION;\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: 1,
character: 12,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result = backend.goto_definition(params).await.unwrap();
assert!(
result.is_none(),
"GTD on a constant name inside its own define() call should return None, got: {:?}",
result
);
let usage_params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri },
position: Position {
line: 3,
character: 7,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let usage_result = backend.goto_definition(usage_params).await.unwrap();
assert!(
usage_result.is_some(),
"GTD on a constant usage should still resolve to the define() call"
);
}
#[tokio::test]
async fn test_goto_definition_method_on_array_pop_same_file() {
let backend = create_test_backend();
let uri = Url::parse("file:///array_pop_gtd.php").unwrap();
let text = concat!(
"<?php\n", "class Pen {\n", " public function write(): void {}\n", "}\n", "class Holder {\n", " /** @return list<Pen> */\n", " public function getPens(): array { return []; }\n", " public function test(): void {\n", " $pens = $this->getPens();\n", " $last = array_pop($pens);\n", " $last->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: 10,
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 $last->write() via array_pop + $this->getPens()"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 2,
"write() is declared on line 2"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_method_on_array_pop_var_method_cross_file() {
let (backend, _dir) = create_psr4_workspace(
r#"{
"autoload": {
"psr-4": {
"Demo\\": "src/"
}
}
}"#,
&[
(
"src/Pen.php",
concat!(
"<?php\n",
"namespace Demo;\n",
"\n",
"class Pen {\n",
" public function write(): void {}\n",
"}\n",
),
),
(
"src/Source.php",
concat!(
"<?php\n",
"namespace Demo;\n",
"\n",
"class Source {\n",
" /** @return list<Pen> */\n",
" public function roster(): array { return []; }\n",
"}\n",
),
),
],
);
let uri = Url::parse("file:///consumer.php").unwrap();
let text = concat!(
"<?php\n", "namespace Demo;\n", "\n", "class Consumer {\n", " public function run(Source $src): void {\n", " $pens = $src->roster();\n", " $last = array_pop($pens);\n", " $last->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 },
position: Position {
line: 7,
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 $last->write() via array_pop + $src->roster() cross-file"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
let path = location.uri.to_file_path().unwrap();
assert!(
path.ends_with("src/Pen.php"),
"Should point to Pen.php, got: {:?}",
path
);
assert_eq!(
location.range.start.line, 4,
"write() is declared on line 4 of Pen.php"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_method_on_array_pop_different_namespace() {
let (backend, _dir) = create_psr4_workspace(
r#"{
"autoload": {
"psr-4": {
"Demo\\": "src/Demo/",
"App\\": "src/App/"
}
}
}"#,
&[
(
"src/Demo/Pen.php",
concat!(
"<?php\n",
"namespace Demo;\n",
"\n",
"class Pen {\n",
" public function write(): void {}\n",
"}\n",
),
),
(
"src/Demo/Source.php",
concat!(
"<?php\n",
"namespace Demo;\n",
"\n",
"class Source {\n",
" /** @return list<Pen> */\n",
" public function roster(): array { return []; }\n",
"}\n",
),
),
],
);
let uri = Url::parse("file:///consumer.php").unwrap();
let text = concat!(
"<?php\n", "namespace App;\n", "\n", "use Demo\\Source;\n", "\n", "class Consumer {\n", " public function run(Source $src): void {\n", " $pens = $src->roster();\n", " $last = array_pop($pens);\n", " $last->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 },
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 $last->write() via array_pop + $src->roster() across namespaces"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
let path = location.uri.to_file_path().unwrap();
assert!(
path.ends_with("src/Demo/Pen.php"),
"Should point to Demo/Pen.php, got: {:?}",
path
);
assert_eq!(
location.range.start.line, 4,
"write() is declared on line 4 of Pen.php"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_method_on_generator_yield_variable() {
let backend = create_test_backend();
let uri = Url::parse("file:///gen_yield_gtd.php").unwrap();
let text = concat!(
"<?php\n", "class User {\n", " public string $name;\n", " public function getEmail(): string {}\n", "}\n", "class UserRepository {\n", " /** @return \\Generator<int, User> */\n", " public function findAll(): \\Generator {\n", " yield $user;\n", " $user->getEmail();\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 $user->getEmail() via generator yield type inference"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 3,
"getEmail() is declared on line 3"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_property_on_generator_yield_pair_variable() {
let backend = create_test_backend();
let uri = Url::parse("file:///gen_yield_pair_gtd.php").unwrap();
let text = concat!(
"<?php\n", "class Product {\n", " public string $title;\n", "}\n", "class ProductLoader {\n", " /** @return \\Generator<int, Product> */\n", " public function loadAll(): \\Generator {\n", " yield 0 => $product;\n", " $product->title;\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: 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 $product->title via generator yield pair type inference"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(location.range.start.line, 2, "$title is declared on line 2");
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_method_on_generator_yield_top_level_function() {
let backend = create_test_backend();
let uri = Url::parse("file:///gen_yield_toplevel_gtd.php").unwrap();
let text = concat!(
"<?php\n", "class Customer {\n", " public string $name;\n", " public function greet(): string {}\n", "}\n", "/** @return \\Generator<int, Customer> */\n", "function generateCustomers(): \\Generator {\n", " yield $customer;\n", " $customer->greet();\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: 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 $customer->greet() via generator yield inference in top-level function"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 3,
"greet() is declared on line 3"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_method_on_generator_yield_cross_file() {
let (backend, _dir) = create_psr4_workspace(
r#"{
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
}"#,
&[(
"src/User.php",
concat!(
"<?php\n",
"namespace App;\n",
"\n",
"class User {\n",
" public function getEmail(): string {}\n",
"}\n",
),
)],
);
let uri = Url::parse("file:///repo.php").unwrap();
let text = concat!(
"<?php\n", "namespace App;\n", "\n", "class UserRepository {\n", " /** @return \\Generator<int, User> */\n", " public function findAll(): \\Generator {\n", " yield $user;\n", " $user->getEmail();\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 },
position: Position {
line: 7,
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 $user->getEmail() via generator yield inference cross-file"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
let path = location.uri.to_file_path().unwrap();
assert!(
path.ends_with("src/User.php"),
"Should point to User.php, got: {:?}",
path
);
assert_eq!(
location.range.start.line, 4,
"getEmail() is declared on line 4 of User.php"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_trait_alias_name_jumps_to_original_method() {
let backend = create_test_backend();
let uri = Url::parse("file:///trait_alias.php").unwrap();
let text = concat!(
"<?php\n",
"trait Notifiable {\n",
" public function routeNotificationFor(): mixed { return null; }\n",
"}\n",
"class User {\n",
" use Notifiable {\n",
" routeNotificationFor as _routeNotificationFor;\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 alias_col = text
.lines()
.nth(6)
.unwrap()
.find("_routeNotificationFor")
.unwrap();
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 6,
character: alias_col as u32 + 1,
},
},
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 trait alias _routeNotificationFor to the original method"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(
location.range.start.line, 2,
"Should point to routeNotificationFor in Notifiable trait (line 2)"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_trait_alias_original_method_reference() {
let backend = create_test_backend();
let uri = Url::parse("file:///trait_alias_orig.php").unwrap();
let text = concat!(
"<?php\n",
"trait Notifiable {\n",
" public function routeNotificationFor(): mixed { return null; }\n",
"}\n",
"class User {\n",
" use Notifiable {\n",
" routeNotificationFor as _routeNotificationFor;\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 method_col = text
.lines()
.nth(6)
.unwrap()
.find("routeNotificationFor")
.unwrap();
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 6,
character: method_col as u32 + 1,
},
},
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 original method name routeNotificationFor in alias declaration"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(
location.range.start.line, 2,
"Should point to routeNotificationFor in Notifiable trait (line 2)"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_trait_alias_qualified_method_reference() {
let backend = create_test_backend();
let uri = Url::parse("file:///trait_alias_qual.php").unwrap();
let text = concat!(
"<?php\n",
"trait TraitA {\n",
" public function shared(): void {}\n",
"}\n",
"trait TraitB {\n",
" public function shared(): void {}\n",
"}\n",
"class Widget {\n",
" use TraitA, TraitB {\n",
" TraitA::shared insteadof TraitB;\n",
" TraitB::shared as sharedFromB;\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 line10 = text.lines().nth(10).unwrap();
let shared_col = line10.find("shared").unwrap();
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 10,
character: shared_col as u32 + 1,
},
},
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 TraitB::shared in alias declaration"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(
location.range.start.line, 5,
"Should point to shared() in TraitB (line 5)"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_trait_name_in_insteadof() {
let backend = create_test_backend();
let uri = Url::parse("file:///trait_insteadof.php").unwrap();
let text = concat!(
"<?php\n",
"trait TraitA {\n",
" public function shared(): void {}\n",
"}\n",
"trait TraitB {\n",
" public function shared(): void {}\n",
"}\n",
"class Widget {\n",
" use TraitA, TraitB {\n",
" TraitA::shared insteadof TraitB;\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 line9 = text.lines().nth(9).unwrap();
let insteadof_trait_col = line9.rfind("TraitB").unwrap();
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 9,
character: insteadof_trait_col as u32 + 1,
},
},
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 TraitB in insteadof to the trait definition"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(
location.range.start.line, 4,
"Should point to TraitB declaration (line 4)"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_method_in_insteadof() {
let backend = create_test_backend();
let uri = Url::parse("file:///trait_insteadof_method.php").unwrap();
let text = concat!(
"<?php\n",
"trait TraitA {\n",
" public function shared(): void {}\n",
"}\n",
"trait TraitB {\n",
" public function shared(): void {}\n",
"}\n",
"class Widget {\n",
" use TraitA, TraitB {\n",
" TraitA::shared insteadof TraitB;\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 line9 = text.lines().nth(9).unwrap();
let shared_col = line9.find("shared").unwrap();
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 9,
character: shared_col as u32 + 1,
},
},
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 method name in insteadof declaration"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(
location.range.start.line, 2,
"Should point to shared() in TraitA (line 2)"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_trait_name_in_alias_adaptation() {
let backend = create_test_backend();
let uri = Url::parse("file:///trait_alias_traitname.php").unwrap();
let text = concat!(
"<?php\n",
"trait TraitA {\n",
" public function shared(): void {}\n",
"}\n",
"trait TraitB {\n",
" public function shared(): void {}\n",
"}\n",
"class Widget {\n",
" use TraitA, TraitB {\n",
" TraitA::shared insteadof TraitB;\n",
" TraitB::shared as sharedFromB;\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 line10 = text.lines().nth(10).unwrap();
let trait_col = line10.find("TraitB").unwrap();
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 10,
character: trait_col as u32 + 1,
},
},
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 TraitB name in alias adaptation to its definition"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(
location.range.start.line, 4,
"Should point to TraitB declaration (line 4)"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_trait_alias_cross_file() {
let trait_php = concat!(
"<?php\n",
"namespace App;\n",
"trait HasNotifications {\n",
" public function routeNotificationFor(): mixed { return null; }\n",
"}\n",
);
let class_php = concat!(
"<?php\n",
"namespace App;\n",
"class User {\n",
" use HasNotifications {\n",
" routeNotificationFor as _routeNotificationFor;\n",
" }\n",
"}\n",
);
let (backend, _dir) = create_psr4_workspace(
r#"{
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
}"#,
&[
("src/HasNotifications.php", trait_php),
("src/User.php", class_php),
],
);
let class_uri = Url::from_file_path(_dir.path().join("src/User.php")).unwrap();
backend
.did_open(DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: class_uri.clone(),
language_id: "php".to_string(),
version: 1,
text: class_php.to_string(),
},
})
.await;
let trait_uri = Url::from_file_path(_dir.path().join("src/HasNotifications.php")).unwrap();
backend
.did_open(DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: trait_uri.clone(),
language_id: "php".to_string(),
version: 1,
text: trait_php.to_string(),
},
})
.await;
let alias_col = class_php
.lines()
.nth(4)
.unwrap()
.find("_routeNotificationFor")
.unwrap();
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier {
uri: class_uri.clone(),
},
position: Position {
line: 4,
character: alias_col as u32 + 1,
},
},
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 cross-file trait alias _routeNotificationFor"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
let path = location.uri.to_file_path().unwrap();
assert!(
path.ends_with("HasNotifications.php"),
"Should point to HasNotifications.php, got: {:?}",
path
);
assert_eq!(
location.range.start.line, 3,
"routeNotificationFor is declared on line 3 of HasNotifications.php"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_trait_alias_when_class_overrides_original_method() {
let backend = create_test_backend();
let uri = Url::parse("file:///trait_alias_override.php").unwrap();
let text = concat!(
"<?php\n",
"trait Foo {\n",
" public function foo(): string { return 'foo'; }\n",
"}\n",
"class User {\n",
" use Foo {\n",
" foo as __foo;\n",
" }\n",
" public function foo(): string {\n",
" return $this->__foo() . 'bar';\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 line9 = text.lines().nth(9).unwrap();
let alias_col = line9.find("__foo").unwrap();
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 9,
character: alias_col as u32 + 1,
},
},
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 $this->__foo() to the trait's foo() method"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(
location.range.start.line, 2,
"Should point to foo() in trait Foo (line 2), not User::foo() (line 8)"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_trait_alias_decl_when_class_overrides_original_method() {
let backend = create_test_backend();
let uri = Url::parse("file:///trait_alias_decl_override.php").unwrap();
let text = concat!(
"<?php\n",
"trait Foo {\n",
" public function foo(): string { return 'foo'; }\n",
"}\n",
"class User {\n",
" use Foo {\n",
" foo as __foo;\n",
" }\n",
" public function foo(): string {\n",
" return $this->__foo() . 'bar';\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 line6 = text.lines().nth(6).unwrap();
let alias_col = line6.find("__foo").unwrap();
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 6,
character: alias_col as u32 + 1,
},
},
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 alias __foo in trait use block to the trait's foo()"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(
location.range.start.line, 2,
"Should point to foo() in trait Foo (line 2), not User::foo() (line 8)"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_trait_alias_original_name_when_class_overrides() {
let backend = create_test_backend();
let uri = Url::parse("file:///trait_alias_orig_override.php").unwrap();
let text = concat!(
"<?php\n",
"trait Foo {\n",
" public function foo(): string { return 'foo'; }\n",
"}\n",
"class User {\n",
" use Foo {\n",
" foo as __foo;\n",
" }\n",
" public function foo(): string {\n",
" return $this->__foo() . 'bar';\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 line6 = text.lines().nth(6).unwrap();
let foo_col = line6.find("foo").unwrap();
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 6,
character: foo_col as u32 + 1,
},
},
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 original method name foo in alias adaptation"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(
location.range.start.line, 2,
"Should point to foo() in trait Foo (line 2), not User::foo() (line 8)"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_see_tag_in_array_no_namespace() {
let backend = create_test_backend();
let uri = Url::parse("file:///test_see_array.php").unwrap();
let text = concat!(
"<?php\n",
"class SupervisorOptions {\n",
" public int $balanceCooldown = 3;\n",
"}\n",
"$defaults = [\n",
" 'balanceCooldown' => 3, /** @see SupervisorOptions::$balanceCooldown */\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: 40, },
},
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 @see class reference in floating docblock inside array literal"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(
location.range.start.line, 1,
"SupervisorOptions is defined on line 1"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 5,
character: 62, },
},
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 @see member reference in floating docblock inside array literal"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(
location.range.start.line, 2,
"$balanceCooldown property is defined on line 2"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_see_tag_inline_in_expression() {
let backend = create_test_backend();
let uri = Url::parse("file:///test_see_inline.php").unwrap();
let text = concat!(
"<?php\n",
"class Config {\n",
" public string $timeout = '30';\n",
"}\n",
"\n",
"$timeout = 30; /** @see Config::$timeout */\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: 25, },
},
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 @see class reference in trailing docblock after expression"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.range.start.line, 1, "Config is defined on line 1");
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_see_tag_cross_file_no_namespace() {
let (backend, _dir) = create_psr4_workspace(
r#"{
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
}"#,
&[(
"src/Models/SupervisorOptions.php",
concat!(
"<?php\n",
"namespace App\\Models;\n",
"class SupervisorOptions {\n",
" public int $balanceCooldown = 3;\n",
"}\n",
),
)],
);
let config_uri = Url::parse("file:///config/horizon.php").unwrap();
let config_text = concat!(
"<?php\n",
"use App\\Models\\SupervisorOptions;\n",
"$defaults = [\n",
" 'balanceCooldown' => 3, /** @see SupervisorOptions::$balanceCooldown */\n",
"];\n",
);
let open_params = DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: config_uri.clone(),
language_id: "php".to_string(),
version: 1,
text: config_text.to_string(),
},
};
backend.did_open(open_params).await;
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier {
uri: config_uri.clone(),
},
position: Position {
line: 3,
character: 40, },
},
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 @see class reference cross-file from a no-namespace config file"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
let path = location.uri.to_file_path().unwrap();
assert!(
path.ends_with("Models/SupervisorOptions.php"),
"Should point to SupervisorOptions.php, got: {:?}",
path
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}