use crate::common::{create_psr4_workspace, create_test_backend};
use phpantom_lsp::php_type::PhpType;
use tower_lsp::LanguageServer;
use tower_lsp::lsp_types::*;
#[tokio::test]
async fn test_goto_definition_union_return_type_function() {
let backend = create_test_backend();
let uri = Url::parse("file:///test.php").unwrap();
let text = concat!(
"<?php\n", "class B {\n", " public function onlyOnB(): void {}\n", "}\n", "\n", "class C {\n", " public function onlyOnC(): void {}\n", "}\n", "\n", "class App {\n", " public function getBC(): B|C { return new B(); }\n", "\n", " public function run(): void {\n", " $a = $this->getBC();\n", " $a->onlyOnB();\n", " $a->onlyOnC();\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: 14,
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 $a->onlyOnB() via union return type B|C"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 2,
"onlyOnB is declared on line 2 in class B"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
let params2 = 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 result2 = backend.goto_definition(params2).await.unwrap();
assert!(
result2.is_some(),
"Should resolve $a->onlyOnC() via union return type B|C"
);
match result2.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 6,
"onlyOnC is declared on line 6 in class C"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_union_return_type_standalone_function() {
let backend = create_test_backend();
let uri = Url::parse("file:///test.php").unwrap();
let text = concat!(
"<?php\n", "class Dog {\n", " public function bark(): void {}\n", "}\n", "\n", "class Cat {\n", " public function meow(): void {}\n", "}\n", "\n", "function getAnimal(): Dog|Cat {\n", " return new Dog();\n", "}\n", "\n", "class App {\n", " public function run(): void {\n", " $pet = getAnimal();\n", " $pet->bark();\n", " $pet->meow();\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 mut fmap = backend.global_functions().write();
fmap.insert(
"getAnimal".to_string(),
(
uri.to_string(),
phpantom_lsp::FunctionInfo {
name: "getAnimal".to_string(),
name_offset: 0,
parameters: vec![],
return_type: Some(PhpType::parse("Dog|Cat")),
native_return_type: None,
description: None,
return_description: None,
links: vec![],
see_refs: vec![],
namespace: None,
conditional_return: None,
type_assertions: vec![],
deprecation_message: None,
deprecated_replacement: None,
template_params: vec![],
template_bindings: vec![],
throws: vec![],
is_polyfill: false,
},
),
);
}
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 16,
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 $pet->bark() via Dog|Cat union return type"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 2,
"bark is declared on line 2 in Dog"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
let params2 = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 17,
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(),
"Should resolve $pet->meow() via Dog|Cat union return type"
);
match result2.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 6,
"meow is declared on line 6 in Cat"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_nullable_union_return_type() {
let backend = create_test_backend();
let uri = Url::parse("file:///test.php").unwrap();
let text = concat!(
"<?php\n", "class Formatter {\n", " public function format(string $s): string { return $s; }\n", "}\n", "\n", "class App {\n", " public function getFormatter(): ?Formatter {\n", " return new Formatter();\n", " }\n", "\n", " public function run(): void {\n", " $f = $this->getFormatter();\n", " $f->format('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: 12,
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 $f->format() via ?Formatter nullable return type"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 2,
"format is declared on line 2 in Formatter"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_union_property_type() {
let backend = create_test_backend();
let uri = Url::parse("file:///test.php").unwrap();
let text = concat!(
"<?php\n", "class Engine {\n", " public function start(): void {}\n", "}\n", "\n", "class Motor {\n", " public function rev(): void {}\n", "}\n", "\n", "class Car {\n", " public Engine|Motor $powerUnit;\n", "\n", " public function run(): void {\n", " $this->powerUnit->start();\n", " $this->powerUnit->rev();\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: 30,
},
},
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->powerUnit->start() via Engine|Motor union property type"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 2,
"start is declared on line 2 in Engine"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
let params2 = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 14,
character: 27,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result2 = backend.goto_definition(params2).await.unwrap();
assert!(
result2.is_some(),
"Should resolve $this->powerUnit->rev() via Engine|Motor union property type"
);
match result2.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 6,
"rev is declared on line 6 in Motor"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_union_parameter_type_hint() {
let backend = create_test_backend();
let uri = Url::parse("file:///test.php").unwrap();
let text = concat!(
"<?php\n", "class Reader {\n", " public function read(): void {}\n", "}\n", "\n", "class Stream {\n", " public function consume(): void {}\n", "}\n", "\n", "class App {\n", " public function process(Reader|Stream $input): void {\n", " $input->read();\n", " $input->consume();\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: 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 $input->read() via Reader|Stream union param type"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 2,
"read is declared on line 2 in Reader"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
let params2 = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 12,
character: 18,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result2 = backend.goto_definition(params2).await.unwrap();
assert!(
result2.is_some(),
"Should resolve $input->consume() via Reader|Stream union param type"
);
match result2.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 6,
"consume is declared on line 6 in Stream"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_union_with_scalar_parts() {
let backend = create_test_backend();
let uri = Url::parse("file:///test.php").unwrap();
let text = concat!(
"<?php\n", "class Result {\n", " public function unwrap(): mixed { return null; }\n", "}\n", "\n", "class App {\n", " public function fetch(): string|Result {\n", " return new Result();\n", " }\n", "\n", " public function run(): void {\n", " $r = $this->fetch();\n", " $r->unwrap();\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: 12,
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 $r->unwrap() via string|Result, ignoring scalar part"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(location.uri, uri);
assert_eq!(
location.range.start.line, 2,
"unwrap is declared on line 2 in Result"
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}
#[tokio::test]
async fn test_goto_definition_union_return_type_cross_file() {
let (backend, _dir) = create_psr4_workspace(
r#"{"autoload":{"psr-4":{"App\\":"src/"}}}"#,
&[
(
"src/Encoder.php",
concat!(
"<?php\n",
"namespace App;\n",
"class Encoder {\n",
" public function encode(string $data): string { return $data; }\n",
"}\n",
),
),
(
"src/Decoder.php",
concat!(
"<?php\n",
"namespace App;\n",
"class Decoder {\n",
" public function decode(string $data): string { return $data; }\n",
"}\n",
),
),
],
);
let uri = Url::parse("file:///test_main.php").unwrap();
let text = concat!(
"<?php\n",
"use App\\Encoder;\n",
"use App\\Decoder;\n",
"\n",
"class Codec {\n",
" public function getCodec(): Encoder|Decoder {\n",
" return new Encoder();\n",
" }\n",
"\n",
" public function run(): void {\n",
" $c = $this->getCodec();\n",
" $c->encode('data');\n",
" $c->decode('data');\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: 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 $c->encode() via Encoder|Decoder union return type (cross-file)"
);
match result.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(
location.range.start.line, 3,
"encode is on line 3 of Encoder.php"
);
let loc_path = location.uri.to_file_path().unwrap();
assert!(
loc_path.ends_with("src/Encoder.php"),
"Should resolve to Encoder.php, got: {:?}",
loc_path
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
let params2 = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 12,
character: 14,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let result2 = backend.goto_definition(params2).await.unwrap();
assert!(
result2.is_some(),
"Should resolve $c->decode() via Encoder|Decoder union return type (cross-file)"
);
match result2.unwrap() {
GotoDefinitionResponse::Scalar(location) => {
assert_eq!(
location.range.start.line, 3,
"decode is on line 3 of Decoder.php"
);
let loc_path = location.uri.to_file_path().unwrap();
assert!(
loc_path.ends_with("src/Decoder.php"),
"Should resolve to Decoder.php, got: {:?}",
loc_path
);
}
other => panic!("Expected Scalar location, got: {:?}", other),
}
}