use crate::common::create_test_backend;
use tower_lsp::LanguageServer;
use tower_lsp::lsp_types::*;
async fn complete_at(
backend: &phpantom_lsp::Backend,
uri: &Url,
text: &str,
line: u32,
character: u32,
) -> Option<CompletionResponse> {
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 completion_params = CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position { line, character },
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: None,
};
backend.completion(completion_params).await.unwrap()
}
fn assert_has_member(items: &[CompletionItem], member: &str) {
let names: Vec<&str> = items
.iter()
.map(|i| i.filter_text.as_deref().unwrap_or(&i.label))
.collect();
assert!(
names.contains(&member),
"Should suggest '{}', got: {:?}",
member,
names
);
}
fn unwrap_items(response: Option<CompletionResponse>) -> Vec<CompletionItem> {
match response.expect("Should return completion results") {
CompletionResponse::Array(items) => items,
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_var_array_int_customer_named_annotation() {
let backend = create_test_backend();
let uri = Url::parse("file:///test_arr_named.php").unwrap();
let text = concat!(
"<?php\n",
"class Customer {\n",
" public string $name;\n",
" public function getEmail(): string {}\n",
"}\n",
"/** @var array<int, Customer> $thing */\n",
"$thing = [];\n",
"$thing[0]->\n",
);
let result = complete_at(&backend, &uri, text, 7, 11).await;
let items = unwrap_items(result);
assert_has_member(&items, "name");
assert_has_member(&items, "getEmail");
}
#[tokio::test]
async fn test_var_array_int_customer_no_varname_annotation() {
let backend = create_test_backend();
let uri = Url::parse("file:///test_arr_no_varname.php").unwrap();
let text = concat!(
"<?php\n",
"class Customer {\n",
" public string $name;\n",
" public function getEmail(): string {}\n",
"}\n",
"/** @var array<int, Customer> */\n",
"$thing = [];\n",
"$thing[0]->\n",
);
let result = complete_at(&backend, &uri, text, 7, 11).await;
let items = unwrap_items(result);
assert_has_member(&items, "name");
assert_has_member(&items, "getEmail");
}
#[tokio::test]
async fn test_var_array_int_customer_empty_array_access() {
let backend = create_test_backend();
let uri = Url::parse("file:///test_arr_int_cust.php").unwrap();
let text = concat!(
"<?php\n",
"class Customer {\n",
" public string $name;\n",
" public function getEmail(): string {}\n",
"}\n",
"/** @var array<int, Customer> $thing */\n",
"$thing = getUnknownValue();\n",
"$thing[0]->\n",
);
let result = complete_at(&backend, &uri, text, 7, 11).await;
let items = unwrap_items(result);
assert_has_member(&items, "name");
assert_has_member(&items, "getEmail");
}
#[tokio::test]
async fn test_var_array_single_param_customer_access() {
let backend = create_test_backend();
let uri = Url::parse("file:///test_arr_single_cust.php").unwrap();
let text = concat!(
"<?php\n",
"class Customer {\n",
" public string $name;\n",
" public function getEmail(): string {}\n",
"}\n",
"/** @var array<Customer> $thing */\n",
"$thing = [];\n",
"$thing[0]->\n",
);
let result = complete_at(&backend, &uri, text, 7, 11).await;
let items = unwrap_items(result);
assert_has_member(&items, "name");
assert_has_member(&items, "getEmail");
}
#[tokio::test]
async fn test_var_list_customer_access() {
let backend = create_test_backend();
let uri = Url::parse("file:///test_list_cust.php").unwrap();
let text = concat!(
"<?php\n",
"class Customer {\n",
" public string $name;\n",
" public function getEmail(): string {}\n",
"}\n",
"/** @var list<Customer> $thing */\n",
"$thing = [];\n",
"$thing[0]->\n",
);
let result = complete_at(&backend, &uri, text, 7, 11).await;
let items = unwrap_items(result);
assert_has_member(&items, "name");
assert_has_member(&items, "getEmail");
}
#[tokio::test]
async fn test_var_list_customer_no_varname_access() {
let backend = create_test_backend();
let uri = Url::parse("file:///test_list_cust_novar.php").unwrap();
let text = concat!(
"<?php\n",
"class Customer {\n",
" public string $name;\n",
" public function getEmail(): string {}\n",
"}\n",
"/** @var list<Customer> */\n",
"$thing = [];\n",
"$thing[0]->\n",
);
let result = complete_at(&backend, &uri, text, 7, 11).await;
let items = unwrap_items(result);
assert_has_member(&items, "name");
assert_has_member(&items, "getEmail");
}
#[tokio::test]
async fn test_inferred_array_new_object_access() {
let backend = create_test_backend();
let uri = Url::parse("file:///test_inferred_arr.php").unwrap();
let text = concat!(
"<?php\n",
"class Customer {\n",
" public string $name;\n",
" public function getEmail(): string {}\n",
"}\n",
"$thing = [new Customer()];\n",
"$thing[0]->\n",
);
let result = complete_at(&backend, &uri, text, 6, 11).await;
let items = unwrap_items(result);
assert_has_member(&items, "name");
assert_has_member(&items, "getEmail");
}
#[tokio::test]
async fn test_inline_array_literal_static_call_access() {
let backend = create_test_backend();
let uri = Url::parse("file:///test_inline_arr.php").unwrap();
let text = concat!(
"<?php\n",
"class Customer {\n",
" public string $name;\n",
" public function getEmail(): string {}\n",
" /** @return static */\n",
" public static function first(): static {}\n",
"}\n",
"[Customer::first()][0]->\n",
);
let result = complete_at(&backend, &uri, text, 7, 24).await;
let items = unwrap_items(result);
assert_has_member(&items, "name");
assert_has_member(&items, "getEmail");
}
#[tokio::test]
async fn test_end_of_method_chain_returning_array() {
let backend = create_test_backend();
let uri = Url::parse("file:///test_end_chain.php").unwrap();
let text = concat!(
"<?php\n",
"class Customer {\n",
" public string $name;\n",
" public function getEmail(): string {}\n",
" /** @return Collection<int, static> */\n",
" public static function get(): Collection {}\n",
"}\n",
"class Collection {\n",
" /** @return array<int, Customer> */\n",
" public function all(): array {}\n",
"}\n",
"end(Customer::get()->all())->\n",
);
let result = complete_at(&backend, &uri, text, 11, 29).await;
let items = unwrap_items(result);
assert_has_member(&items, "name");
assert_has_member(&items, "getEmail");
}
#[tokio::test]
async fn test_variable_assigned_from_end_array_generic() {
let backend = create_test_backend();
let uri = Url::parse("file:///test_end_assign.php").unwrap();
let text = concat!(
"<?php\n",
"class Customer {\n",
" public string $name;\n",
" public function getEmail(): string {}\n",
"}\n",
"/** @var array<int, Customer> $customers */\n",
"$customers = [];\n",
"$last = end($customers);\n",
"$last->\n",
);
let result = complete_at(&backend, &uri, text, 8, 7).await;
let items = unwrap_items(result);
assert_has_member(&items, "name");
assert_has_member(&items, "getEmail");
}
#[tokio::test]
async fn test_var_array_generic_with_unknown_value_rhs() {
let backend = create_test_backend();
let uri = Url::parse("file:///test_arr_unknown_rhs.php").unwrap();
let text = concat!(
"<?php\n",
"class Customer {\n",
" public string $name;\n",
" public function getEmail(): string {}\n",
"}\n",
"function getUnknownValue(): mixed { return null; }\n",
"/** @var array<int, Customer> $thing */\n",
"$thing = getUnknownValue();\n",
"$thing[0]->\n",
);
let result = complete_at(&backend, &uri, text, 8, 11).await;
let items = unwrap_items(result);
assert_has_member(&items, "name");
assert_has_member(&items, "getEmail");
}
#[tokio::test]
async fn test_method_return_array_access_bracket_type() {
let backend = create_test_backend();
let uri = Url::parse("file:///test_method_arr.php").unwrap();
let text = concat!(
"<?php\n",
"class Item {\n",
" public function getLabel(): string { return ''; }\n",
"}\n",
"class Collection {\n",
" /** @return Item[] */\n",
" public function items(): array { return []; }\n",
"}\n",
"class Consumer {\n",
" public function run(): void {\n",
" $c = new Collection();\n",
" $c->items()[0]->\n",
" }\n",
"}\n",
);
let result = complete_at(&backend, &uri, text, 11, 24).await;
let items = unwrap_items(result);
assert_has_member(&items, "getLabel");
}
#[tokio::test]
async fn test_method_return_array_access_generic_type() {
let backend = create_test_backend();
let uri = Url::parse("file:///test_method_arr_generic.php").unwrap();
let text = concat!(
"<?php\n",
"class Item {\n",
" public function getLabel(): string { return ''; }\n",
"}\n",
"class Collection {\n",
" /** @return array<int, Item> */\n",
" public function items(): array { return []; }\n",
"}\n",
"class Consumer {\n",
" public function run(): void {\n",
" $c = new Collection();\n",
" $c->items()[0]->\n",
" }\n",
"}\n",
);
let result = complete_at(&backend, &uri, text, 11, 24).await;
let items = unwrap_items(result);
assert_has_member(&items, "getLabel");
}
#[tokio::test]
async fn test_static_method_return_array_access() {
let backend = create_test_backend();
let uri = Url::parse("file:///test_static_method_arr.php").unwrap();
let text = concat!(
"<?php\n",
"class Item {\n",
" public function getLabel(): string { return ''; }\n",
"}\n",
"class Collection {\n",
" /** @return Item[] */\n",
" public static function all(): array { return []; }\n",
"}\n",
"class Consumer {\n",
" public function run(): void {\n",
" Collection::all()[0]->\n",
" }\n",
"}\n",
);
let result = complete_at(&backend, &uri, text, 10, 30).await;
let items = unwrap_items(result);
assert_has_member(&items, "getLabel");
}
#[tokio::test]
async fn test_method_return_list_array_access() {
let backend = create_test_backend();
let uri = Url::parse("file:///test_method_list_arr.php").unwrap();
let text = concat!(
"<?php\n",
"class Item {\n",
" public function getLabel(): string { return ''; }\n",
"}\n",
"class Collection {\n",
" /** @return list<Item> */\n",
" public function items(): array { return []; }\n",
"}\n",
"class Consumer {\n",
" public function run(): void {\n",
" $c = new Collection();\n",
" $c->items()[0]->\n",
" }\n",
"}\n",
);
let result = complete_at(&backend, &uri, text, 11, 24).await;
let items = unwrap_items(result);
assert_has_member(&items, "getLabel");
}
#[tokio::test]
async fn test_property_generic_array_bracket_access() {
let backend = create_test_backend();
let uri = Url::parse("file:///test_prop_generic_arr.php").unwrap();
let text = concat!(
"<?php\n",
"class IntCollection {\n",
" public function contains(int $id): bool { return false; }\n",
" public function count(): int { return 0; }\n",
"}\n",
"class SalesCampaignGroup {\n",
" /** @var array<string, IntCollection> */\n",
" private array $cache = [];\n",
"\n",
" public function check(string $key, int $id): bool {\n",
" return $this->cache[$key]->\n",
" }\n",
"}\n",
);
let result = complete_at(&backend, &uri, text, 10, 39).await;
let items = unwrap_items(result);
assert_has_member(&items, "contains");
assert_has_member(&items, "count");
}
#[tokio::test]
async fn test_property_collection_generic_bracket_access() {
let backend = create_test_backend();
let uri = Url::parse("file:///test_prop_collection_arr.php").unwrap();
let text = concat!(
"<?php\n",
"class Translation {\n",
" public string $name;\n",
" public function getLocale(): string { return ''; }\n",
"}\n",
"/**\n",
" * @template TKey\n",
" * @template TValue\n",
" */\n",
"class Collection {\n",
" /** @return TValue */\n",
" public function first() {}\n",
"}\n",
"class Product {\n",
" /** @var Collection<int, Translation> */\n",
" public Collection $translations;\n",
"\n",
" public function getTranslationName(): string {\n",
" return $this->translations[0]->\n",
" }\n",
"}\n",
);
let result = complete_at(&backend, &uri, text, 18, 42).await;
let items = unwrap_items(result);
assert_has_member(&items, "name");
assert_has_member(&items, "getLocale");
}
#[tokio::test]
async fn test_variable_generic_array_bracket_access_var_annotation() {
let backend = create_test_backend();
let uri = Url::parse("file:///test_var_generic_arr_key.php").unwrap();
let text = concat!(
"<?php\n",
"class Order {\n",
" public int $id;\n",
" public function getTotal(): float { return 0.0; }\n",
"}\n",
"/** @var array<string, Order> $orders */\n",
"$orders = [];\n",
"$orders['abc']->\n",
);
let result = complete_at(&backend, &uri, text, 7, 16).await;
let items = unwrap_items(result);
assert_has_member(&items, "id");
assert_has_member(&items, "getTotal");
}
#[tokio::test]
async fn test_object_property_generic_array_bracket_access() {
let backend = create_test_backend();
let uri = Url::parse("file:///test_obj_prop_generic_arr.php").unwrap();
let text = concat!(
"<?php\n",
"class Widget {\n",
" public string $label;\n",
" public function render(): string { return ''; }\n",
"}\n",
"class WidgetService {\n",
" /** @var array<int, Widget> */\n",
" public array $items = [];\n",
"}\n",
"function test(WidgetService $service): void {\n",
" $service->items[0]->\n",
"}\n",
);
let result = complete_at(&backend, &uri, text, 10, 24).await;
let items = unwrap_items(result);
assert_has_member(&items, "label");
assert_has_member(&items, "render");
}
#[tokio::test]
async fn test_property_generic_array_bracket_access_then_method_chain() {
let backend = create_test_backend();
let uri = Url::parse("file:///test_prop_arr_chain.php").unwrap();
let text = concat!(
"<?php\n",
"class Result {\n",
" public string $value;\n",
"}\n",
"class ResultSet {\n",
" public function first(): Result { return new Result(); }\n",
"}\n",
"class Cache {\n",
" /** @var array<string, ResultSet> */\n",
" private array $data = [];\n",
"\n",
" public function lookup(string $key): void {\n",
" $this->data[$key]->first()->\n",
" }\n",
"}\n",
);
let result = complete_at(&backend, &uri, text, 12, 38).await;
let items = unwrap_items(result);
assert_has_member(&items, "value");
}
#[tokio::test]
async fn test_property_generic_array_string_literal_key_access() {
let backend = create_test_backend();
let uri = Url::parse("file:///test_prop_generic_arr_strkey.php").unwrap();
let text = concat!(
"<?php\n",
"class IntCollection {\n",
" public function contains(int $id): bool { return false; }\n",
" public function count(): int { return 0; }\n",
"}\n",
"class CacheHolder {\n",
" /** @var array<string, IntCollection> */\n",
" private array $cache = [];\n",
"\n",
" public function check(int $id): bool {\n",
" return $this->cache['myKey']->\n",
" }\n",
"}\n",
);
let result = complete_at(&backend, &uri, text, 10, 41).await;
let items = unwrap_items(result);
assert_has_member(&items, "contains");
assert_has_member(&items, "count");
}