use crate::common::create_test_backend;
use phpantom_lsp::{AccessKind, extract_completion_target};
use tower_lsp::LanguageServer;
use tower_lsp::lsp_types::*;
#[tokio::test]
async fn test_detect_access_kind_arrow() {
let target = extract_completion_target(
"$this->",
Position {
line: 0,
character: 7,
},
)
.expect("should detect arrow");
assert_eq!(target.access_kind, AccessKind::Arrow);
assert_eq!(target.subject, "$this");
}
#[tokio::test]
async fn test_detect_access_kind_arrow_with_partial_identifier() {
let target = extract_completion_target(
"$this->get",
Position {
line: 0,
character: 10,
},
)
.expect("should detect arrow");
assert_eq!(target.access_kind, AccessKind::Arrow);
assert_eq!(target.subject, "$this");
}
#[tokio::test]
async fn test_detect_access_kind_double_colon() {
let target = extract_completion_target(
"self::",
Position {
line: 0,
character: 6,
},
)
.expect("should detect double colon");
assert_eq!(target.access_kind, AccessKind::DoubleColon);
assert_eq!(target.subject, "self");
}
#[tokio::test]
async fn test_detect_access_kind_double_colon_with_partial_identifier() {
let target = extract_completion_target(
"Foo::cr",
Position {
line: 0,
character: 7,
},
)
.expect("should detect double colon");
assert_eq!(target.access_kind, AccessKind::DoubleColon);
assert_eq!(target.subject, "Foo");
}
#[tokio::test]
async fn test_detect_access_kind_other() {
let result = extract_completion_target(
" $x = 1;",
Position {
line: 0,
character: 4,
},
);
assert!(result.is_none(), "no access operator expected");
}
#[tokio::test]
async fn test_detect_access_kind_multiline() {
let content = "<?php\n$obj->meth";
let target = extract_completion_target(
content,
Position {
line: 1,
character: 10,
},
)
.expect("should detect arrow");
assert_eq!(target.access_kind, AccessKind::Arrow);
assert_eq!(target.subject, "$obj");
}
#[tokio::test]
async fn test_completion_arrow_shows_only_non_static() {
let backend = create_test_backend();
let uri = Url::parse("file:///arrow.php").unwrap();
let text = concat!(
"<?php\n",
"class Svc {\n",
" public static function create(): self {}\n",
" public function run(): void {}\n",
" public static string $instance = '';\n",
" public int $count = 0;\n",
" const MAX = 10;\n",
" function helper() {\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(), "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();
let property_names: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::PROPERTY))
.map(|i| i.label.as_str())
.collect();
let constant_names: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::CONSTANT))
.map(|i| i.label.as_str())
.collect();
assert!(
method_names.contains(&"run"),
"Arrow should include non-static method 'run'"
);
assert!(
method_names.contains(&"helper"),
"Arrow should include non-static method 'helper'"
);
assert!(
!method_names.contains(&"create"),
"Arrow should exclude static method 'create'"
);
assert!(
property_names.contains(&"count"),
"Arrow should include non-static property 'count'"
);
assert!(
!property_names.contains(&"instance"),
"Arrow should exclude static property 'instance'"
);
assert!(
constant_names.is_empty(),
"Arrow should not include constants, got: {:?}",
constant_names
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_completion_self_double_colon_shows_all_members() {
let backend = create_test_backend();
let uri = Url::parse("file:///dcolon.php").unwrap();
let text = concat!(
"<?php\n",
"class Svc {\n",
" public static function create(): self {}\n",
" public function run(): void {}\n",
" public static string $instance = '';\n",
" public int $count = 0;\n",
" const MAX = 10;\n",
" function helper() {\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: 8,
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(), "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();
let property_names: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::PROPERTY))
.map(|i| i.label.as_str())
.collect();
let constant_names: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::CONSTANT))
.map(|i| i.label.as_str())
.collect();
assert!(
method_names.contains(&"create"),
"self:: should include static method 'create'"
);
assert!(
method_names.contains(&"run"),
"self:: should include non-static method 'run'"
);
assert!(
method_names.contains(&"helper"),
"self:: should include non-static method 'helper'"
);
assert!(
property_names.contains(&"$instance"),
"self:: should include static property '$instance'"
);
assert!(
!property_names.contains(&"count") && !property_names.contains(&"$count"),
"self:: should exclude non-static property 'count'"
);
assert!(
constant_names.contains(&"MAX"),
"self:: should include constant 'MAX'"
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_completion_arrow_with_partial_typed_identifier() {
let backend = create_test_backend();
let uri = Url::parse("file:///partial.php").unwrap();
let text = concat!(
"<?php\n",
"class Obj {\n",
" public static function staticMethod(): void {}\n",
" public function instanceMethod(): void {}\n",
" function test() {\n",
" $this->inst\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: 19,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: None,
};
let result = backend.completion(completion_params).await.unwrap();
assert!(result.is_some());
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(&"instanceMethod"),
"Should include instanceMethod"
);
assert!(method_names.contains(&"test"), "Should include test");
assert!(
!method_names.contains(&"staticMethod"),
"Should exclude staticMethod when using ->"
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_construct_shown_via_self_inside_same_class() {
let backend = create_test_backend();
let uri = Url::parse("file:///construct_self.php").unwrap();
let text = concat!(
"<?php\n",
"class A {\n",
" public function __construct() {}\n",
" public 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: 4,
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(), "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(&"__construct"),
"self:: inside same class should show __construct, got: {:?}",
method_names
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_construct_shown_via_classname_inside_same_class() {
let backend = create_test_backend();
let uri = Url::parse("file:///construct_classname.php").unwrap();
let text = concat!(
"<?php\n",
"class A {\n",
" public function __construct() {}\n",
" public function test() {\n",
" A::\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: 4,
character: 11,
},
},
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(&"__construct"),
"A:: inside class A should show __construct, got: {:?}",
method_names
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_construct_shown_via_static_inside_same_class() {
let backend = create_test_backend();
let uri = Url::parse("file:///construct_static.php").unwrap();
let text = concat!(
"<?php\n",
"class A {\n",
" public function __construct() {}\n",
" public function test() {\n",
" static::\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: 4,
character: 16,
},
},
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(&"__construct"),
"static:: inside same class should show __construct, got: {:?}",
method_names
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_construct_shown_via_parent_in_subclass() {
let backend = create_test_backend();
let uri = Url::parse("file:///construct_parent.php").unwrap();
let text = concat!(
"<?php\n",
"class A {\n",
" public function __construct() {}\n",
"}\n",
"class B extends A {\n",
" public function __construct() {\n",
" parent::\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: 16,
},
},
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(&"__construct"),
"parent:: in subclass should show __construct, got: {:?}",
method_names
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_construct_shown_via_self_in_subclass() {
let backend = create_test_backend();
let uri = Url::parse("file:///construct_self_sub.php").unwrap();
let text = concat!(
"<?php\n",
"class A {\n",
" public function __construct() {}\n",
"}\n",
"class B extends A {\n",
" public function __construct() {\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: 6,
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(), "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(&"__construct"),
"self:: in subclass should show __construct, got: {:?}",
method_names
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_construct_shown_via_parent_classname_in_subclass() {
let backend = create_test_backend();
let uri = Url::parse("file:///construct_parent_name.php").unwrap();
let text = concat!(
"<?php\n",
"class A {\n",
" public function __construct() {}\n",
"}\n",
"class B extends A {\n",
" public function __construct() {\n",
" A::\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: 11,
},
},
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(&"__construct"),
"A:: inside subclass B should show __construct, got: {:?}",
method_names
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_construct_hidden_via_classname_outside_class() {
let backend = create_test_backend();
let uri = Url::parse("file:///construct_outside.php").unwrap();
let text = concat!(
"<?php\n",
"class A {\n",
" public function __construct() {}\n",
"}\n",
"A::\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: 4,
character: 3,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: None,
};
let result = backend.completion(completion_params).await.unwrap();
if let Some(CompletionResponse::Array(items)) = result {
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"),
"A:: outside any class should NOT show __construct, got: {:?}",
method_names
);
}
}
#[tokio::test]
async fn test_construct_hidden_via_classname_in_unrelated_class() {
let backend = create_test_backend();
let uri = Url::parse("file:///construct_unrelated.php").unwrap();
let text = concat!(
"<?php\n",
"class A {\n",
" public function __construct() {}\n",
" public static function make(): self {}\n",
"}\n",
"class C {\n",
" public function test() {\n",
" A::\n",
" }\n",
"}\n",
);
let open_params = DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: uri.clone(),
language_id: "php".to_string(),
version: 1,
text: text.to_string(),
},
};
backend.did_open(open_params).await;
let completion_params = CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri },
position: Position {
line: 7,
character: 11,
},
},
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 (static method 'make')"
);
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(&"make"),
"A:: in unrelated class C should still show static method 'make', got: {:?}",
method_names
);
assert!(
!method_names.contains(&"__construct"),
"A:: in unrelated class C should NOT show __construct, got: {:?}",
method_names
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_double_colon_shows_class_keyword() {
let backend = create_test_backend();
let uri = Url::parse("file:///class_keyword.php").unwrap();
let text = concat!(
"<?php\n",
"class Foo {\n",
" const BAR = 1;\n",
" public static function baz(): 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: 5,
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(), "Completion should return results");
match result.unwrap() {
CompletionResponse::Array(items) => {
let keyword_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::KEYWORD))
.map(|i| i.label.as_str())
.collect();
assert!(
keyword_labels.contains(&"class"),
"self:: should offer ::class keyword, got: {:?}",
keyword_labels
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_self_and_static_show_non_static_methods_like_parent() {
let backend = create_test_backend();
let uri = Url::parse("file:///self_static_parity.php").unwrap();
let text = concat!(
"<?php\n",
"class Animal {\n",
" public function breathe(): void {}\n",
" public static function kingdom(): string { return 'Animalia'; }\n",
" public const LEGS = 4;\n",
" public static string $species = '';\n",
" public int $age = 0;\n",
"}\n",
"class Dog extends Animal {\n",
" public function bark(): void {}\n",
" function testSelf() {\n",
" self::\n",
" }\n",
" function testStatic() {\n",
" static::\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 self_params = CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 11,
character: 14,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: None,
};
let self_result = backend.completion(self_params).await.unwrap();
assert!(self_result.is_some(), "self:: should return completions");
let self_items = match self_result.unwrap() {
CompletionResponse::Array(items) => items,
_ => panic!("Expected CompletionResponse::Array"),
};
let self_method_names: Vec<&str> = self_items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::METHOD))
.map(|i| i.filter_text.as_deref().unwrap())
.collect();
let self_property_names: Vec<&str> = self_items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::PROPERTY))
.map(|i| i.label.as_str())
.collect();
let self_constant_names: Vec<&str> = self_items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::CONSTANT))
.map(|i| i.label.as_str())
.collect();
assert!(
self_method_names.contains(&"breathe"),
"self:: should include inherited non-static 'breathe', got: {:?}",
self_method_names
);
assert!(
self_method_names.contains(&"kingdom"),
"self:: should include inherited static 'kingdom', got: {:?}",
self_method_names
);
assert!(
self_method_names.contains(&"bark"),
"self:: should include own non-static 'bark', got: {:?}",
self_method_names
);
assert!(
self_property_names.contains(&"$species"),
"self:: should include static property '$species', got: {:?}",
self_property_names
);
assert!(
!self_property_names.contains(&"age") && !self_property_names.contains(&"$age"),
"self:: should exclude non-static property 'age', got: {:?}",
self_property_names
);
assert!(
self_constant_names.contains(&"LEGS"),
"self:: should include constant 'LEGS', got: {:?}",
self_constant_names
);
let static_params = CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri },
position: Position {
line: 14,
character: 16,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: None,
};
let static_result = backend.completion(static_params).await.unwrap();
assert!(
static_result.is_some(),
"static:: should return completions"
);
let static_items = match static_result.unwrap() {
CompletionResponse::Array(items) => items,
_ => panic!("Expected CompletionResponse::Array"),
};
let static_method_names: Vec<&str> = static_items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::METHOD))
.map(|i| i.filter_text.as_deref().unwrap())
.collect();
let static_constant_names: Vec<&str> = static_items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::CONSTANT))
.map(|i| i.label.as_str())
.collect();
assert!(
static_method_names.contains(&"breathe"),
"static:: should include inherited non-static 'breathe', got: {:?}",
static_method_names
);
assert!(
static_method_names.contains(&"kingdom"),
"static:: should include inherited static 'kingdom', got: {:?}",
static_method_names
);
assert!(
static_method_names.contains(&"bark"),
"static:: should include own non-static 'bark', got: {:?}",
static_method_names
);
assert!(
static_constant_names.contains(&"LEGS"),
"static:: should include constant 'LEGS', got: {:?}",
static_constant_names
);
}
#[tokio::test]
async fn test_static_property_access_resolves_to_property_type() {
let backend = create_test_backend();
let uri = Url::parse("file:///b17_static_prop.php").unwrap();
let text = concat!(
"<?php\n",
"class Connection {\n",
" public function setConfig(string $key): void {}\n",
" public function ping(): bool { return true; }\n",
"}\n",
"class ConnectionManager {\n",
" private static Connection $instance;\n",
"\n",
" public static function test(): void {\n",
" self::$instance->\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: 26,
},
},
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.unwrap() {
CompletionResponse::Array(items) => items,
CompletionResponse::List(list) => list.items,
};
let method_names: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::METHOD))
.map(|i| i.label.as_str())
.collect();
assert!(
method_names.iter().any(|n| n.starts_with("setConfig")),
"self::$instance-> should resolve to Connection and show 'setConfig', got: {:?}",
method_names
);
assert!(
method_names.iter().any(|n| n.starts_with("ping")),
"self::$instance-> should resolve to Connection and show 'ping', got: {:?}",
method_names
);
}
#[tokio::test]
async fn test_classname_static_property_access_resolves_to_property_type() {
let backend = create_test_backend();
let uri = Url::parse("file:///b17_classname_static.php").unwrap();
let text = concat!(
"<?php\n",
"class Logger {\n",
" public function info(string $msg): void {}\n",
"}\n",
"class Registry {\n",
" public static Logger $logger;\n",
"\n",
" public static function test(): void {\n",
" Registry::$logger->\n",
" }\n",
"}\n",
);
let open_params = DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: uri.clone(),
language_id: "php".to_string(),
version: 1,
text: text.to_string(),
},
};
backend.did_open(open_params).await;
let completion_params = CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri },
position: Position {
line: 8,
character: 28,
},
},
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.unwrap() {
CompletionResponse::Array(items) => items,
CompletionResponse::List(list) => list.items,
};
let method_names: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::METHOD))
.map(|i| i.label.as_str())
.collect();
assert!(
method_names.iter().any(|n| n.starts_with("info")),
"Registry::$logger-> should resolve to Logger and show 'info', got: {:?}",
method_names
);
}
#[tokio::test]
async fn test_parent_static_property_access_resolves_to_property_type() {
let backend = create_test_backend();
let uri = Url::parse("file:///b17_parent_static.php").unwrap();
let text = concat!(
"<?php\n",
"class Formatter {\n",
" public function format(string $s): string { return $s; }\n",
"}\n",
"class Base {\n",
" protected static Formatter $fmt;\n",
"}\n",
"class Child extends Base {\n",
" public function test(): void {\n",
" parent::$fmt->\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: 22,
},
},
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.unwrap() {
CompletionResponse::Array(items) => items,
CompletionResponse::List(list) => list.items,
};
let method_names: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::METHOD))
.map(|i| i.label.as_str())
.collect();
assert!(
method_names.iter().any(|n| n.starts_with("format")),
"parent::$fmt-> should resolve to Formatter and show 'format', got: {:?}",
method_names
);
}