use crate::common::{
create_psr4_workspace, create_test_backend, create_test_backend_with_full_stubs,
};
use tower_lsp::LanguageServer;
use tower_lsp::lsp_types::*;
#[tokio::test]
async fn test_completion_inherits_public_and_protected_methods_same_file() {
let backend = create_test_backend();
let uri = Url::parse("file:///inherit.php").unwrap();
let text = concat!(
"<?php\n",
"class Animal {\n",
" public function breathe(): void {}\n",
" protected function sleep(): void {}\n",
" private function digest(): void {}\n",
"}\n",
"class Dog extends Animal {\n",
" public function bark(): void {}\n",
" function test() {\n",
" $this->\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: 9,
character: 15,
},
},
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");
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(&"bark"),
"Should include own method 'bark'"
);
assert!(
method_names.contains(&"test"),
"Should include own method 'test'"
);
assert!(
method_names.contains(&"breathe"),
"Should include inherited public 'breathe'"
);
assert!(
method_names.contains(&"sleep"),
"Should include inherited protected 'sleep'"
);
assert!(
!method_names.contains(&"digest"),
"Should NOT include inherited private 'digest'"
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_completion_inherits_properties_same_file() {
let backend = create_test_backend();
let uri = Url::parse("file:///inherit_props.php").unwrap();
let text = concat!(
"<?php\n",
"class Base {\n",
" public string $publicProp;\n",
" protected int $protectedProp;\n",
" private float $privateProp;\n",
"}\n",
"class Child extends Base {\n",
" public string $ownProp;\n",
" function test() {\n",
" $this->\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: 9,
character: 15,
},
},
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());
match result.unwrap() {
CompletionResponse::Array(items) => {
let prop_names: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::PROPERTY))
.map(|i| i.label.as_str())
.collect();
assert!(
prop_names.contains(&"ownProp"),
"Should include own property"
);
assert!(
prop_names.contains(&"publicProp"),
"Should include inherited public property"
);
assert!(
prop_names.contains(&"protectedProp"),
"Should include inherited protected property"
);
assert!(
!prop_names.contains(&"privateProp"),
"Should NOT include inherited private property"
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_completion_inherits_constants_same_file() {
let backend = create_test_backend();
let uri = Url::parse("file:///inherit_const.php").unwrap();
let text = concat!(
"<?php\n",
"class Base {\n",
" public const PUB_CONST = 1;\n",
" protected const PROT_CONST = 2;\n",
" private const PRIV_CONST = 3;\n",
"}\n",
"class Child extends Base {\n",
" const OWN_CONST = 4;\n",
" function test() {\n",
" self::\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: 9,
character: 14,
},
},
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());
match result.unwrap() {
CompletionResponse::Array(items) => {
let const_names: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::CONSTANT))
.map(|i| i.label.as_str())
.collect();
assert!(
const_names.contains(&"OWN_CONST"),
"Should include own constant"
);
assert!(
const_names.contains(&"PUB_CONST"),
"Should include inherited public constant"
);
assert!(
const_names.contains(&"PROT_CONST"),
"Should include inherited protected constant"
);
assert!(
!const_names.contains(&"PRIV_CONST"),
"Should NOT include inherited private constant"
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_completion_child_method_overrides_parent() {
let backend = create_test_backend();
let uri = Url::parse("file:///override.php").unwrap();
let text = concat!(
"<?php\n",
"class Base {\n",
" public function greet(string $name): string { return ''; }\n",
" public function hello(): void {}\n",
"}\n",
"class Child extends Base {\n",
" public function greet(string $name, string $greeting): string { return ''; }\n",
" function test() {\n",
" $this->\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: 8,
character: 15,
},
},
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());
match result.unwrap() {
CompletionResponse::Array(items) => {
let method_items: Vec<&CompletionItem> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::METHOD))
.collect();
let greet_items: Vec<&&CompletionItem> = method_items
.iter()
.filter(|i| i.filter_text.as_deref() == Some("greet"))
.collect();
assert_eq!(
greet_items.len(),
1,
"Overridden method 'greet' should appear exactly once"
);
let greet_label = &greet_items[0].label;
assert!(
greet_label.contains("$greeting"),
"Should use child's signature with $greeting, got: {}",
greet_label
);
let hello_items: Vec<&&CompletionItem> = method_items
.iter()
.filter(|i| i.filter_text.as_deref() == Some("hello"))
.collect();
assert_eq!(hello_items.len(), 1, "Inherited 'hello' should appear once");
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_completion_multi_level_inheritance_same_file() {
let backend = create_test_backend();
let uri = Url::parse("file:///multi_level.php").unwrap();
let text = concat!(
"<?php\n",
"class Grandparent {\n",
" public function ancestorMethod(): void {}\n",
" private function gpPrivate(): void {}\n",
"}\n",
"class ParentClass extends Grandparent {\n",
" public function parentMethod(): void {}\n",
" protected function parentProtected(): void {}\n",
"}\n",
"class ChildClass extends ParentClass {\n",
" public function childMethod(): void {}\n",
" function test() {\n",
" $this->\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: 15,
},
},
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());
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(&"childMethod"),
"Should include own 'childMethod'"
);
assert!(method_names.contains(&"test"), "Should include own 'test'");
assert!(
method_names.contains(&"parentMethod"),
"Should include parent's 'parentMethod'"
);
assert!(
method_names.contains(&"parentProtected"),
"Should include parent's protected 'parentProtected'"
);
assert!(
method_names.contains(&"ancestorMethod"),
"Should include grandparent's 'ancestorMethod'"
);
assert!(
!method_names.contains(&"gpPrivate"),
"Should NOT include grandparent's private 'gpPrivate'"
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_completion_inherits_static_members() {
let backend = create_test_backend();
let uri = Url::parse("file:///inherit_static.php").unwrap();
let text = concat!(
"<?php\n",
"class Base {\n",
" public static function staticBase(): void {}\n",
" protected static string $staticProp = '';\n",
" private static function privateStatic(): void {}\n",
"}\n",
"class Child extends Base {\n",
" public static function staticChild(): void {}\n",
" function test() {\n",
" self::\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: 9,
character: 14,
},
},
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());
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.label.as_str())
.collect();
assert!(
method_names.contains(&"staticChild"),
"Should include own static method"
);
assert!(
method_names.contains(&"staticBase"),
"Should include inherited public static method"
);
assert!(
!method_names.contains(&"privateStatic"),
"Should NOT include inherited private static method"
);
assert!(
prop_names.contains(&"$staticProp"),
"Should include inherited protected static property"
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_completion_cross_file_inheritance_psr4() {
let (backend, _dir) = create_psr4_workspace(
r#"{
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
}"#,
&[(
"src/BaseModel.php",
concat!(
"<?php\n",
"namespace App;\n",
"class BaseModel {\n",
" public function save(): bool { return true; }\n",
" protected function validate(): bool { return true; }\n",
" private function internalLog(): void {}\n",
" public string $id;\n",
"}\n",
),
)],
);
let uri = Url::parse("file:///app.php").unwrap();
let text = concat!(
"<?php\n",
"use App\\BaseModel;\n",
"class User extends BaseModel {\n",
" public string $name;\n",
" public function getName(): string { return $this->name; }\n",
" function test() {\n",
" $this->\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: 6,
character: 15,
},
},
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 cross-file inheritance"
);
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.label.as_str())
.collect();
assert!(
method_names.contains(&"getName"),
"Should include own 'getName'"
);
assert!(
prop_names.contains(&"name"),
"Should include own property 'name'"
);
assert!(
method_names.contains(&"save"),
"Should include inherited 'save' from BaseModel"
);
assert!(
prop_names.contains(&"id"),
"Should include inherited property 'id' from BaseModel"
);
assert!(
method_names.contains(&"validate"),
"Should include inherited protected 'validate'"
);
assert!(
!method_names.contains(&"internalLog"),
"Should NOT include inherited private 'internalLog'"
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_completion_cross_file_multi_level_inheritance_psr4() {
let (backend, _dir) = create_psr4_workspace(
r#"{
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
}"#,
&[
(
"src/Base.php",
concat!(
"<?php\n",
"namespace App;\n",
"class Base {\n",
" public function baseMethod(): void {}\n",
" protected function baseProtected(): void {}\n",
" private function basePrivate(): void {}\n",
"}\n",
),
),
(
"src/Middle.php",
concat!(
"<?php\n",
"namespace App;\n",
"class Middle extends Base {\n",
" public function middleMethod(): void {}\n",
"}\n",
),
),
],
);
let uri = Url::parse("file:///app.php").unwrap();
let text = concat!(
"<?php\n",
"use App\\Middle;\n",
"class Leaf extends Middle {\n",
" public function leafMethod(): void {}\n",
" function test() {\n",
" $this->\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: 5,
character: 15,
},
},
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 multi-level cross-file inheritance"
);
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(&"leafMethod"),
"Should include own 'leafMethod'"
);
assert!(
method_names.contains(&"middleMethod"),
"Should include parent's 'middleMethod'"
);
assert!(
method_names.contains(&"baseMethod"),
"Should include grandparent's 'baseMethod'"
);
assert!(
method_names.contains(&"baseProtected"),
"Should include grandparent's protected 'baseProtected'"
);
assert!(
!method_names.contains(&"basePrivate"),
"Should NOT include grandparent's private 'basePrivate'"
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_completion_override_across_files() {
let (backend, _dir) = create_psr4_workspace(
r#"{
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
}"#,
&[(
"src/ParentClass.php",
concat!(
"<?php\n",
"namespace App;\n",
"class ParentClass {\n",
" public function render(): string { return ''; }\n",
" public function prepare(): void {}\n",
"}\n",
),
)],
);
let uri = Url::parse("file:///app.php").unwrap();
let text = concat!(
"<?php\n",
"use App\\ParentClass;\n",
"class ChildView extends ParentClass {\n",
" public function render(string $template): string { return ''; }\n",
" function test() {\n",
" $this->\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: 5,
character: 15,
},
},
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());
match result.unwrap() {
CompletionResponse::Array(items) => {
let method_items: Vec<&CompletionItem> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::METHOD))
.collect();
let render_items: Vec<&&CompletionItem> = method_items
.iter()
.filter(|i| i.filter_text.as_deref() == Some("render"))
.collect();
assert_eq!(render_items.len(), 1, "'render' should appear exactly once");
assert!(
render_items[0].label.contains("$template"),
"Should use child's signature with $template, got: {}",
render_items[0].label
);
let prepare_items: Vec<&&CompletionItem> = method_items
.iter()
.filter(|i| i.filter_text.as_deref() == Some("prepare"))
.collect();
assert_eq!(
prepare_items.len(),
1,
"'prepare' should be inherited from parent"
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_completion_no_parent_class_unchanged_behavior() {
let backend = create_test_backend();
let uri = Url::parse("file:///standalone.php").unwrap();
let text = concat!(
"<?php\n",
"class Standalone {\n",
" public function doStuff(): void {}\n",
" private function internal(): void {}\n",
" function test() {\n",
" $this->\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: 5,
character: 15,
},
},
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());
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(&"doStuff"),
"Should include 'doStuff'"
);
assert!(
method_names.contains(&"internal"),
"Own private methods should still appear"
);
assert!(method_names.contains(&"test"), "Should include 'test'");
assert_eq!(method_names.len(), 3, "Should have exactly 3 methods");
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_completion_variable_of_child_type_includes_inherited() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_inherit.php").unwrap();
let text = concat!(
"<?php\n",
"class Vehicle {\n",
" public function start(): void {}\n",
" protected function fuelCheck(): bool { return true; }\n",
" private function internalDiag(): void {}\n",
"}\n",
"class Car extends Vehicle {\n",
" public function openTrunk(): void {}\n",
"}\n",
"class Garage {\n",
" function test() {\n",
" $car = new Car();\n",
" $car->\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: 14,
},
},
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());
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(&"openTrunk"),
"Should include Car's own 'openTrunk'"
);
assert!(
method_names.contains(&"start"),
"Should include inherited 'start' from Vehicle"
);
assert!(
!method_names.contains(&"fuelCheck"),
"Should NOT include inherited protected 'fuelCheck' from unrelated class"
);
assert!(
!method_names.contains(&"internalDiag"),
"Should NOT include inherited private 'internalDiag'"
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_completion_magic_methods_not_inherited() {
let backend = create_test_backend();
let uri = Url::parse("file:///magic_inherit.php").unwrap();
let text = concat!(
"<?php\n",
"class Base {\n",
" public function __construct() {}\n",
" public function __toString(): string { return ''; }\n",
" public function realMethod(): void {}\n",
"}\n",
"class Child extends Base {\n",
" function test() {\n",
" $this->\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: 8,
character: 15,
},
},
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());
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(&"__construct"),
"Magic methods should be filtered"
);
assert!(
!method_names.contains(&"__toString"),
"Magic methods should be filtered"
);
assert!(
method_names.contains(&"realMethod"),
"Should include inherited 'realMethod'"
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_static_return_type_resolved_to_caller_class() {
let backend = create_test_backend();
let uri = Url::parse("file:///static_return.php").unwrap();
let text = concat!(
"<?php\n",
"class Builder {\n",
" /** @return static */\n",
" public function configure(): static { return $this; }\n",
" public function build(): void {}\n",
"}\n",
"class AppBuilder extends Builder {\n",
" public function setDebug(): void {}\n",
"}\n",
"class TestClass {\n",
" public function test() {\n",
" $builder = new AppBuilder();\n",
" $builder->configure()->\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: 31,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: None,
};
let result = backend.completion(completion_params).await.unwrap();
let items = match result {
Some(CompletionResponse::Array(items)) => items,
Some(CompletionResponse::List(list)) => list.items,
None => vec![],
};
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(&"setDebug"),
"static return should resolve to AppBuilder and include setDebug. Got: {:?}",
method_names
);
assert!(
method_names.contains(&"configure"),
"Should include inherited configure. Got: {:?}",
method_names
);
assert!(
method_names.contains(&"build"),
"Should include inherited build. Got: {:?}",
method_names
);
}
#[tokio::test]
async fn test_static_return_type_cross_file_psr4() {
let (backend, _dir) = create_psr4_workspace(
r#"{
"autoload": {
"psr-4": {
"Acme\\": "src/"
}
}
}"#,
&[
(
"src/Builder.php",
concat!(
"<?php\n",
"namespace Acme;\n",
"class Builder {\n",
" /** @return static */\n",
" public function configure(): static { return $this; }\n",
" public function build(): void {}\n",
"}\n",
),
),
(
"src/AppBuilder.php",
concat!(
"<?php\n",
"namespace Acme;\n",
"class AppBuilder extends Builder {\n",
" public function setDebug(): void {}\n",
"}\n",
),
),
],
);
let uri = Url::parse("file:///app.php").unwrap();
let text = concat!(
"<?php\n",
"use Acme\\AppBuilder;\n",
"class Controller {\n",
" public function test() {\n",
" $builder = new AppBuilder();\n",
" $builder->configure()->\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: 5,
character: 31,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: None,
};
let result = backend.completion(completion_params).await.unwrap();
let items = match result {
Some(CompletionResponse::Array(items)) => items,
Some(CompletionResponse::List(list)) => list.items,
None => vec![],
};
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(&"setDebug"),
"Cross-file static return should resolve to AppBuilder. Got: {:?}",
method_names
);
assert!(
method_names.contains(&"configure"),
"Should include inherited configure cross-file. Got: {:?}",
method_names
);
assert!(
method_names.contains(&"build"),
"Should include inherited build cross-file. Got: {:?}",
method_names
);
}
#[tokio::test]
async fn test_static_return_type_chained_calls() {
let backend = create_test_backend();
let uri = Url::parse("file:///static_chain.php").unwrap();
let text = concat!(
"<?php\n",
"class Query {\n",
" /** @return static */\n",
" public function where(): static { return $this; }\n",
" /** @return static */\n",
" public function orderBy(): static { return $this; }\n",
" public function get(): array { return []; }\n",
"}\n",
"class UserQuery extends Query {\n",
" public function active(): static { return $this; }\n",
"}\n",
"class TestClass {\n",
" public function test() {\n",
" $q = new UserQuery();\n",
" $q->where()->orderBy()->\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: 14,
character: 32,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: None,
};
let result = backend.completion(completion_params).await.unwrap();
let items = match result {
Some(CompletionResponse::Array(items)) => items,
Some(CompletionResponse::List(list)) => list.items,
None => vec![],
};
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(&"active"),
"Chained static returns should resolve to UserQuery. Got: {:?}",
method_names
);
assert!(
method_names.contains(&"where"),
"Should include inherited where. Got: {:?}",
method_names
);
assert!(
method_names.contains(&"get"),
"Should include inherited get. Got: {:?}",
method_names
);
}
#[tokio::test]
async fn test_namespaced_static_method_return_type_chain() {
let (backend, _dir) = create_psr4_workspace(
r#"{
"autoload": {
"psr-4": {
"App\\Models\\": "src/Models/",
"Illuminate\\Database\\Eloquent\\": "src/Eloquent/"
}
}
}"#,
&[
(
"src/Eloquent/Builder.php",
concat!(
"<?php\n",
"namespace Illuminate\\Database\\Eloquent;\n",
"class Builder {\n",
" public function where(): static { return $this; }\n",
" public function first(): mixed { return null; }\n",
"}\n",
),
),
(
"src/Models/Model.php",
concat!(
"<?php\n",
"namespace App\\Models;\n",
"abstract class Model {\n",
" /** @return \\Illuminate\\Database\\Eloquent\\Builder */\n",
" public static function query() {}\n",
" public function save(): bool { return true; }\n",
"}\n",
),
),
(
"src/Models/LeadProvider.php",
concat!(
"<?php\n",
"namespace App\\Models;\n",
"final class LeadProvider extends Model {}\n",
),
),
],
);
let uri1 = Url::parse("file:///step1.php").unwrap();
let text1 = concat!(
"<?php\n",
"use App\\Models\\LeadProvider;\n",
"LeadProvider::\n",
);
backend
.did_open(DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: uri1.clone(),
language_id: "php".to_string(),
version: 1,
text: text1.to_string(),
},
})
.await;
let result1 = backend
.completion(CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri1 },
position: Position {
line: 2,
character: 15,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: None,
})
.await
.unwrap();
let items1 = match result1 {
Some(CompletionResponse::Array(items)) => items,
Some(CompletionResponse::List(list)) => list.items,
None => vec![],
};
let labels1: Vec<&str> = items1
.iter()
.map(|i| i.filter_text.as_deref().unwrap_or(i.label.as_str()))
.collect();
assert!(
labels1.contains(&"query"),
"LeadProvider:: should include inherited 'query'. Got: {:?}",
labels1
);
let uri2 = Url::parse("file:///step2.php").unwrap();
let text2 = concat!(
"<?php\n",
"use App\\Models\\Model;\n",
"class Step2 {\n",
" function t() {\n",
" Model::query()->\n",
" }\n",
"}\n",
);
backend
.did_open(DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: uri2.clone(),
language_id: "php".to_string(),
version: 1,
text: text2.to_string(),
},
})
.await;
let result2 = backend
.completion(CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri2 },
position: Position {
line: 4,
character: 24,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: None,
})
.await
.unwrap();
let items2 = match result2 {
Some(CompletionResponse::Array(items)) => items,
Some(CompletionResponse::List(list)) => list.items,
None => vec![],
};
let labels2: Vec<&str> = items2
.iter()
.map(|i| i.filter_text.as_deref().unwrap_or(i.label.as_str()))
.collect();
assert!(
labels2.contains(&"where"),
"Model::query()-> should include Builder::where(). Got: {:?}",
labels2
);
let uri3 = Url::parse("file:///step3.php").unwrap();
let text3 = concat!(
"<?php\n",
"use App\\Models\\LeadProvider;\n",
"class Step3 {\n",
" function t() {\n",
" LeadProvider::query()->\n",
" }\n",
"}\n",
);
backend
.did_open(DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: uri3.clone(),
language_id: "php".to_string(),
version: 1,
text: text3.to_string(),
},
})
.await;
let result3 = backend
.completion(CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri3 },
position: Position {
line: 4,
character: 31,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: None,
})
.await
.unwrap();
let items3 = match result3 {
Some(CompletionResponse::Array(items)) => items,
Some(CompletionResponse::List(list)) => list.items,
None => vec![],
};
let labels3: Vec<&str> = items3
.iter()
.map(|i| i.filter_text.as_deref().unwrap_or(i.label.as_str()))
.collect();
assert!(
labels3.contains(&"where"),
"LeadProvider::query()-> should include Builder::where(). Got: {:?}",
labels3
);
assert!(
labels3.contains(&"first"),
"LeadProvider::query()-> should include Builder::first(). Got: {:?}",
labels3
);
}
#[tokio::test]
async fn test_interface_method_and_property_tags_visible_on_implementor() {
let backend = create_test_backend();
let uri = Url::parse("file:///iface_virtual.php").unwrap();
let text = concat!(
"<?php\n",
"/**\n",
" * @property-read string $iguana\n",
" * @method string jaguar()\n",
" */\n",
"interface Contract {}\n",
"\n",
"/**\n",
" * @property string $gorilla\n",
" * @method bool hyena(string $x)\n",
" */\n",
"class Zoo implements Contract {\n",
" public function demo(): void {\n",
" $this->\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: 13,
character: 15,
},
},
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");
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_or(i.label.as_str()))
.collect();
let prop_names: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::PROPERTY))
.map(|i| i.filter_text.as_deref().unwrap_or(i.label.as_str()))
.collect();
assert!(
method_names.contains(&"hyena"),
"Should include @method 'hyena' from own class docblock, got: {:?}",
method_names
);
assert!(
prop_names.contains(&"gorilla"),
"Should include @property 'gorilla' from own class docblock, got: {:?}",
prop_names
);
assert!(
method_names.contains(&"jaguar"),
"Should include @method 'jaguar' from implemented interface, got: {:?}",
method_names
);
assert!(
prop_names.contains(&"iguana"),
"Should include @property-read 'iguana' from implemented interface, got: {:?}",
prop_names
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_interface_virtual_members_visible_on_variable() {
let backend = create_test_backend();
let uri = Url::parse("file:///iface_virtual_var.php").unwrap();
let text = concat!(
"<?php\n",
"/**\n",
" * @property-read string $iguana\n",
" * @method string jaguar()\n",
" */\n",
"interface Contract {}\n",
"\n",
"/**\n",
" * @property string $gorilla\n",
" * @method bool hyena(string $x)\n",
" */\n",
"class Zoo implements Contract {\n",
" public string $baboon = '';\n",
" public function aardvark(): void {}\n",
"}\n",
"\n",
"function demo(): void {\n",
" $zoo = new Zoo();\n",
" $zoo->\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: 18,
character: 10,
},
},
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");
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_or(i.label.as_str()))
.collect();
let prop_names: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::PROPERTY))
.map(|i| i.filter_text.as_deref().unwrap_or(i.label.as_str()))
.collect();
assert!(
method_names.contains(&"aardvark"),
"Should include own method 'aardvark', got: {:?}",
method_names
);
assert!(
prop_names.contains(&"baboon"),
"Should include own property 'baboon', got: {:?}",
prop_names
);
assert!(
method_names.contains(&"hyena"),
"Should include @method 'hyena' from own class, got: {:?}",
method_names
);
assert!(
prop_names.contains(&"gorilla"),
"Should include @property 'gorilla' from own class, got: {:?}",
prop_names
);
assert!(
method_names.contains(&"jaguar"),
"Should include @method 'jaguar' from interface, got: {:?}",
method_names
);
assert!(
prop_names.contains(&"iguana"),
"Should include @property-read 'iguana' from interface, got: {:?}",
prop_names
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_interface_virtual_members_visible_through_parent_chain() {
let backend = create_test_backend();
let uri = Url::parse("file:///iface_parent_chain.php").unwrap();
let text = concat!(
"<?php\n",
"/**\n",
" * @property-read string $sensor\n",
" * @method string scan()\n",
" */\n",
"interface Scannable {}\n",
"\n",
"class BaseDevice implements Scannable {\n",
" public function power(): void {}\n",
"}\n",
"\n",
"class Scanner extends BaseDevice {\n",
" public function demo(): void {\n",
" $this->\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: 13,
character: 15,
},
},
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");
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_or(i.label.as_str()))
.collect();
let prop_names: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::PROPERTY))
.map(|i| i.filter_text.as_deref().unwrap_or(i.label.as_str()))
.collect();
assert!(
method_names.contains(&"power"),
"Should include inherited 'power' from BaseDevice, got: {:?}",
method_names
);
assert!(
method_names.contains(&"scan"),
"Should include @method 'scan' from interface on parent class, got: {:?}",
method_names
);
assert!(
prop_names.contains(&"sensor"),
"Should include @property-read 'sensor' from interface on parent class, got: {:?}",
prop_names
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_completion_deep_inheritance_through_stubs() {
let backend = create_test_backend_with_full_stubs();
let uri = Url::parse("file:///deep_chain.php").unwrap();
let text = concat!(
"<?php\n",
"class QueryException extends \\PDOException {\n",
" public function getSql(): string { return ''; }\n",
"}\n",
"class DeepChainTest {\n",
" public function handle(QueryException $e): void {\n",
" $e->\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: 6,
character: 12,
},
},
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 QueryException"
);
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(&"getSql"),
"Should include own method 'getSql', got: {:?}",
method_names
);
assert!(
method_names.contains(&"getMessage"),
"Should include 'getMessage' inherited from Exception through deep chain, got: {:?}",
method_names
);
assert!(
method_names.contains(&"getCode"),
"Should include 'getCode' inherited from Exception through deep chain, got: {:?}",
method_names
);
assert!(
method_names.contains(&"getTrace"),
"Should include 'getTrace' inherited from Exception through deep chain, got: {:?}",
method_names
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_static_return_type_on_static_method_called_from_subclass() {
let backend = create_test_backend();
let uri = Url::parse("file:///static_method_return.php").unwrap();
let text = concat!(
"<?php\n",
"class Model {\n",
" /** @return ?static */\n",
" public static function first(): ?static { return null; }\n",
" public function save(): bool { return true; }\n",
"}\n",
"class AdminUser extends Model {\n",
" public function assignRole(string $role): void {}\n",
"}\n",
"class TestClass {\n",
" public function test() {\n",
" $admin = AdminUser::first();\n",
" $admin->\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: 16,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: None,
};
let result = backend.completion(completion_params).await.unwrap();
let items = match result {
Some(CompletionResponse::Array(items)) => items,
Some(CompletionResponse::List(list)) => list.items,
None => vec![],
};
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.is_empty(),
"Should resolve type of $admin from AdminUser::first() returning ?static. Got no methods."
);
assert!(
method_names.contains(&"assignRole"),
"static return should resolve to AdminUser, including own method assignRole. Got: {:?}",
method_names
);
assert!(
method_names.contains(&"save"),
"Should include inherited save from Model. Got: {:?}",
method_names
);
}
#[tokio::test]
async fn test_static_return_type_on_static_method_cross_file() {
let (backend, _dir) = create_psr4_workspace(
r#"{
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
}"#,
&[
(
"src/Model.php",
concat!(
"<?php\n",
"namespace App;\n",
"class Model {\n",
" /** @return ?static */\n",
" public static function first(): ?static { return null; }\n",
" public function save(): bool { return true; }\n",
"}\n",
),
),
(
"src/AdminUser.php",
concat!(
"<?php\n",
"namespace App;\n",
"class AdminUser extends Model {\n",
" public function assignRole(string $role): void {}\n",
"}\n",
),
),
],
);
let uri = Url::parse("file:///app.php").unwrap();
let text = concat!(
"<?php\n",
"use App\\AdminUser;\n",
"class Seeder {\n",
" public function run() {\n",
" $admin = AdminUser::first();\n",
" $admin->\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: 5,
character: 16,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: None,
};
let result = backend.completion(completion_params).await.unwrap();
let items = match result {
Some(CompletionResponse::Array(items)) => items,
Some(CompletionResponse::List(list)) => list.items,
None => vec![],
};
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(&"assignRole"),
"Cross-file static method return should resolve to AdminUser. Got: {:?}",
method_names
);
assert!(
method_names.contains(&"save"),
"Should include inherited save cross-file. Got: {:?}",
method_names
);
}
#[tokio::test]
async fn test_static_return_type_static_method_chained() {
let backend = create_test_backend();
let uri = Url::parse("file:///static_chain.php").unwrap();
let text = concat!(
"<?php\n",
"class Model {\n",
" /** @return static */\n",
" public static function query(): static { return new static(); }\n",
" /** @return static */\n",
" public function where(string $col): static { return $this; }\n",
" public function get(): array { return []; }\n",
"}\n",
"class Product extends Model {\n",
" public function applyDiscount(): void {}\n",
"}\n",
"Product::query()->where('active')->\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: 37,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: None,
};
let result = backend.completion(completion_params).await.unwrap();
let items = match result {
Some(CompletionResponse::Array(items)) => items,
Some(CompletionResponse::List(list)) => list.items,
None => vec![],
};
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(&"applyDiscount"),
"Static chain should preserve Product type through query()->where(). Got: {:?}",
method_names
);
assert!(
method_names.contains(&"get"),
"Should include inherited get. Got: {:?}",
method_names
);
}
#[tokio::test]
async fn test_hover_shows_inherited_interface_return_type() {
let backend = create_test_backend();
let uri = Url::parse("file:///hover_iface_ret.php").unwrap();
let text = concat!(
"<?php\n", "class Pen { public function write(): void {} }\n", "interface PenHolder {\n", " /** @return list<Pen> */\n", " public function getPens(): array;\n", "}\n", "class Drawer implements PenHolder {\n", " public function getPens(): array { return []; }\n", "}\n", "class Consumer {\n", " function demo(): void {\n", " $holder = new Drawer();\n", " $holder->getPens();\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 hover_params = HoverParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri },
position: Position {
line: 12,
character: 20,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
};
let result = backend.hover(hover_params).await.unwrap();
assert!(result.is_some(), "Hover should return a result");
let hover = result.unwrap();
if let HoverContents::Markup(markup) = hover.contents {
let value = &markup.value;
assert!(
value.contains("list<Pen>"),
"Hover on getPens() should show inherited return type 'list<Pen>'. Got:\n{}",
value
);
} else {
panic!("Expected HoverContents::Markup");
}
}
#[tokio::test]
async fn test_hover_shows_inherited_parent_return_type() {
let backend = create_test_backend();
let uri = Url::parse("file:///hover_parent_ret.php").unwrap();
let text = concat!(
"<?php\n", "class Pen { public function write(): void {} }\n", "class BasePenHolder {\n", " /** @return list<Pen> */\n", " public function getPens(): array { return []; }\n", "}\n", "class ChildHolder extends BasePenHolder {\n", " public function getPens(): array { return []; }\n", "}\n", "class Consumer {\n", " function demo(): void {\n", " $child = new ChildHolder();\n", " $child->getPens();\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 hover_params = HoverParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri },
position: Position {
line: 12,
character: 19,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
};
let result = backend.hover(hover_params).await.unwrap();
assert!(result.is_some(), "Hover should return a result");
let hover = result.unwrap();
if let HoverContents::Markup(markup) = hover.contents {
let value = &markup.value;
assert!(
value.contains("list<Pen>"),
"Hover on getPens() should show inherited return type 'list<Pen>'. Got:\n{}",
value
);
} else {
panic!("Expected HoverContents::Markup");
}
}
#[tokio::test]
async fn test_hover_shows_inherited_param_type() {
let backend = create_test_backend();
let uri = Url::parse("file:///hover_iface_param.php").unwrap();
let text = concat!(
"<?php\n", "class Pen { public function write(): void {} }\n", "interface PenAcceptor {\n", " /** @param list<Pen> $pens */\n", " public function accept(array $pens): void;\n", "}\n", "class PenBox implements PenAcceptor {\n", " public function accept(array $items): void {}\n", "}\n", "class Consumer {\n", " function demo(): void {\n", " $box = new PenBox();\n", " $box->accept([]);\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 hover_params = HoverParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri },
position: Position {
line: 12,
character: 16,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
};
let result = backend.hover(hover_params).await.unwrap();
assert!(result.is_some(), "Hover should return a result");
let hover = result.unwrap();
if let HoverContents::Markup(markup) = hover.contents {
let value = &markup.value;
assert!(
value.contains("list<Pen>"),
"Hover on accept() should show inherited param type 'list<Pen>'. Got:\n{}",
value
);
} else {
panic!("Expected HoverContents::Markup");
}
}
#[tokio::test]
async fn test_interface_docblock_return_type_propagates_to_implementor() {
let backend = create_test_backend();
let uri = Url::parse("file:///iface_return.php").unwrap();
let text = concat!(
"<?php\n",
"class Pen { public function write(): void {} }\n",
"interface PenHolder {\n",
" /** @return list<Pen> */\n",
" public function getPens(): array;\n",
"}\n",
"class Drawer implements PenHolder {\n",
" public function getPens(): array { return []; }\n",
" function test() {\n",
" $this->\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: 9,
character: 15,
},
},
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");
match result.unwrap() {
CompletionResponse::Array(items) => {
let get_pens = items
.iter()
.find(|i| i.filter_text.as_deref() == Some("getPens"))
.expect("Should find getPens in completion");
let detail = get_pens.detail.as_deref().unwrap_or("");
assert!(
detail.contains("list<Pen>"),
"Interface @return list<Pen> should propagate to implementor's \
method detail, not just 'array'. Got: {:?}",
detail
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_parent_docblock_return_type_propagates_to_child() {
let backend = create_test_backend();
let uri = Url::parse("file:///parent_return.php").unwrap();
let text = concat!(
"<?php\n",
"class Pen { public function write(): void {} }\n",
"class BasePenHolder {\n",
" /** @return list<Pen> */\n",
" public function getPens(): array { return []; }\n",
"}\n",
"class ConcretePenHolder extends BasePenHolder {\n",
" public function getPens(): array { return [new Pen()]; }\n",
" function test() {\n",
" $this->\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: 9,
character: 15,
},
},
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");
match result.unwrap() {
CompletionResponse::Array(items) => {
let get_pens = items
.iter()
.find(|i| i.filter_text.as_deref() == Some("getPens"))
.expect("Should find getPens in completion");
let detail = get_pens.detail.as_deref().unwrap_or("");
assert!(
detail.contains("list<Pen>"),
"Parent @return list<Pen> should propagate to child's \
method detail. Got: {:?}",
detail
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_child_own_docblock_return_type_wins_over_parent() {
let backend = create_test_backend();
let uri = Url::parse("file:///child_wins.php").unwrap();
let text = concat!(
"<?php\n",
"class Dog { public function bark(): void {} }\n",
"class Cat { public function meow(): void {} }\n",
"class AnimalStore {\n",
" /** @return list<Dog> */\n",
" public function getAnimals(): array { return []; }\n",
"}\n",
"class CatStore extends AnimalStore {\n",
" /** @return list<Cat> */\n",
" public function getAnimals(): array { return []; }\n",
" function test() {\n",
" $this->\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: 11,
character: 15,
},
},
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");
match result.unwrap() {
CompletionResponse::Array(items) => {
let get_animals = items
.iter()
.find(|i| i.filter_text.as_deref() == Some("getAnimals"))
.expect("Should find getAnimals in completion");
let detail = get_animals.detail.as_deref().unwrap_or("");
assert!(
detail.contains("list<Cat>"),
"Child's own @return list<Cat> should win over parent's list<Dog>. Got: {:?}",
detail
);
assert!(
!detail.contains("Dog"),
"Parent's Dog type should NOT leak through. Got: {:?}",
detail
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_parent_docblock_param_type_propagates_to_child() {
let backend = create_test_backend();
let uri = Url::parse("file:///parent_param.php").unwrap();
let text = concat!(
"<?php\n", "class Pen { public function write(): void {} }\n", "class BasePenAcceptor {\n", " /** @param list<Pen> $pens */\n", " public function accept(array $pens): void {}\n", "}\n", "class ConcretePenAcceptor extends BasePenAcceptor {\n", " public function accept(array $pens): void {\n", " $pens[0]->\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: 8,
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");
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(&"write"),
"Parent @param list<Pen> should propagate to child, \
enabling Pen member completion. Got: {:?}",
method_names
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_interface_docblock_return_type_with_generics() {
let backend = create_test_backend();
let uri = Url::parse("file:///iface_generic.php").unwrap();
let text = concat!(
"<?php\n", "class Pen { public function write(): void {} }\n", "class FancyPen extends Pen {\n", " public function engrave(): void {}\n", "}\n", "/**\n", " * @template T\n", " */\n", "interface Holder {\n", " /** @return list<T> */\n", " public function getItems(): array;\n", "}\n", "/** @implements Holder<FancyPen> */\n", "class FancyDrawer implements Holder {\n", " public function getItems(): array { return []; }\n", " function test() {\n", " $this->\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: 16,
character: 15,
},
},
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");
match result.unwrap() {
CompletionResponse::Array(items) => {
let get_items = items
.iter()
.find(|i| i.filter_text.as_deref() == Some("getItems"))
.expect("Should find getItems in completion");
let detail = get_items.detail.as_deref().unwrap_or("");
assert!(
detail.contains("list<FancyPen>"),
"Generic @implements substitution should propagate through \
interface return type enrichment. Got: {:?}",
detail
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_parent_docblock_return_type_propagates_cross_file() {
let (backend, _dir) = create_psr4_workspace(
r#"{
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
}"#,
&[
(
"src/Pen.php",
concat!(
"<?php\n",
"namespace App;\n",
"class Pen {\n",
" public function write(): void {}\n",
"}\n",
),
),
(
"src/BasePenHolder.php",
concat!(
"<?php\n",
"namespace App;\n",
"class BasePenHolder {\n",
" /** @return list<Pen> */\n",
" public function getPens(): array { return []; }\n",
"}\n",
),
),
],
);
let uri = Url::parse("file:///app.php").unwrap();
let text = concat!(
"<?php\n", "use App\\BasePenHolder;\n", "class Drawer extends BasePenHolder {\n", " public function getPens(): array { return []; }\n", " function test() {\n", " $this->\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: 5,
character: 15,
},
},
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");
match result.unwrap() {
CompletionResponse::Array(items) => {
let get_pens = items
.iter()
.find(|i| i.filter_text.as_deref() == Some("getPens"))
.expect("Should find getPens in completion");
let detail = get_pens.detail.as_deref().unwrap_or("");
assert!(
detail.contains("list<Pen>"),
"Cross-file parent @return list<Pen> should propagate to child. Got: {:?}",
detail
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_grandparent_docblock_return_type_propagates_through_chain() {
let backend = create_test_backend();
let uri = Url::parse("file:///grandparent_return.php").unwrap();
let text = concat!(
"<?php\n", "class Pen { public function write(): void {} }\n", "class GrandParent_ {\n", " /** @return list<Pen> */\n", " public function getPens(): array { return []; }\n", "}\n", "class Parent_ extends GrandParent_ {\n", " public function getPens(): array { return []; }\n", "}\n", "class Child_ extends Parent_ {\n", " public function getPens(): array { return []; }\n", " function test() {\n", " $this->\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: 15,
},
},
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");
match result.unwrap() {
CompletionResponse::Array(items) => {
let get_pens = items
.iter()
.find(|i| i.filter_text.as_deref() == Some("getPens"))
.expect("Should find getPens in completion");
let detail = get_pens.detail.as_deref().unwrap_or("");
assert!(
detail.contains("list<Pen>"),
"Grandparent @return list<Pen> should propagate through \
the entire chain to the child. Got: {:?}",
detail
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_interface_description_propagates_to_implementor() {
let backend = create_test_backend();
let uri = Url::parse("file:///iface_desc.php").unwrap();
let text = concat!(
"<?php\n", "class Pen { public function write(): void {} }\n", "interface Describable {\n", " /**\n", " * Get the pens.\n", " * @return list<Pen> The pens in the holder\n", " */\n", " public function getPens(): array;\n", "}\n", "class Person implements Describable {\n", " public function getPens(): array { return []; }\n", " function test() {\n", " $this->\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: 15,
},
},
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");
match result.unwrap() {
CompletionResponse::Array(items) => {
let get_pens = items
.iter()
.find(|i| i.filter_text.as_deref() == Some("getPens"))
.expect("Should find getPens in completion");
let detail = get_pens.detail.as_deref().unwrap_or("");
assert!(
detail.contains("list<Pen>"),
"Interface return type and description should propagate. Got: {:?}",
detail
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_parent_description_propagates_to_child() {
let backend = create_test_backend();
let uri = Url::parse("file:///parent_desc.php").unwrap();
let text = concat!(
"<?php\n", "class Pen { public function write(): void {} }\n", "class BaseRepo {\n", " /**\n", " * Find all pens.\n", " * @return list<Pen> The found pens\n", " */\n", " public function findPens(): array { return []; }\n", "}\n", "class UserRepo extends BaseRepo {\n", " public function findPens(): array { return []; }\n", " function test() {\n", " $this->\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: 15,
},
},
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");
match result.unwrap() {
CompletionResponse::Array(items) => {
let find_pens = items
.iter()
.find(|i| i.filter_text.as_deref() == Some("findPens"))
.expect("Should find findPens in completion");
let detail = find_pens.detail.as_deref().unwrap_or("");
assert!(
detail.contains("list<Pen>"),
"Parent @return list<Pen> should propagate to child. Got: {:?}",
detail
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_no_return_type_at_all_inherits_from_interface() {
let backend = create_test_backend();
let uri = Url::parse("file:///no_return.php").unwrap();
let text = concat!(
"<?php\n", "class Pen { public function write(): void {} }\n", "interface PenHolder {\n", " /** @return list<Pen> */\n", " public function getPens(): array;\n", "}\n", "class Drawer implements PenHolder {\n", " public function getPens() { return []; }\n", " function test() {\n", " $this->\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: 9,
character: 15,
},
},
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");
match result.unwrap() {
CompletionResponse::Array(items) => {
let get_pens = items
.iter()
.find(|i| i.filter_text.as_deref() == Some("getPens"))
.expect("Should find getPens in completion");
let detail = get_pens.detail.as_deref().unwrap_or("");
assert!(
detail.contains("list<Pen>"),
"When child has no return type at all, interface @return \
should still propagate. Got: {:?}",
detail
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_return_type_detail_shows_enriched_type() {
let backend = create_test_backend();
let uri = Url::parse("file:///detail_enriched.php").unwrap();
let text = concat!(
"<?php\n",
"class Pen { public function write(): void {} }\n",
"interface PenHolder {\n",
" /** @return list<Pen> */\n",
" public function getPens(): array;\n",
"}\n",
"class Drawer implements PenHolder {\n",
" public function getPens(): array { return []; }\n",
" function test() {\n",
" $this->\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: 9,
character: 15,
},
},
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");
match result.unwrap() {
CompletionResponse::Array(items) => {
let get_pens = items
.iter()
.find(|i| i.filter_text.as_deref() == Some("getPens"))
.expect("Should find getPens in completion");
let detail = get_pens.detail.as_deref().unwrap_or("");
assert!(
detail.contains("list<Pen>"),
"Completion detail should show enriched return type 'list<Pen>', \
not just 'array'. Got: {:?}",
detail
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_child_own_docblock_param_type_wins_over_interface() {
let backend = create_test_backend();
let uri = Url::parse("file:///child_param_wins.php").unwrap();
let text = concat!(
"<?php\n", "class Dog { public function bark(): void {} }\n", "class Cat { public function meow(): void {} }\n", "interface AnimalAcceptor {\n", " /** @param list<Dog> $animals */\n", " public function accept(array $animals): void;\n", "}\n", "class CatAcceptor implements AnimalAcceptor {\n", " /** @param list<Cat> $animals */\n", " public function accept(array $animals): void {\n", " $animals[0]->\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: 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");
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(&"meow"),
"Child's own @param list<Cat> should win over interface's list<Dog>. Got: {:?}",
method_names
);
assert!(
!method_names.contains(&"bark"),
"Interface's Dog type should NOT leak through. Got: {:?}",
method_names
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_inherited_interface_return_type_enables_array_element_completion() {
let backend = create_test_backend();
let uri = Url::parse("file:///chain_iface.php").unwrap();
let text = concat!(
"<?php\n", "class Pen { public function write(): void {} }\n", "interface PenHolder {\n", " /** @return list<Pen> */\n", " public function getPens(): array;\n", "}\n", "class Drawer implements PenHolder {\n", " public function getPens(): array { return []; }\n", "}\n", "class Consumer {\n", " function demo(): void {\n", " $d = new Drawer();\n", " $d->getPens()[0]->\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: 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");
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(&"write"),
"Interface @return list<Pen> should propagate to implementor, \
enabling Pen member completion on [0]->. Got: {:?}",
method_names
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_inherited_parent_return_type_enables_array_element_completion() {
let backend = create_test_backend();
let uri = Url::parse("file:///chain_parent.php").unwrap();
let text = concat!(
"<?php\n", "class Pen { public function write(): void {} }\n", "class BasePenHolder {\n", " /** @return list<Pen> */\n", " public function getPens(): array { return []; }\n", "}\n", "class ChildHolder extends BasePenHolder {\n", " public function getPens(): array { return []; }\n", "}\n", "class Consumer {\n", " function demo(): void {\n", " $c = new ChildHolder();\n", " $c->getPens()[0]->\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: 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");
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(&"write"),
"Parent @return list<Pen> should propagate to child, \
enabling Pen member completion on [0]->. Got: {:?}",
method_names
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_inherited_grandparent_return_type_enables_array_element_completion() {
let backend = create_test_backend();
let uri = Url::parse("file:///chain_grandparent.php").unwrap();
let text = concat!(
"<?php\n", "class Pen { public function write(): void {} }\n", "class GrandBase {\n", " /** @return list<Pen> */\n", " public function getPens(): array { return []; }\n", "}\n", "class Mid extends GrandBase {\n", " public function getPens(): array { return []; }\n", "}\n", "class Deep extends Mid {\n", " public function getPens(): array { return []; }\n", "}\n", "class Consumer {\n", " function demo(): void {\n", " $d = new Deep();\n", " $d->getPens()[0]->\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: 15,
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");
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(&"write"),
"Grandparent @return list<Pen> should propagate through \
the entire chain, enabling Pen completion on [0]->. Got: {:?}",
method_names
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}