use crate::common::{create_psr4_workspace, create_test_backend};
use tower_lsp::LanguageServer;
use tower_lsp::lsp_types::*;
#[tokio::test]
async fn test_completion_this_inside_anonymous_class() {
let backend = create_test_backend();
let uri = Url::parse("file:///anon_class_basic.php").unwrap();
let text = concat!(
"<?php\n",
"$handler = new class {\n",
" public string $name;\n",
" public function greet(): string { return ''; }\n",
" public function test() {\n",
" $this->\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 result = backend
.completion(CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri },
position: Position {
line: 5,
character: 15,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: None,
})
.await
.unwrap();
assert!(result.is_some(), "Should return completion results");
match result.unwrap() {
CompletionResponse::Array(items) => {
let method_names: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::METHOD))
.map(|i| i.filter_text.as_deref().unwrap())
.collect();
let prop_names: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::PROPERTY))
.map(|i| i.filter_text.as_deref().unwrap())
.collect();
assert!(
method_names.contains(&"greet"),
"Should include anonymous class method 'greet', got: {:?}",
method_names
);
assert!(
method_names.contains(&"test"),
"Should include anonymous class method 'test', got: {:?}",
method_names
);
assert!(
prop_names.contains(&"name"),
"Should include anonymous class property 'name', got: {:?}",
prop_names
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_completion_anonymous_class_extends_parent() {
let backend = create_test_backend();
let uri = Url::parse("file:///anon_extends.php").unwrap();
let text = concat!(
"<?php\n",
"class BaseHandler {\n",
" public function baseMethod(): string { return ''; }\n",
"}\n",
"$handler = new class extends BaseHandler {\n",
" public function handle(): void {}\n",
" public function test() {\n",
" $this->\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 result = backend
.completion(CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri },
position: Position {
line: 7,
character: 15,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: None,
})
.await
.unwrap();
assert!(result.is_some(), "Should return completion results");
match result.unwrap() {
CompletionResponse::Array(items) => {
let method_names: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::METHOD))
.map(|i| i.filter_text.as_deref().unwrap())
.collect();
assert!(
method_names.contains(&"handle"),
"Should include own method 'handle', got: {:?}",
method_names
);
assert!(
method_names.contains(&"baseMethod"),
"Should include inherited method 'baseMethod', got: {:?}",
method_names
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_completion_anonymous_class_implements_interface() {
let backend = create_test_backend();
let uri = Url::parse("file:///anon_implements.php").unwrap();
let text = concat!(
"<?php\n",
"interface Renderable {\n",
" public function render(): string;\n",
"}\n",
"$widget = new class implements Renderable {\n",
" public function render(): string { return ''; }\n",
" public function extra(): void {}\n",
" public function test() {\n",
" $this->\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 result = backend
.completion(CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri },
position: Position {
line: 8,
character: 15,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: None,
})
.await
.unwrap();
assert!(result.is_some(), "Should return completion results");
match result.unwrap() {
CompletionResponse::Array(items) => {
let method_names: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::METHOD))
.map(|i| i.filter_text.as_deref().unwrap())
.collect();
assert!(
method_names.contains(&"render"),
"Should include 'render' method, got: {:?}",
method_names
);
assert!(
method_names.contains(&"extra"),
"Should include 'extra' method, got: {:?}",
method_names
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_completion_anonymous_class_uses_trait() {
let backend = create_test_backend();
let uri = Url::parse("file:///anon_trait.php").unwrap();
let text = concat!(
"<?php\n",
"trait Loggable {\n",
" public function log(): void {}\n",
"}\n",
"$worker = new class {\n",
" use Loggable;\n",
" public function run(): void {}\n",
" public function test() {\n",
" $this->\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 result = backend
.completion(CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri },
position: Position {
line: 8,
character: 15,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: None,
})
.await
.unwrap();
assert!(result.is_some(), "Should return completion results");
match result.unwrap() {
CompletionResponse::Array(items) => {
let method_names: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::METHOD))
.map(|i| i.filter_text.as_deref().unwrap())
.collect();
assert!(
method_names.contains(&"log"),
"Should include trait method 'log', got: {:?}",
method_names
);
assert!(
method_names.contains(&"run"),
"Should include own method 'run', got: {:?}",
method_names
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_completion_anonymous_class_inside_named_class_method() {
let backend = create_test_backend();
let uri = Url::parse("file:///anon_nested.php").unwrap();
let text = concat!(
"<?php\n",
"class Outer {\n",
" public function outerMethod(): void {}\n",
" public function factory() {\n",
" return new class {\n",
" public function innerMethod(): void {}\n",
" public function test() {\n",
" $this->\n",
" }\n",
" };\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 result = backend
.completion(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,
})
.await
.unwrap();
assert!(result.is_some(), "Should return completion results");
match result.unwrap() {
CompletionResponse::Array(items) => {
let method_names: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::METHOD))
.map(|i| i.filter_text.as_deref().unwrap())
.collect();
assert!(
method_names.contains(&"innerMethod"),
"Should include anonymous class's own 'innerMethod', got: {:?}",
method_names
);
assert!(
!method_names.contains(&"outerMethod"),
"Should NOT include outer class's 'outerMethod', got: {:?}",
method_names
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_completion_anonymous_class_extends_cross_file() {
let composer = r#"{ "autoload": { "psr-4": { "App\\": "src/" } } }"#;
let base_file = concat!(
"<?php\n",
"namespace App;\n",
"class BaseHandler {\n",
" public function baseMethod(): string { return ''; }\n",
" protected function helperMethod(): void {}\n",
"}\n",
);
let main_file = concat!(
"<?php\n",
"namespace App;\n",
"$handler = new class extends BaseHandler {\n",
" public function handle(): void {}\n",
" public function test() {\n",
" $this->\n",
" }\n",
"};\n",
);
let (backend, dir) = create_psr4_workspace(
composer,
&[
("src/BaseHandler.php", base_file),
("src/main.php", main_file),
],
);
let uri = Url::from_file_path(dir.path().join("src/main.php")).unwrap();
backend
.did_open(DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: uri.clone(),
language_id: "php".to_string(),
version: 1,
text: main_file.to_string(),
},
})
.await;
let result = backend
.completion(CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri },
position: Position {
line: 5,
character: 15,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: None,
})
.await
.unwrap();
assert!(result.is_some(), "Should return completion results");
match result.unwrap() {
CompletionResponse::Array(items) => {
let method_names: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::METHOD))
.map(|i| i.filter_text.as_deref().unwrap())
.collect();
assert!(
method_names.contains(&"handle"),
"Should include own method 'handle', got: {:?}",
method_names
);
assert!(
method_names.contains(&"baseMethod"),
"Should include inherited 'baseMethod', got: {:?}",
method_names
);
assert!(
method_names.contains(&"helperMethod"),
"Should include inherited protected 'helperMethod', got: {:?}",
method_names
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_completion_anonymous_class_in_if_block() {
let backend = create_test_backend();
let uri = Url::parse("file:///anon_in_if.php").unwrap();
let text = concat!(
"<?php\n",
"if (true) {\n",
" $handler = new class {\n",
" public function ifMethod(): void {}\n",
" public function test() {\n",
" $this->\n",
" }\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 result = backend
.completion(CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri },
position: Position {
line: 5,
character: 19,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: None,
})
.await
.unwrap();
assert!(result.is_some(), "Should return completion results");
match result.unwrap() {
CompletionResponse::Array(items) => {
let method_names: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::METHOD))
.map(|i| i.filter_text.as_deref().unwrap())
.collect();
assert!(
method_names.contains(&"ifMethod"),
"Should include 'ifMethod' from anonymous class inside if block, got: {:?}",
method_names
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_completion_anonymous_class_in_try_catch() {
let backend = create_test_backend();
let uri = Url::parse("file:///anon_in_try.php").unwrap();
let text = concat!(
"<?php\n",
"try {\n",
" $handler = new class {\n",
" public function tryMethod(): void {}\n",
" public function test() {\n",
" $this->\n",
" }\n",
" };\n",
"} catch (Exception $e) {}\n",
);
backend
.did_open(DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: uri.clone(),
language_id: "php".to_string(),
version: 1,
text: text.to_string(),
},
})
.await;
let result = backend
.completion(CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri },
position: Position {
line: 5,
character: 19,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: None,
})
.await
.unwrap();
assert!(result.is_some(), "Should return completion results");
match result.unwrap() {
CompletionResponse::Array(items) => {
let method_names: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::METHOD))
.map(|i| i.filter_text.as_deref().unwrap())
.collect();
assert!(
method_names.contains(&"tryMethod"),
"Should include 'tryMethod' from anonymous class inside try block, got: {:?}",
method_names
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_completion_anonymous_class_in_foreach() {
let backend = create_test_backend();
let uri = Url::parse("file:///anon_in_foreach.php").unwrap();
let text = concat!(
"<?php\n",
"foreach ([1, 2, 3] as $item) {\n",
" $handler = new class {\n",
" public function loopMethod(): void {}\n",
" public function test() {\n",
" $this->\n",
" }\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 result = backend
.completion(CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri },
position: Position {
line: 5,
character: 19,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: None,
})
.await
.unwrap();
assert!(result.is_some(), "Should return completion results");
match result.unwrap() {
CompletionResponse::Array(items) => {
let method_names: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::METHOD))
.map(|i| i.filter_text.as_deref().unwrap())
.collect();
assert!(
method_names.contains(&"loopMethod"),
"Should include 'loopMethod' from anonymous class inside foreach, got: {:?}",
method_names
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_completion_anonymous_class_as_function_argument() {
let backend = create_test_backend();
let uri = Url::parse("file:///anon_func_arg.php").unwrap();
let text = concat!(
"<?php\n",
"function process($handler) {}\n",
"process(new class {\n",
" public function argMethod(): void {}\n",
" public function test() {\n",
" $this->\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 result = backend
.completion(CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri },
position: Position {
line: 5,
character: 15,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: None,
})
.await
.unwrap();
assert!(result.is_some(), "Should return completion results");
match result.unwrap() {
CompletionResponse::Array(items) => {
let method_names: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::METHOD))
.map(|i| i.filter_text.as_deref().unwrap())
.collect();
assert!(
method_names.contains(&"argMethod"),
"Should include 'argMethod' from anonymous class passed as function arg, got: {:?}",
method_names
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_completion_anonymous_class_in_return_statement() {
let backend = create_test_backend();
let uri = Url::parse("file:///anon_return.php").unwrap();
let text = concat!(
"<?php\n",
"function factory() {\n",
" return new class {\n",
" public function returnedMethod(): void {}\n",
" public function test() {\n",
" $this->\n",
" }\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 result = backend
.completion(CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri },
position: Position {
line: 5,
character: 19,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: None,
})
.await
.unwrap();
assert!(result.is_some(), "Should return completion results");
match result.unwrap() {
CompletionResponse::Array(items) => {
let method_names: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::METHOD))
.map(|i| i.filter_text.as_deref().unwrap())
.collect();
assert!(
method_names.contains(&"returnedMethod"),
"Should include 'returnedMethod' from anonymous class in return, got: {:?}",
method_names
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_completion_anonymous_class_in_closure() {
let backend = create_test_backend();
let uri = Url::parse("file:///anon_in_closure.php").unwrap();
let text = concat!(
"<?php\n",
"$fn = function() {\n",
" return new class {\n",
" public function closureMethod(): void {}\n",
" public function test() {\n",
" $this->\n",
" }\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 result = backend
.completion(CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri },
position: Position {
line: 5,
character: 19,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: None,
})
.await
.unwrap();
assert!(result.is_some(), "Should return completion results");
match result.unwrap() {
CompletionResponse::Array(items) => {
let method_names: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::METHOD))
.map(|i| i.filter_text.as_deref().unwrap())
.collect();
assert!(
method_names.contains(&"closureMethod"),
"Should include 'closureMethod' from anonymous class in closure, got: {:?}",
method_names
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_completion_anonymous_class_promoted_properties() {
let backend = create_test_backend();
let uri = Url::parse("file:///anon_promoted.php").unwrap();
let text = concat!(
"<?php\n",
"$obj = new class('hello') {\n",
" public function __construct(\n",
" public string $message,\n",
" private int $count = 0,\n",
" ) {}\n",
" public function test() {\n",
" $this->\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 result = backend
.completion(CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri },
position: Position {
line: 7,
character: 15,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: None,
})
.await
.unwrap();
assert!(result.is_some(), "Should return completion results");
match result.unwrap() {
CompletionResponse::Array(items) => {
let prop_names: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::PROPERTY))
.map(|i| i.filter_text.as_deref().unwrap())
.collect();
assert!(
prop_names.contains(&"message"),
"Should include promoted property 'message', got: {:?}",
prop_names
);
assert!(
prop_names.contains(&"count"),
"Should include promoted property 'count', got: {:?}",
prop_names
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_completion_anonymous_class_in_switch() {
let backend = create_test_backend();
let uri = Url::parse("file:///anon_in_switch.php").unwrap();
let text = concat!(
"<?php\n",
"switch (true) {\n",
" case true:\n",
" $handler = new class {\n",
" public function switchMethod(): void {}\n",
" public function test() {\n",
" $this->\n",
" }\n",
" };\n",
" break;\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 result = backend
.completion(CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri },
position: Position {
line: 6,
character: 23,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: None,
})
.await
.unwrap();
assert!(result.is_some(), "Should return completion results");
match result.unwrap() {
CompletionResponse::Array(items) => {
let method_names: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::METHOD))
.map(|i| i.filter_text.as_deref().unwrap())
.collect();
assert!(
method_names.contains(&"switchMethod"),
"Should include 'switchMethod' from anonymous class inside switch, got: {:?}",
method_names
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_completion_outer_class_not_affected_by_anonymous_class() {
let backend = create_test_backend();
let uri = Url::parse("file:///anon_outer_ok.php").unwrap();
let text = concat!(
"<?php\n",
"class Outer {\n",
" public function outerOnly(): void {}\n",
" public function factory() {\n",
" return new class {\n",
" public function innerOnly(): void {}\n",
" };\n",
" }\n",
" public function test() {\n",
" $this->\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 result = backend
.completion(CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri },
position: Position {
line: 9,
character: 15,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: None,
})
.await
.unwrap();
assert!(result.is_some(), "Should return completion results");
match result.unwrap() {
CompletionResponse::Array(items) => {
let method_names: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::METHOD))
.map(|i| i.filter_text.as_deref().unwrap())
.collect();
assert!(
method_names.contains(&"outerOnly"),
"Outer class should still see its own 'outerOnly', got: {:?}",
method_names
);
assert!(
method_names.contains(&"factory"),
"Outer class should still see its own 'factory', got: {:?}",
method_names
);
assert!(
!method_names.contains(&"innerOnly"),
"Outer class should NOT see anonymous class's 'innerOnly', got: {:?}",
method_names
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_completion_anonymous_class_in_namespace() {
let composer = r#"{ "autoload": { "psr-4": { "App\\": "src/" } } }"#;
let file = concat!(
"<?php\n",
"namespace App;\n",
"$handler = new class {\n",
" public function nsMethod(): void {}\n",
" public function test() {\n",
" $this->\n",
" }\n",
"};\n",
);
let (backend, dir) = create_psr4_workspace(composer, &[("src/handler.php", file)]);
let uri = Url::from_file_path(dir.path().join("src/handler.php")).unwrap();
backend
.did_open(DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: uri.clone(),
language_id: "php".to_string(),
version: 1,
text: file.to_string(),
},
})
.await;
let result = backend
.completion(CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri },
position: Position {
line: 5,
character: 15,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: None,
})
.await
.unwrap();
assert!(result.is_some(), "Should return completion results");
match result.unwrap() {
CompletionResponse::Array(items) => {
let method_names: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::METHOD))
.map(|i| i.filter_text.as_deref().unwrap())
.collect();
assert!(
method_names.contains(&"nsMethod"),
"Should include 'nsMethod' from anonymous class in namespace, got: {:?}",
method_names
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_completion_multiple_anonymous_classes_in_same_file() {
let backend = create_test_backend();
let uri = Url::parse("file:///anon_multiple.php").unwrap();
let text = concat!(
"<?php\n",
"$first = new class {\n",
" public function firstMethod(): void {}\n",
"};\n",
"$second = new class {\n",
" public function secondMethod(): void {}\n",
" public function test() {\n",
" $this->\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 result = backend
.completion(CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri },
position: Position {
line: 7,
character: 15,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: None,
})
.await
.unwrap();
assert!(result.is_some(), "Should return completion results");
match result.unwrap() {
CompletionResponse::Array(items) => {
let method_names: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::METHOD))
.map(|i| i.filter_text.as_deref().unwrap())
.collect();
assert!(
method_names.contains(&"secondMethod"),
"Should include second anonymous class's 'secondMethod', got: {:?}",
method_names
);
assert!(
!method_names.contains(&"firstMethod"),
"Should NOT include first anonymous class's 'firstMethod', got: {:?}",
method_names
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_completion_anonymous_class_property_docblock_chain() {
let backend = create_test_backend();
let uri = Url::parse("file:///anon_prop_chain.php").unwrap();
let text = concat!(
"<?php\n",
"class Logger {\n",
" public function info(string $msg): void {}\n",
"}\n",
"$handler = new class {\n",
" /** @var Logger */\n",
" public $logger;\n",
" public function test() {\n",
" $this->logger->\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 result = backend
.completion(CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri },
position: Position {
line: 8,
character: 23,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: None,
})
.await
.unwrap();
assert!(result.is_some(), "Should return completion results");
match result.unwrap() {
CompletionResponse::Array(items) => {
let method_names: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::METHOD))
.map(|i| i.filter_text.as_deref().unwrap())
.collect();
assert!(
method_names.contains(&"info"),
"Should resolve @var type on anonymous class property and show Logger::info, got: {:?}",
method_names
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_goto_definition_anonymous_class_method() {
let backend = create_test_backend();
let uri = Url::parse("file:///anon_goto.php").unwrap();
let text = concat!(
"<?php\n",
"$handler = new class {\n",
" public function handle(): void {}\n",
" public function test() {\n",
" $this->handle();\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 result = backend
.goto_definition(GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 4,
character: 18,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
})
.await
.unwrap();
assert!(
result.is_some(),
"Should resolve go-to-definition for method in anonymous class"
);
if let Some(GotoDefinitionResponse::Scalar(location)) = result {
assert_eq!(location.uri, uri, "Should point to the same file");
assert_eq!(
location.range.start.line, 2,
"Should jump to the handle method definition on line 2"
);
} else {
panic!("Expected GotoDefinitionResponse::Scalar");
}
}
#[tokio::test]
async fn test_completion_anonymous_class_detail_shows_friendly_name() {
let backend = create_test_backend();
let uri = Url::parse("file:///anon_detail.php").unwrap();
let text = concat!(
"<?php\n",
"$obj = new class {\n",
" public string $name;\n",
" public function greet(): void {}\n",
" public function test() {\n",
" $this->\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 result = backend
.completion(CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri },
position: Position {
line: 5,
character: 15,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: None,
})
.await
.unwrap();
assert!(result.is_some(), "Should return completion results");
match result.unwrap() {
CompletionResponse::Array(items) => {
let method_item = items
.iter()
.find(|i| {
i.kind == Some(CompletionItemKind::METHOD)
&& i.filter_text.as_deref() == Some("greet")
})
.expect("Should find 'greet' method");
assert!(
!method_item
.detail
.as_deref()
.unwrap_or("")
.contains("__anonymous@"),
"Method detail should NOT contain raw synthetic name, got: {:?}",
method_item.detail
);
assert!(
!method_item
.label_details
.as_ref()
.and_then(|ld| ld.description.as_deref())
.unwrap_or("")
.contains("__anonymous@"),
"Method label_details should NOT contain raw synthetic name, got: {:?}",
method_item.label_details
);
let prop_item = items
.iter()
.find(|i| {
i.kind == Some(CompletionItemKind::PROPERTY)
&& i.filter_text.as_deref() == Some("name")
})
.expect("Should find 'name' property");
assert_eq!(
prop_item.detail.as_deref(),
Some("string"),
"Typed property detail should show the type hint"
);
assert!(
!prop_item
.detail
.as_deref()
.unwrap_or("")
.contains("__anonymous@"),
"Property detail should NOT contain raw synthetic name, got: {:?}",
prop_item.detail
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_closure_param_type_hint_in_anonymous_class_static_call() {
let backend = create_test_backend();
let uri = Url::parse("file:///anon_closure_param.php").unwrap();
let text = concat!(
"<?php\n",
"class Blueprint {\n",
" public function enum(string $column): static { return $this; }\n",
" public function after(string $column): static { return $this; }\n",
" public function default(mixed $value): static { return $this; }\n",
" public function dropColumn(string $column): void {}\n",
"}\n",
"class Migration {\n",
" public function up(): void {}\n",
" public function down(): void {}\n",
"}\n",
"class Schema {\n",
" public static function table(string $table, \\Closure $callback): void {}\n",
"}\n",
"return new class extends Migration {\n",
" public function up(): void {\n",
" Schema::table('orders', function (Blueprint $table): void {\n",
" $table->\n",
" });\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 result = backend
.completion(CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri },
position: Position {
line: 17,
character: 20,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: None,
})
.await
.unwrap();
assert!(
result.is_some(),
"Completion should resolve $table via closure parameter type hint in anonymous class"
);
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("enum")),
"Should include enum method from Blueprint, got: {:?}",
labels
);
assert!(
labels.iter().any(|l| l.starts_with("dropColumn")),
"Should include dropColumn method from Blueprint, got: {:?}",
labels
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}