use crate::common::{create_psr4_workspace, create_test_backend};
use tower_lsp::LanguageServer;
use tower_lsp::lsp_types::*;
#[tokio::test]
async fn test_var_property_chain_simple() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_prop_chain.php").unwrap();
let text = concat!(
"<?php\n",
"class Address {\n",
" public string $city;\n",
" public string $zip;\n",
" public function format(): string {}\n",
"}\n",
"class User {\n",
" public Address $address;\n",
" public string $name;\n",
"}\n",
"function demo() {\n",
" $user = new User();\n",
" $user->address->\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 completion_params = CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri },
position: Position {
line: 12,
character: 22,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: None,
};
let result = backend.completion(completion_params).await.unwrap();
assert!(
result.is_some(),
"Completion should return results for $user->address->"
);
match result.unwrap() {
CompletionResponse::Array(items) => {
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(
labels.iter().any(|l| l.starts_with("city")),
"Should include 'city' property from Address. Got: {:?}",
labels
);
assert!(
labels.iter().any(|l| l.starts_with("zip")),
"Should include 'zip' property from Address. Got: {:?}",
labels
);
assert!(
labels.iter().any(|l| l.starts_with("format")),
"Should include 'format' method from Address. Got: {:?}",
labels
);
assert!(
!labels.iter().any(|l| l.starts_with("name")),
"Should NOT include 'name' from User. Got: {:?}",
labels
);
}
CompletionResponse::List(_) => panic!("Expected Array response"),
}
}
#[tokio::test]
async fn test_var_property_chain_deep() {
let backend = create_test_backend();
let uri = Url::parse("file:///deep_chain.php").unwrap();
let text = concat!(
"<?php\n",
"class Address {\n",
" public string $street;\n",
" public string $city;\n",
"}\n",
"class Customer {\n",
" public Address $address;\n",
" public string $email;\n",
"}\n",
"class Order {\n",
" public Customer $customer;\n",
" public float $total;\n",
"}\n",
"function demo() {\n",
" $order = new Order();\n",
" $order->customer->address->\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 completion_params = CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri },
position: Position {
line: 15,
character: 34,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: None,
};
let result = backend.completion(completion_params).await.unwrap();
assert!(
result.is_some(),
"Completion should return results for $order->customer->address->"
);
match result.unwrap() {
CompletionResponse::Array(items) => {
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(
labels.iter().any(|l| l.starts_with("street")),
"Should include 'street' from Address. Got: {:?}",
labels
);
assert!(
labels.iter().any(|l| l.starts_with("city")),
"Should include 'city' from Address. Got: {:?}",
labels
);
assert!(
!labels.iter().any(|l| l.starts_with("email")),
"Should NOT include 'email' from Customer. Got: {:?}",
labels
);
assert!(
!labels.iter().any(|l| l.starts_with("total")),
"Should NOT include 'total' from Order. Got: {:?}",
labels
);
}
CompletionResponse::List(_) => panic!("Expected Array response"),
}
}
#[tokio::test]
async fn test_var_property_chain_from_parameter() {
let backend = create_test_backend();
let uri = Url::parse("file:///param_chain.php").unwrap();
let text = concat!(
"<?php\n",
"class Address {\n",
" public string $city;\n",
" public function getZip(): string {}\n",
"}\n",
"class User {\n",
" public Address $address;\n",
"}\n",
"function processUser(User $user) {\n",
" $user->address->\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 completion_params = CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri },
position: Position {
line: 9,
character: 22,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: None,
};
let result = backend.completion(completion_params).await.unwrap();
assert!(
result.is_some(),
"Completion should return results for parameter $user->address->"
);
match result.unwrap() {
CompletionResponse::Array(items) => {
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(
labels.iter().any(|l| l.starts_with("city")),
"Should include 'city' from Address. Got: {:?}",
labels
);
assert!(
labels.iter().any(|l| l.starts_with("getZip")),
"Should include 'getZip' from Address. Got: {:?}",
labels
);
}
CompletionResponse::List(_) => panic!("Expected Array response"),
}
}
#[tokio::test]
async fn test_var_property_chain_nullsafe() {
let backend = create_test_backend();
let uri = Url::parse("file:///nullsafe_chain.php").unwrap();
let text = concat!(
"<?php\n",
"class Engine {\n",
" public int $horsepower;\n",
" public function start(): void {}\n",
"}\n",
"class Car {\n",
" public ?Engine $engine;\n",
"}\n",
"function demo(?Car $car) {\n",
" $car?->engine->\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 completion_params = CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri },
position: Position {
line: 9,
character: 21,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: None,
};
let result = backend.completion(completion_params).await.unwrap();
assert!(
result.is_some(),
"Completion should return results for $car?->engine->"
);
match result.unwrap() {
CompletionResponse::Array(items) => {
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(
labels.iter().any(|l| l.starts_with("horsepower")),
"Should include 'horsepower' from Engine. Got: {:?}",
labels
);
assert!(
labels.iter().any(|l| l.starts_with("start")),
"Should include 'start' from Engine. Got: {:?}",
labels
);
}
CompletionResponse::List(_) => panic!("Expected Array response"),
}
}
#[tokio::test]
async fn test_var_property_chain_from_docblock_var() {
let backend = create_test_backend();
let uri = Url::parse("file:///docblock_chain.php").unwrap();
let text = concat!(
"<?php\n",
"class Settings {\n",
" public bool $debug;\n",
" public string $locale;\n",
"}\n",
"class Config {\n",
" public Settings $settings;\n",
"}\n",
"function demo() {\n",
" /** @var Config $config */\n",
" $config = getConfig();\n",
" $config->settings->\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 completion_params = CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri },
position: Position {
line: 11,
character: 24,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: None,
};
let result = backend.completion(completion_params).await.unwrap();
assert!(
result.is_some(),
"Completion should return results for @var annotated $config->settings->"
);
match result.unwrap() {
CompletionResponse::Array(items) => {
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(
labels.iter().any(|l| l.starts_with("debug")),
"Should include 'debug' from Settings. Got: {:?}",
labels
);
assert!(
labels.iter().any(|l| l.starts_with("locale")),
"Should include 'locale' from Settings. Got: {:?}",
labels
);
}
CompletionResponse::List(_) => panic!("Expected Array response"),
}
}
#[tokio::test]
async fn test_var_method_then_property_chain() {
let backend = create_test_backend();
let uri = Url::parse("file:///method_prop_chain.php").unwrap();
let text = concat!(
"<?php\n",
"class Address {\n",
" public string $city;\n",
"}\n",
"class Customer {\n",
" public Address $address;\n",
"}\n",
"class Order {\n",
" public function getCustomer(): Customer {}\n",
"}\n",
"function demo() {\n",
" $order = new Order();\n",
" $order->getCustomer()->address->\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 completion_params = CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri },
position: Position {
line: 12,
character: 38,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: None,
};
let result = backend.completion(completion_params).await.unwrap();
assert!(
result.is_some(),
"Completion should return results for $order->getCustomer()->address->"
);
match result.unwrap() {
CompletionResponse::Array(items) => {
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(
labels.iter().any(|l| l.starts_with("city")),
"Should include 'city' from Address. Got: {:?}",
labels
);
}
CompletionResponse::List(_) => panic!("Expected Array response"),
}
}
#[tokio::test]
async fn test_var_property_chain_inside_class() {
let backend = create_test_backend();
let uri = Url::parse("file:///inside_class.php").unwrap();
let text = concat!(
"<?php\n",
"class Logger {\n",
" public function log(string $msg): void {}\n",
"}\n",
"class Database {\n",
" public Logger $logger;\n",
"}\n",
"class Service {\n",
" public function run() {\n",
" $db = new Database();\n",
" $db->logger->\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 completion_params = CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri },
position: Position {
line: 10,
character: 21,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: None,
};
let result = backend.completion(completion_params).await.unwrap();
assert!(
result.is_some(),
"Completion should return results for $db->logger->"
);
match result.unwrap() {
CompletionResponse::Array(items) => {
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(
labels.iter().any(|l| l.starts_with("log")),
"Should include 'log' from Logger. Got: {:?}",
labels
);
}
CompletionResponse::List(_) => panic!("Expected Array response"),
}
}
#[tokio::test]
async fn test_var_property_chain_docblock_property_type() {
let backend = create_test_backend();
let uri = Url::parse("file:///docblock_prop_type.php").unwrap();
let text = concat!(
"<?php\n",
"class Renderer {\n",
" public function render(): string {}\n",
"}\n",
"class View {\n",
" /** @var Renderer */\n",
" public $renderer;\n",
"}\n",
"function demo() {\n",
" $view = new View();\n",
" $view->renderer->\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 completion_params = CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri },
position: Position {
line: 10,
character: 22,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: None,
};
let result = backend.completion(completion_params).await.unwrap();
assert!(
result.is_some(),
"Completion should return results for docblock-typed $view->renderer->"
);
match result.unwrap() {
CompletionResponse::Array(items) => {
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(
labels.iter().any(|l| l.starts_with("render")),
"Should include 'render' from Renderer. Got: {:?}",
labels
);
}
CompletionResponse::List(_) => panic!("Expected Array response"),
}
}
#[tokio::test]
async fn test_var_property_chain_inherited_property() {
let backend = create_test_backend();
let uri = Url::parse("file:///inherited_prop.php").unwrap();
let text = concat!(
"<?php\n",
"class Connection {\n",
" public function query(string $sql): void {}\n",
" public function close(): void {}\n",
"}\n",
"class BaseRepository {\n",
" public Connection $conn;\n",
"}\n",
"class UserRepository extends BaseRepository {\n",
"}\n",
"function demo() {\n",
" $repo = new UserRepository();\n",
" $repo->conn->\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 completion_params = CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri },
position: Position {
line: 12,
character: 18,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: None,
};
let result = backend.completion(completion_params).await.unwrap();
assert!(
result.is_some(),
"Completion should return results for $repo->conn-> (inherited property)"
);
match result.unwrap() {
CompletionResponse::Array(items) => {
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(
labels.iter().any(|l| l.starts_with("query")),
"Should include 'query' from Connection. Got: {:?}",
labels
);
assert!(
labels.iter().any(|l| l.starts_with("close")),
"Should include 'close' from Connection. Got: {:?}",
labels
);
}
CompletionResponse::List(_) => panic!("Expected Array response"),
}
}
#[tokio::test]
async fn test_var_property_chain_cross_file_psr4() {
let composer = r#"{"autoload":{"psr-4":{"App\\":"src/"}}}"#;
let address_php = concat!(
"<?php\n",
"namespace App;\n",
"\n",
"class Address {\n",
" public string $city;\n",
" public string $country;\n",
" public function fullAddress(): string {}\n",
"}\n",
);
let user_php = concat!(
"<?php\n",
"namespace App;\n",
"\n",
"class User {\n",
" public Address $address;\n",
" public string $name;\n",
"}\n",
);
let controller_php = concat!(
"<?php\n",
"namespace App;\n",
"\n",
"class Controller {\n",
" public function show(User $user) {\n",
" $user->address->\n",
" }\n",
"}\n",
);
let (backend, _dir) = create_psr4_workspace(
composer,
&[
("src/Address.php", address_php),
("src/User.php", user_php),
("src/Controller.php", controller_php),
],
);
let uri = Url::parse("file:///src/Controller.php").unwrap();
let open_params = DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: uri.clone(),
language_id: "php".to_string(),
version: 1,
text: controller_php.to_string(),
},
};
backend.did_open(open_params).await;
let completion_params = CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri },
position: Position {
line: 5,
character: 24,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: None,
};
let result = backend.completion(completion_params).await.unwrap();
assert!(
result.is_some(),
"Completion should return results for PSR-4 $user->address->"
);
match result.unwrap() {
CompletionResponse::Array(items) => {
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(
labels.iter().any(|l| l.starts_with("city")),
"Should include 'city' from cross-file Address. Got: {:?}",
labels
);
assert!(
labels.iter().any(|l| l.starts_with("country")),
"Should include 'country' from cross-file Address. Got: {:?}",
labels
);
assert!(
labels.iter().any(|l| l.starts_with("fullAddress")),
"Should include 'fullAddress' from cross-file Address. Got: {:?}",
labels
);
}
CompletionResponse::List(_) => panic!("Expected Array response"),
}
}
#[tokio::test]
async fn test_var_property_chain_top_level() {
let backend = create_test_backend();
let uri = Url::parse("file:///top_level_chain.php").unwrap();
let text = concat!(
"<?php\n",
"class Point {\n",
" public float $x;\n",
" public float $y;\n",
"}\n",
"class Shape {\n",
" public Point $origin;\n",
"}\n",
"$shape = new Shape();\n",
"$shape->origin->\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 completion_params = CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri },
position: Position {
line: 9,
character: 19,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: None,
};
let result = backend.completion(completion_params).await.unwrap();
assert!(
result.is_some(),
"Completion should return results for top-level $shape->origin->"
);
match result.unwrap() {
CompletionResponse::Array(items) => {
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(
labels.iter().any(|l| l.starts_with("x")),
"Should include 'x' from Point. Got: {:?}",
labels
);
assert!(
labels.iter().any(|l| l.starts_with("y")),
"Should include 'y' from Point. Got: {:?}",
labels
);
}
CompletionResponse::List(_) => panic!("Expected Array response"),
}
}
#[tokio::test]
async fn test_var_property_chain_from_method_return() {
let backend = create_test_backend();
let uri = Url::parse("file:///return_chain.php").unwrap();
let text = concat!(
"<?php\n",
"class Stats {\n",
" public int $count;\n",
" public float $average;\n",
"}\n",
"class Report {\n",
" public Stats $stats;\n",
"}\n",
"class ReportFactory {\n",
" public function create(): Report {}\n",
"}\n",
"function demo() {\n",
" $factory = new ReportFactory();\n",
" $report = $factory->create();\n",
" $report->stats->\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 completion_params = CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri },
position: Position {
line: 14,
character: 21,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: None,
};
let result = backend.completion(completion_params).await.unwrap();
assert!(
result.is_some(),
"Completion should return results for $report->stats->"
);
match result.unwrap() {
CompletionResponse::Array(items) => {
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(
labels.iter().any(|l| l.starts_with("count")),
"Should include 'count' from Stats. Got: {:?}",
labels
);
assert!(
labels.iter().any(|l| l.starts_with("average")),
"Should include 'average' from Stats. Got: {:?}",
labels
);
}
CompletionResponse::List(_) => panic!("Expected Array response"),
}
}
#[tokio::test]
async fn test_var_property_chain_nonexistent_property() {
let backend = create_test_backend();
let uri = Url::parse("file:///no_prop.php").unwrap();
let text = concat!(
"<?php\n",
"class User {\n",
" public string $name;\n",
"}\n",
"function demo() {\n",
" $user = new User();\n",
" $user->nonexistent->\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 completion_params = CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri },
position: Position {
line: 6,
character: 26,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: None,
};
let result = backend.completion(completion_params).await.unwrap();
assert!(
result.is_none(),
"Should return None for nonexistent property chain, got: {:?}",
result
);
}
#[tokio::test]
async fn test_var_property_chain_foreach_value() {
let backend = create_test_backend();
let uri = Url::parse("file:///foreach_chain.php").unwrap();
let text = concat!(
"<?php\n",
"class Address {\n",
" public string $city;\n",
"}\n",
"class User {\n",
" public Address $address;\n",
"}\n",
"class Service {\n",
" public function process() {\n",
" /** @var list<User> $users */\n",
" $users = loadUsers();\n",
" foreach ($users as $user) {\n",
" $user->address->\n",
" }\n",
" }\n",
"}\n",
);
let open_params = DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: uri.clone(),
language_id: "php".to_string(),
version: 1,
text: text.to_string(),
},
};
backend.did_open(open_params).await;
let completion_params = CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri },
position: Position {
line: 12,
character: 30,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: None,
};
let result = backend.completion(completion_params).await.unwrap();
assert!(
result.is_some(),
"Completion should return results for foreach $user->address->"
);
match result.unwrap() {
CompletionResponse::Array(items) => {
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(
labels.iter().any(|l| l.starts_with("city")),
"Should include 'city' from Address via foreach chain. Got: {:?}",
labels
);
}
CompletionResponse::List(_) => panic!("Expected Array response"),
}
}
#[tokio::test]
async fn test_var_property_chain_promoted_property() {
let backend = create_test_backend();
let uri = Url::parse("file:///promoted_chain.php").unwrap();
let text = concat!(
"<?php\n",
"class Formatter {\n",
" public function format(string $val): string {}\n",
"}\n",
"class Printer {\n",
" public function __construct(\n",
" public Formatter $formatter,\n",
" ) {}\n",
"}\n",
"function demo() {\n",
" $printer = new Printer(new Formatter());\n",
" $printer->formatter->\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 completion_params = CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri },
position: Position {
line: 11,
character: 26,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: None,
};
let result = backend.completion(completion_params).await.unwrap();
assert!(
result.is_some(),
"Completion should return results for $printer->formatter->"
);
match result.unwrap() {
CompletionResponse::Array(items) => {
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(
labels.iter().any(|l| l.starts_with("format")),
"Should include 'format' from Formatter. Got: {:?}",
labels
);
}
CompletionResponse::List(_) => panic!("Expected Array response"),
}
}
#[tokio::test]
async fn test_var_property_chain_no_static_members() {
let backend = create_test_backend();
let uri = Url::parse("file:///no_static.php").unwrap();
let text = concat!(
"<?php\n",
"class Cache {\n",
" public function get(string $key): mixed {}\n",
" public static function flush(): void {}\n",
" public static int $hits = 0;\n",
"}\n",
"class App {\n",
" public Cache $cache;\n",
"}\n",
"function demo() {\n",
" $app = new App();\n",
" $app->cache->\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 completion_params = CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri },
position: Position {
line: 11,
character: 18,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: None,
};
let result = backend.completion(completion_params).await.unwrap();
assert!(
result.is_some(),
"Completion should return results for $app->cache->"
);
match result.unwrap() {
CompletionResponse::Array(items) => {
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(
labels.iter().any(|l| l.starts_with("get")),
"Should include instance method 'get' from Cache. Got: {:?}",
labels
);
assert!(
!labels.iter().any(|l| l.starts_with("flush")),
"Should NOT include static method 'flush' via ->. Got: {:?}",
labels
);
}
CompletionResponse::List(_) => panic!("Expected Array response"),
}
}
#[tokio::test]
async fn test_this_property_chain_still_works() {
let backend = create_test_backend();
let uri = Url::parse("file:///this_regression.php").unwrap();
let text = concat!(
"<?php\n",
"class Logger {\n",
" public function info(string $msg): void {}\n",
"}\n",
"class Service {\n",
" public Logger $logger;\n",
" public function run() {\n",
" $this->logger->\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 completion_params = CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri },
position: Position {
line: 7,
character: 23,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: None,
};
let result = backend.completion(completion_params).await.unwrap();
assert!(
result.is_some(),
"Completion should still work for $this->logger->"
);
match result.unwrap() {
CompletionResponse::Array(items) => {
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(
labels.iter().any(|l| l.starts_with("info")),
"Should include 'info' from Logger via $this->. Got: {:?}",
labels
);
}
CompletionResponse::List(_) => panic!("Expected Array response"),
}
}