use crate::common::create_test_backend;
use tower_lsp::LanguageServer;
use tower_lsp::lsp_types::*;
#[test]
fn test_parse_object_shape_basic() {
use phpantom_lsp::docblock::parse_object_shape_typed;
use phpantom_lsp::php_type::PhpType;
let entries =
parse_object_shape_typed(&PhpType::parse("object{foo: int, bar: string}")).unwrap();
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].key.as_deref(), Some("foo"));
assert_eq!(entries[0].value_type.to_string(), "int");
assert!(!entries[0].optional);
assert_eq!(entries[1].key.as_deref(), Some("bar"));
assert_eq!(entries[1].value_type.to_string(), "string");
assert!(!entries[1].optional);
}
#[test]
fn test_parse_object_shape_optional_property() {
use phpantom_lsp::docblock::parse_object_shape_typed;
use phpantom_lsp::php_type::PhpType;
let entries =
parse_object_shape_typed(&PhpType::parse("object{foo: int, bar?: string}")).unwrap();
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].key.as_deref(), Some("foo"));
assert!(!entries[0].optional);
assert_eq!(entries[1].key.as_deref(), Some("bar"));
assert_eq!(entries[1].value_type.to_string(), "string");
assert!(entries[1].optional);
}
#[test]
fn test_parse_object_shape_quoted_keys() {
use phpantom_lsp::docblock::parse_object_shape_typed;
use phpantom_lsp::php_type::PhpType;
let entries =
parse_object_shape_typed(&PhpType::parse(r#"object{'foo': int, "bar": string}"#)).unwrap();
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].key.as_deref(), Some("foo"));
assert_eq!(entries[0].value_type.to_string(), "int");
assert_eq!(entries[1].key.as_deref(), Some("bar"));
assert_eq!(entries[1].value_type.to_string(), "string");
}
#[test]
fn test_parse_object_shape_quoted_optional() {
use phpantom_lsp::docblock::parse_object_shape_typed;
use phpantom_lsp::php_type::PhpType;
let entries =
parse_object_shape_typed(&PhpType::parse(r#"object{'foo': int, "bar"?: string}"#)).unwrap();
assert_eq!(entries.len(), 2);
assert!(!entries[0].optional);
assert!(entries[1].optional);
}
#[test]
fn test_parse_object_shape_empty() {
use phpantom_lsp::docblock::parse_object_shape_typed;
use phpantom_lsp::php_type::PhpType;
let entries = parse_object_shape_typed(&PhpType::parse("object{}")).unwrap();
assert!(entries.is_empty());
}
#[test]
fn test_parse_object_shape_nullable() {
use phpantom_lsp::docblock::parse_object_shape_typed;
use phpantom_lsp::php_type::PhpType;
let entries = parse_object_shape_typed(&PhpType::parse("?object{foo: int}")).unwrap();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].key.as_deref(), Some("foo"));
assert_eq!(entries[0].value_type.to_string(), "int");
}
#[test]
fn test_parse_object_shape_canonical_form() {
use phpantom_lsp::docblock::parse_object_shape_typed;
use phpantom_lsp::php_type::PhpType;
let entries = parse_object_shape_typed(&PhpType::parse("object{foo: int}")).unwrap();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].key.as_deref(), Some("foo"));
}
#[test]
fn test_parse_object_shape_complex_value_types() {
use phpantom_lsp::docblock::parse_object_shape_typed;
use phpantom_lsp::php_type::PhpType;
let entries = parse_object_shape_typed(&PhpType::parse(
"object{user: User, items: list<Item>, meta: array{page: int}}",
))
.unwrap();
assert_eq!(entries.len(), 3);
assert_eq!(entries[0].key.as_deref(), Some("user"));
assert_eq!(entries[0].value_type.to_string(), "User");
assert_eq!(entries[1].key.as_deref(), Some("items"));
assert_eq!(entries[1].value_type.to_string(), "list<Item>");
assert_eq!(entries[2].key.as_deref(), Some("meta"));
assert_eq!(entries[2].value_type.to_string(), "array{page: int}");
}
#[test]
fn test_parse_object_shape_not_an_object_shape() {
use phpantom_lsp::docblock::parse_object_shape_typed;
use phpantom_lsp::php_type::PhpType;
assert!(parse_object_shape_typed(&PhpType::parse("array{foo: int}")).is_none());
assert!(parse_object_shape_typed(&PhpType::parse("string")).is_none());
assert!(parse_object_shape_typed(&PhpType::parse("object")).is_none());
assert!(parse_object_shape_typed(&PhpType::parse("User")).is_none());
}
#[test]
fn test_is_object_shape() {
use phpantom_lsp::docblock::is_object_shape_typed;
use phpantom_lsp::php_type::PhpType;
assert!(is_object_shape_typed(&PhpType::parse("object{foo: int}")));
assert!(is_object_shape_typed(&PhpType::parse("?object{foo: int}")));
assert!(is_object_shape_typed(&PhpType::parse("object{}")));
assert!(!is_object_shape_typed(&PhpType::parse("object")));
assert!(!is_object_shape_typed(&PhpType::parse("array{foo: int}")));
assert!(!is_object_shape_typed(&PhpType::parse("string")));
}
#[test]
fn test_extract_object_shape_property_type() {
use phpantom_lsp::docblock::extract_object_shape_property_type_typed;
use phpantom_lsp::php_type::PhpType;
assert_eq!(
extract_object_shape_property_type_typed(
&PhpType::parse("object{name: string, user: User}"),
"user"
)
.map(|t| t.to_string()),
Some("User".to_string())
);
assert_eq!(
extract_object_shape_property_type_typed(
&PhpType::parse("object{name: string, user: User}"),
"name"
)
.map(|t| t.to_string()),
Some("string".to_string())
);
assert_eq!(
extract_object_shape_property_type_typed(
&PhpType::parse("object{name: string, user: User}"),
"missing"
)
.map(|t| t.to_string()),
None
);
assert_eq!(
extract_object_shape_property_type_typed(&PhpType::parse("array{name: string}"), "name")
.map(|t| t.to_string()),
None
);
}
#[test]
fn test_extract_object_shape_property_type_quoted_key() {
use phpantom_lsp::docblock::extract_object_shape_property_type_typed;
use phpantom_lsp::php_type::PhpType;
let parsed = PhpType::parse(r#"object{"host": string, 'port': int, ssl: bool}"#);
assert_eq!(
extract_object_shape_property_type_typed(&parsed, "host").map(|t| t.to_string()),
Some("string".to_string())
);
assert_eq!(
extract_object_shape_property_type_typed(&parsed, "port").map(|t| t.to_string()),
Some("int".to_string())
);
assert_eq!(
extract_object_shape_property_type_typed(&parsed, "ssl").map(|t| t.to_string()),
Some("bool".to_string())
);
}
#[tokio::test]
async fn test_object_shape_basic_property_completion() {
let backend = create_test_backend();
let uri = Url::parse("file:///object_shape_basic.php").unwrap();
let text = concat!(
"<?php\n",
"class Service {\n",
" /**\n",
" * @return object{name: string, age: int, active: bool}\n",
" */\n",
" public function getData(): object {\n",
" return (object) [];\n",
" }\n",
"}\n",
"$svc = new Service();\n",
"$result = $svc->getData();\n",
"$result->\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: 9,
},
},
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(),
"Should return completions for object shape properties"
);
match result.unwrap() {
CompletionResponse::Array(items) => {
let prop_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::PROPERTY))
.map(|i| i.filter_text.as_deref().unwrap_or(&i.label))
.collect();
assert!(
prop_labels.contains(&"name"),
"Should suggest 'name', got {:?}",
prop_labels
);
assert!(
prop_labels.contains(&"age"),
"Should suggest 'age', got {:?}",
prop_labels
);
assert!(
prop_labels.contains(&"active"),
"Should suggest 'active', got {:?}",
prop_labels
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_object_shape_optional_property_completion() {
let backend = create_test_backend();
let uri = Url::parse("file:///object_shape_optional.php").unwrap();
let text = concat!(
"<?php\n",
"class Maker {\n",
" /**\n",
" * @return object{required: int, optional?: string}\n",
" */\n",
" public function make(): object {\n",
" return (object) [];\n",
" }\n",
"}\n",
"$m = new Maker();\n",
"$obj = $m->make();\n",
"$obj->\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: 6,
},
},
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(), "Should return completions");
match result.unwrap() {
CompletionResponse::Array(items) => {
let prop_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::PROPERTY))
.map(|i| i.filter_text.as_deref().unwrap_or(&i.label))
.collect();
assert!(
prop_labels.contains(&"required"),
"Should suggest 'required', got {:?}",
prop_labels
);
assert!(
prop_labels.contains(&"optional"),
"Should suggest 'optional', got {:?}",
prop_labels
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_object_shape_value_type_chain_resolution() {
let backend = create_test_backend();
let uri = Url::parse("file:///object_shape_chain.php").unwrap();
let text = concat!(
"<?php\n",
"class User {\n",
" public string $name;\n",
" public function getEmail(): string {}\n",
"}\n",
"class Handler {\n",
" /**\n",
" * @return object{user: User, count: int}\n",
" */\n",
" public function process(): object {\n",
" return (object) [];\n",
" }\n",
"}\n",
"$h = new Handler();\n",
"$data = $h->process();\n",
"$data->user->\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: 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(),
"Should chain-resolve object shape property type to User"
);
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))
.collect();
assert!(
method_names.contains(&"getEmail"),
"Should suggest User::getEmail(), got {:?}",
method_names
);
let prop_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::PROPERTY))
.map(|i| i.filter_text.as_deref().unwrap_or(&i.label))
.collect();
assert!(
prop_labels.contains(&"name"),
"Should suggest User::$name, got {:?}",
prop_labels
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_object_shape_var_annotation() {
let backend = create_test_backend();
let uri = Url::parse("file:///object_shape_var.php").unwrap();
let text = concat!(
"<?php\n",
"/** @var object{title: string, score: float} $item */\n",
"$item = getUnknown();\n",
"$item->\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: 3,
character: 7,
},
},
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(),
"Should return completions from @var object shape"
);
match result.unwrap() {
CompletionResponse::Array(items) => {
let prop_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::PROPERTY))
.map(|i| i.filter_text.as_deref().unwrap_or(&i.label))
.collect();
assert!(
prop_labels.contains(&"title"),
"Should suggest 'title', got {:?}",
prop_labels
);
assert!(
prop_labels.contains(&"score"),
"Should suggest 'score', got {:?}",
prop_labels
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_object_shape_param_annotation() {
let backend = create_test_backend();
let uri = Url::parse("file:///object_shape_param.php").unwrap();
let text = concat!(
"<?php\n",
"class Handler {\n",
" /**\n",
" * @param object{host: string, port: int, ssl: bool} $config\n",
" */\n",
" public function connect(object $config): void {\n",
" $config->\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: 17,
},
},
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(),
"Should return completions from @param object shape"
);
match result.unwrap() {
CompletionResponse::Array(items) => {
let prop_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::PROPERTY))
.map(|i| i.filter_text.as_deref().unwrap_or(&i.label))
.collect();
assert!(
prop_labels.contains(&"host"),
"Should suggest 'host', got {:?}",
prop_labels
);
assert!(
prop_labels.contains(&"port"),
"Should suggest 'port', got {:?}",
prop_labels
);
assert!(
prop_labels.contains(&"ssl"),
"Should suggest 'ssl', got {:?}",
prop_labels
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_object_shape_intersection_with_stdclass() {
let backend = create_test_backend();
let uri = Url::parse("file:///object_shape_intersection.php").unwrap();
let text = concat!(
"<?php\n",
"class Factory {\n",
" /**\n",
" * @return object{name: string, value: int}&\\stdClass\n",
" */\n",
" public function create(): object {\n",
" return (object) [];\n",
" }\n",
"}\n",
"$f = new Factory();\n",
"$obj = $f->create();\n",
"$obj->\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: 6,
},
},
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(),
"Should return completions for object shape intersection"
);
match result.unwrap() {
CompletionResponse::Array(items) => {
let prop_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::PROPERTY))
.map(|i| i.filter_text.as_deref().unwrap_or(&i.label))
.collect();
assert!(
prop_labels.contains(&"name"),
"Should suggest 'name' from object shape, got {:?}",
prop_labels
);
assert!(
prop_labels.contains(&"value"),
"Should suggest 'value' from object shape, got {:?}",
prop_labels
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_object_shape_in_union_type() {
let backend = create_test_backend();
let uri = Url::parse("file:///object_shape_union.php").unwrap();
let text = concat!(
"<?php\n",
"class Provider {\n",
" /**\n",
" * @return object{status: string, code: int}|null\n",
" */\n",
" public function fetch(): ?object {\n",
" return null;\n",
" }\n",
"}\n",
"$p = new Provider();\n",
"$result = $p->fetch();\n",
"$result->\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: 9,
},
},
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(),
"Should return completions for nullable object shape"
);
match result.unwrap() {
CompletionResponse::Array(items) => {
let prop_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::PROPERTY))
.map(|i| i.filter_text.as_deref().unwrap_or(&i.label))
.collect();
assert!(
prop_labels.contains(&"status"),
"Should suggest 'status', got {:?}",
prop_labels
);
assert!(
prop_labels.contains(&"code"),
"Should suggest 'code', got {:?}",
prop_labels
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_bare_object_no_completions() {
let backend = create_test_backend();
let uri = Url::parse("file:///object_bare.php").unwrap();
let text = concat!(
"<?php\n",
"class Service {\n",
" public function get(): object {\n",
" return (object) [];\n",
" }\n",
"}\n",
"$s = new Service();\n",
"$obj = $s->get();\n",
"$obj->\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: 6,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: None,
};
let result = backend.completion(completion_params).await.unwrap();
if let Some(resp) = result {
let items = match resp {
CompletionResponse::Array(items) => items,
CompletionResponse::List(list) => list.items,
};
let class_members: Vec<&str> = items
.iter()
.filter(|i| {
i.kind == Some(CompletionItemKind::METHOD)
|| i.kind == Some(CompletionItemKind::PROPERTY)
})
.map(|i| i.label.as_str())
.collect();
assert!(
class_members.is_empty(),
"Bare 'object' should not produce member completions, got {:?}",
class_members
);
}
}
#[tokio::test]
async fn test_object_shape_from_this_method() {
let backend = create_test_backend();
let uri = Url::parse("file:///object_shape_this.php").unwrap();
let text = concat!(
"<?php\n",
"class Api {\n",
" /**\n",
" * @return object{id: int, title: string}\n",
" */\n",
" public function getItem(): object {\n",
" return (object) [];\n",
" }\n",
"\n",
" public function test(): void {\n",
" $item = $this->getItem();\n",
" $item->\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(),
"Should return completions from $this->getItem() object shape"
);
match result.unwrap() {
CompletionResponse::Array(items) => {
let prop_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::PROPERTY))
.map(|i| i.filter_text.as_deref().unwrap_or(&i.label))
.collect();
assert!(
prop_labels.contains(&"id"),
"Should suggest 'id', got {:?}",
prop_labels
);
assert!(
prop_labels.contains(&"title"),
"Should suggest 'title', got {:?}",
prop_labels
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_object_shape_nested_chain() {
let backend = create_test_backend();
let uri = Url::parse("file:///object_shape_nested.php").unwrap();
let text = concat!(
"<?php\n",
"class Paginator {\n",
" /**\n",
" * @return object{items: array, meta: object{page: int, total: int}}\n",
" */\n",
" public function paginate(): object {\n",
" return (object) [];\n",
" }\n",
"}\n",
"$p = new Paginator();\n",
"$result = $p->paginate();\n",
"$result->meta->\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: 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(), "Should chain-resolve nested object shape");
match result.unwrap() {
CompletionResponse::Array(items) => {
let prop_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::PROPERTY))
.map(|i| i.filter_text.as_deref().unwrap_or(&i.label))
.collect();
assert!(
prop_labels.contains(&"page"),
"Should suggest 'page' from inner object shape, got {:?}",
prop_labels
);
assert!(
prop_labels.contains(&"total"),
"Should suggest 'total' from inner object shape, got {:?}",
prop_labels
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_object_shape_from_function_return() {
let backend = create_test_backend();
let uri = Url::parse("file:///object_shape_func.php").unwrap();
let text = concat!(
"<?php\n",
"/**\n",
" * @return object{success: bool, message: string}\n",
" */\n",
"function apiCall(): object {\n",
" return (object) [];\n",
"}\n",
"$response = apiCall();\n",
"$response->\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: 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(),
"Should return completions from function return object shape"
);
match result.unwrap() {
CompletionResponse::Array(items) => {
let prop_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::PROPERTY))
.map(|i| i.filter_text.as_deref().unwrap_or(&i.label))
.collect();
assert!(
prop_labels.contains(&"success"),
"Should suggest 'success', got {:?}",
prop_labels
);
assert!(
prop_labels.contains(&"message"),
"Should suggest 'message', got {:?}",
prop_labels
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}
#[tokio::test]
async fn test_object_shape_does_not_break_array_shape() {
let backend = create_test_backend();
let uri = Url::parse("file:///object_shape_no_break.php").unwrap();
let text = concat!(
"<?php\n",
"/** @var array{user: string, count: int} $data */\n",
"$data = getStats();\n",
"$data['\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: 3,
character: 7,
},
},
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(),
"Array shape key completion should still work"
);
match result.unwrap() {
CompletionResponse::Array(items) => {
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(
labels.contains(&"user"),
"Should suggest 'user' array key, got {:?}",
labels
);
assert!(
labels.contains(&"count"),
"Should suggest 'count' array key, got {:?}",
labels
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}