use crate::common::create_test_backend;
use phpantom_lsp::Backend;
use tower_lsp::LanguageServer;
use tower_lsp::lsp_types::*;
async fn complete_at(
backend: &Backend,
uri: &Url,
text: &str,
line: u32,
character: u32,
) -> Vec<CompletionItem> {
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: uri.clone() },
position: Position { line, character },
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: None,
};
match backend.completion(completion_params).await.unwrap() {
Some(CompletionResponse::Array(items)) => items,
Some(CompletionResponse::List(list)) => list.items,
_ => vec![],
}
}
#[test]
fn test_extract_partial_variable_name_simple() {
let content = "<?php\n$user\n";
let result = Backend::extract_partial_variable_name(
content,
Position {
line: 1,
character: 5,
},
);
assert_eq!(result, Some("$user".to_string()));
}
#[test]
fn test_extract_partial_variable_name_partial() {
let content = "<?php\n$us\n";
let result = Backend::extract_partial_variable_name(
content,
Position {
line: 1,
character: 3,
},
);
assert_eq!(result, Some("$us".to_string()));
}
#[test]
fn test_extract_partial_variable_name_bare_dollar() {
let content = "<?php\n$\n";
let result = Backend::extract_partial_variable_name(
content,
Position {
line: 1,
character: 1,
},
);
assert_eq!(
result,
Some("$".to_string()),
"Bare '$' should return Some(\"$\") to trigger showing all variables"
);
}
#[test]
fn test_extract_partial_variable_name_underscore_prefix() {
let content = "<?php\n$_SE\n";
let result = Backend::extract_partial_variable_name(
content,
Position {
line: 1,
character: 4,
},
);
assert_eq!(result, Some("$_SE".to_string()));
}
#[test]
fn test_extract_partial_variable_name_not_a_variable() {
let content = "<?php\nfoo\n";
let result = Backend::extract_partial_variable_name(
content,
Position {
line: 1,
character: 3,
},
);
assert!(
result.is_none(),
"Non-variable identifiers should return None"
);
}
#[test]
fn test_extract_partial_variable_name_class_name() {
let content = "<?php\nMyClass\n";
let result = Backend::extract_partial_variable_name(
content,
Position {
line: 1,
character: 7,
},
);
assert!(result.is_none(), "Class names (no $) should return None");
}
#[test]
fn test_extract_partial_variable_name_variable_variable_skipped() {
let content = "<?php\n$$var\n";
let result = Backend::extract_partial_variable_name(
content,
Position {
line: 1,
character: 5,
},
);
assert!(
result.is_none(),
"Variable variables ($$var) should return None"
);
}
#[test]
fn test_extract_partial_variable_name_after_arrow_returns_none() {
let content = "<?php\n$obj->$prop\n";
let result = Backend::extract_partial_variable_name(
content,
Position {
line: 1,
character: 11,
},
);
assert!(
result.is_none(),
"Variable after '->' should return None (member access context)"
);
}
#[tokio::test]
async fn test_completion_variable_name_basic() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_basic.php").unwrap();
let text = concat!("<?php\n", "$user = new stdClass();\n", "$us\n",);
let items = complete_at(&backend, &uri, text, 2, 3).await;
let var_items: Vec<_> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.collect();
let labels: Vec<&str> = var_items.iter().map(|i| i.label.as_str()).collect();
assert!(
labels.contains(&"$user"),
"Should suggest $user when typing $us. Got: {:?}",
labels
);
}
#[tokio::test]
async fn test_completion_bare_dollar_shows_all_variables() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_dollar.php").unwrap();
let text = concat!(
"<?php\n",
"$name = 'Alice';\n",
"$age = 30;\n",
"$email = 'alice@example.com';\n",
"$\n",
);
let items = complete_at(&backend, &uri, text, 4, 1).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$name"),
"Should suggest $name. Got: {:?}",
var_labels
);
assert!(
var_labels.contains(&"$age"),
"Should suggest $age. Got: {:?}",
var_labels
);
assert!(
var_labels.contains(&"$email"),
"Should suggest $email. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_variable_names_deduplicated() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_dedup.php").unwrap();
let text = concat!(
"<?php\n",
"$user = getUser();\n",
"$user->name;\n",
"echo $user;\n",
"$us\n",
);
let items = complete_at(&backend, &uri, text, 4, 3).await;
let user_items: Vec<_> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE) && i.label == "$user")
.collect();
assert_eq!(
user_items.len(),
1,
"Should have exactly one $user completion (deduplicated). Got: {}",
user_items.len()
);
}
#[tokio::test]
async fn test_completion_superglobals() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_super.php").unwrap();
let text = concat!("<?php\n", "$_GE\n",);
let items = complete_at(&backend, &uri, text, 1, 4).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$_GET"),
"Should suggest $_GET superglobal. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_all_superglobals() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_all_super.php").unwrap();
let text = concat!("<?php\n", "$_\n",);
let items = complete_at(&backend, &uri, text, 1, 2).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
let expected_superglobals = [
"$_GET",
"$_POST",
"$_REQUEST",
"$_SESSION",
"$_COOKIE",
"$_SERVER",
"$_FILES",
"$_ENV",
];
for sg in &expected_superglobals {
assert!(
var_labels.contains(sg),
"Should suggest superglobal {}. Got: {:?}",
sg,
var_labels
);
}
}
#[tokio::test]
async fn test_completion_superglobal_detail() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_sg_detail.php").unwrap();
let text = concat!("<?php\n", "$_POST\n",);
let items = complete_at(&backend, &uri, text, 1, 6).await;
let post = items.iter().find(|i| i.label == "$_POST");
assert!(post.is_some(), "Should find $_POST in completions");
let post = post.unwrap();
assert_eq!(
post.detail.as_deref(),
Some("PHP superglobal"),
"Superglobals should have 'PHP superglobal' as detail"
);
assert!(
post.tags
.as_ref()
.is_some_and(|t| t.contains(&CompletionItemTag::DEPRECATED)),
"Superglobals should be tagged deprecated (grayed out)"
);
}
#[tokio::test]
async fn test_completion_variable_detail() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_detail.php").unwrap();
let text = concat!("<?php\n", "$myVariable = 42;\n", "$myV\n",);
let items = complete_at(&backend, &uri, text, 2, 4).await;
let my_var = items.iter().find(|i| i.label == "$myVariable");
assert!(my_var.is_some(), "Should find $myVariable in completions");
assert_eq!(
my_var.unwrap().detail.as_deref(),
Some("variable"),
"User variables should have 'variable' as detail"
);
}
#[tokio::test]
async fn test_completion_variable_kind() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_kind.php").unwrap();
let text = concat!("<?php\n", "$count = 42;\n", "$cou\n",);
let items = complete_at(&backend, &uri, text, 2, 4).await;
let count_item = items.iter().find(|i| i.label == "$count");
assert!(count_item.is_some(), "Should find $count in completions");
assert_eq!(
count_item.unwrap().kind,
Some(CompletionItemKind::VARIABLE),
"Variable completions should use VARIABLE kind"
);
}
#[tokio::test]
async fn test_completion_superglobals_sort_after_variables() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_sg_sort.php").unwrap();
let text = concat!("<?php\n", "$_GET['name'];\n", "$_myVar = 1;\n", "$_\n",);
let items = complete_at(&backend, &uri, text, 3, 2).await;
let var_items: Vec<_> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.collect();
let my_var = var_items.iter().find(|i| i.label == "$_myVar");
let get_sg = var_items.iter().find(|i| i.label == "$_GET");
assert!(
my_var.is_some(),
"Should find $_myVar. Got: {:?}",
var_items.iter().map(|i| &i.label).collect::<Vec<_>>()
);
assert!(
get_sg.is_some(),
"Should find $_GET. Got: {:?}",
var_items.iter().map(|i| &i.label).collect::<Vec<_>>()
);
let my_var = my_var.unwrap();
let get_sg = get_sg.unwrap();
assert!(
!my_var
.tags
.as_ref()
.is_some_and(|t| t.contains(&CompletionItemTag::DEPRECATED)),
"User-defined variables should not be tagged deprecated"
);
assert!(
get_sg
.tags
.as_ref()
.is_some_and(|t| t.contains(&CompletionItemTag::DEPRECATED)),
"Superglobals should be tagged deprecated (grayed out)"
);
assert!(
my_var.sort_text.as_deref().unwrap() < get_sg.sort_text.as_deref().unwrap(),
"User variables (sort_text={:?}) should sort before superglobals (sort_text={:?})",
my_var.sort_text,
get_sg.sort_text
);
}
#[tokio::test]
async fn test_completion_variable_uses_text_edit_with_dollar() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_insert.php").unwrap();
let text = concat!("<?php\n", "$result = compute();\n", "$res\n",);
let items = complete_at(&backend, &uri, text, 2, 4).await;
let result_item = items.iter().find(|i| i.label == "$result");
assert!(result_item.is_some(), "Should find $result in completions");
let item = result_item.unwrap();
match &item.text_edit {
Some(CompletionTextEdit::Edit(te)) => {
assert_eq!(
te.new_text, "$result",
"text_edit new_text should be $result"
);
assert_eq!(te.range.start, Position::new(2, 0));
assert_eq!(te.range.end, Position::new(2, 4));
}
other => panic!("Expected text_edit with Edit variant, got: {:?}", other),
}
}
#[tokio::test]
async fn test_completion_variable_from_function_params() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_params.php").unwrap();
let text = concat!(
"<?php\n",
"function greet(string $firstName, string $lastName): string {\n",
" return $fir\n",
"}\n",
);
let items = complete_at(&backend, &uri, text, 2, 15).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$firstName"),
"Should suggest $firstName from function params. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_variable_from_method_params() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_method_params.php").unwrap();
let text = concat!(
"<?php\n",
"class UserService {\n",
" public function findUser(int $userId, string $role): void {\n",
" $user\n",
" }\n",
"}\n",
);
let items = complete_at(&backend, &uri, text, 3, 13).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$userId"),
"Should suggest $userId from method params. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_variable_from_later_in_file() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_later.php").unwrap();
let text = concat!("<?php\n", "$ear\n", "$earlyVar = 1;\n", "$laterVar = 2;\n",);
let items = complete_at(&backend, &uri, text, 1, 4).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
!var_labels.contains(&"$earlyVar"),
"$earlyVar is defined after the cursor and should NOT be suggested. Got: {:?}",
var_labels
);
assert!(
!var_labels.contains(&"$laterVar"),
"$laterVar is defined after the cursor and should NOT be suggested. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_variable_defined_before_cursor() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_before.php").unwrap();
let text = concat!("<?php\n", "$earlyVar = 1;\n", "$laterVar = 2;\n", "$ear\n",);
let items = complete_at(&backend, &uri, text, 3, 4).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$earlyVar"),
"Should suggest $earlyVar (defined before cursor). Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_variable_far_below_cursor_not_suggested() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_far_below.php").unwrap();
let mut text = String::from("<?php\n$amb\n");
for _ in 0..100 {
text.push_str("// filler line\n");
}
text.push_str("$ambiguous = new stdClass();\n");
let items = complete_at(&backend, &uri, &text, 1, 4).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
!var_labels.contains(&"$ambiguous"),
"$ambiguous is defined far below the cursor and should NOT be suggested. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_excludes_variable_at_cursor() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_exclude.php").unwrap();
let text = concat!("<?php\n", "$uniqueTestVar\n",);
let items = complete_at(&backend, &uri, text, 1, 14).await;
let self_items: Vec<_> = items
.iter()
.filter(|i| i.label == "$uniqueTestVar")
.collect();
assert!(
self_items.is_empty(),
"Should NOT suggest the variable being typed at the cursor. Got: {:?}",
self_items.iter().map(|i| &i.label).collect::<Vec<_>>()
);
}
#[tokio::test]
async fn test_completion_variable_not_after_arrow() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_no_arrow.php").unwrap();
let text = concat!(
"<?php\n",
"class Foo { public string $name; }\n",
"$foo = new Foo();\n",
"$foo->na\n",
);
let items = complete_at(&backend, &uri, text, 3, 8).await;
let standalone_var_items: Vec<_> = items
.iter()
.filter(|i| {
i.kind == Some(CompletionItemKind::VARIABLE) && i.detail.as_deref() == Some("variable")
})
.collect();
assert!(
standalone_var_items.is_empty(),
"Standalone variable names should not appear after '->'. Got: {:?}",
standalone_var_items
.iter()
.map(|i| &i.label)
.collect::<Vec<_>>()
);
}
#[tokio::test]
async fn test_completion_multiple_matching_variables() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_multi.php").unwrap();
let text = concat!(
"<?php\n",
"$userData = [];\n",
"$userName = 'Alice';\n",
"$userEmail = 'alice@test.com';\n",
"$userAge = 30;\n",
"$user\n",
);
let items = complete_at(&backend, &uri, text, 5, 5).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$userData"),
"Should suggest $userData. Got: {:?}",
var_labels
);
assert!(
var_labels.contains(&"$userName"),
"Should suggest $userName. Got: {:?}",
var_labels
);
assert!(
var_labels.contains(&"$userEmail"),
"Should suggest $userEmail. Got: {:?}",
var_labels
);
assert!(
var_labels.contains(&"$userAge"),
"Should suggest $userAge. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_this_inside_method() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_this.php").unwrap();
let text = concat!(
"<?php\n",
"class MyClass {\n",
" public function doSomething(): void {\n",
" $th\n",
" }\n",
"}\n",
);
let items = complete_at(&backend, &uri, text, 3, 11).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$this"),
"Should suggest $this inside a class method (built-in). Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_variable_from_foreach() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_foreach.php").unwrap();
let text = concat!(
"<?php\n",
"$items = [1, 2, 3];\n",
"foreach ($items as $key => $value) {\n",
" echo $val\n",
"}\n",
);
let items = complete_at(&backend, &uri, text, 3, 13).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$value"),
"Should suggest $value from foreach. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_variable_from_foreach_key() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_foreach_key.php").unwrap();
let text = concat!(
"<?php\n",
"$items = [1, 2, 3];\n",
"foreach ($items as $key => $value) {\n",
" echo $ke\n",
"}\n",
);
let items = complete_at(&backend, &uri, text, 3, 12).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$key"),
"Should suggest $key from foreach. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_variable_from_catch() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_catch.php").unwrap();
let text = concat!(
"<?php\n",
"try {\n",
" riskyOperation();\n",
"} catch (Exception $exception) {\n",
" echo $exc\n",
"}\n",
);
let items = complete_at(&backend, &uri, text, 4, 13).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$exception"),
"Should suggest $exception from catch block. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_globals_superglobal() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_globals.php").unwrap();
let text = concat!("<?php\n", "$GL\n",);
let items = complete_at(&backend, &uri, text, 1, 3).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$GLOBALS"),
"Should suggest $GLOBALS superglobal. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_argc_argv() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_cli.php").unwrap();
let text = concat!("<?php\n", "$arg\n",);
let items = complete_at(&backend, &uri, text, 1, 4).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$argc"),
"Should suggest $argc. Got: {:?}",
var_labels
);
assert!(
var_labels.contains(&"$argv"),
"Should suggest $argv. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_variable_inside_if_block() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_if.php").unwrap();
let text = concat!(
"<?php\n",
"$config = loadConfig();\n",
"$connection = null;\n",
"if ($config) {\n",
" $con\n",
"}\n",
);
let items = complete_at(&backend, &uri, text, 4, 8).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$config"),
"Should suggest $config. Got: {:?}",
var_labels
);
assert!(
var_labels.contains(&"$connection"),
"Should suggest $connection. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_no_variable_for_classname() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_no_class.php").unwrap();
let text = concat!("<?php\n", "MyClass\n",);
let items = complete_at(&backend, &uri, text, 1, 7).await;
let var_items: Vec<_> = items
.iter()
.filter(|i| {
i.kind == Some(CompletionItemKind::VARIABLE) && i.detail.as_deref() == Some("variable")
})
.collect();
assert!(
var_items.is_empty(),
"Class name identifiers should not produce variable completions. Got: {:?}",
var_items.iter().map(|i| &i.label).collect::<Vec<_>>()
);
}
#[tokio::test]
async fn test_completion_variable_with_underscores() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_underscore.php").unwrap();
let text = concat!("<?php\n", "$my_long_variable_name = 'hello';\n", "$my_lo\n",);
let items = complete_at(&backend, &uri, text, 2, 6).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$my_long_variable_name"),
"Should suggest $my_long_variable_name. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_variable_case_insensitive() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_case.php").unwrap();
let text = concat!("<?php\n", "$MyVariable = 42;\n", "$myv\n",);
let items = complete_at(&backend, &uri, text, 2, 4).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$MyVariable"),
"Should suggest $MyVariable (case-insensitive match). Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_variable_in_closure() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_closure.php").unwrap();
let text = concat!(
"<?php\n",
"$outerVar = 'hello';\n",
"$callback = function() use ($outerVar) {\n",
" echo $outer\n",
"};\n",
);
let items = complete_at(&backend, &uri, text, 3, 16).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$outerVar"),
"Should suggest $outerVar in closure. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_no_matching_variables() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_no_match.php").unwrap();
let text = concat!(
"<?php\n",
"$apple = 1;\n",
"$banana = 2;\n",
"$zzz_unique_prefix_xyz\n",
);
let items = complete_at(&backend, &uri, text, 3, 22).await;
let var_items: Vec<_> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE) && i.label.starts_with("$zzz"))
.collect();
assert!(
var_items.is_empty(),
"Should not suggest variables with no match. Got: {:?}",
var_items.iter().map(|i| &i.label).collect::<Vec<_>>()
);
}
#[tokio::test]
async fn test_completion_this_not_visible_at_top_level() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_this_scope.php").unwrap();
let text = concat!(
"<?php\n",
"class Foo {\n",
" public function bar(): void {\n",
" $this->doSomething();\n",
" }\n",
"}\n",
"$th\n",
);
let items = complete_at(&backend, &uri, text, 6, 3).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
!var_labels.contains(&"$this"),
"$this should NOT appear in top-level scope. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_this_not_in_static_method() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_this_static.php").unwrap();
let text = concat!(
"<?php\n",
"class Foo {\n",
" public static function create(): void {\n",
" $th\n",
" }\n",
"}\n",
);
let items = complete_at(&backend, &uri, text, 3, 11).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
!var_labels.contains(&"$this"),
"$this should NOT appear inside a static method. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_variables_scoped_to_method() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_method_scope.php").unwrap();
let text = concat!(
"<?php\n",
"class Foo {\n",
" public function first(): void {\n",
" $onlyInFirst = 1;\n",
" }\n",
" public function second(): void {\n",
" $on\n",
" }\n",
"}\n",
);
let items = complete_at(&backend, &uri, text, 6, 11).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
!var_labels.contains(&"$onlyInFirst"),
"$onlyInFirst should NOT appear in second(). Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_params_scoped_to_method() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_param_scope.php").unwrap();
let text = concat!(
"<?php\n",
"class Foo {\n",
" public function doWork(string $taskName, int $priority): void {\n",
" echo $taskName;\n",
" }\n",
"}\n",
"$ta\n",
);
let items = complete_at(&backend, &uri, text, 6, 3).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
!var_labels.contains(&"$taskName"),
"$taskName should NOT appear outside its method. Got: {:?}",
var_labels
);
assert!(
!var_labels.contains(&"$priority"),
"$priority should NOT appear outside its method. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_properties_not_listed_as_variables() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_no_props.php").unwrap();
let text = concat!(
"<?php\n",
"class Post {\n",
" public string $title;\n",
" protected ?string $createdAt = null;\n",
" private int $views = 0;\n",
" public function render(): void {\n",
" $cr\n",
" }\n",
"}\n",
);
let items = complete_at(&backend, &uri, text, 6, 11).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
!var_labels.contains(&"$createdAt"),
"Properties should NOT appear as variable completions. Got: {:?}",
var_labels
);
assert!(
!var_labels.contains(&"$title"),
"Properties should NOT appear as variable completions. Got: {:?}",
var_labels
);
assert!(
!var_labels.contains(&"$views"),
"Properties should NOT appear as variable completions. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_variables_from_function_not_at_top_level() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_far_scope.php").unwrap();
let text = concat!(
"<?php\n",
"$a\n",
"function farAway(): void {\n",
" $aDistantVariable = 42;\n",
"}\n",
);
let items = complete_at(&backend, &uri, text, 1, 2).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
!var_labels.contains(&"$aDistantVariable"),
"Variables inside a function should NOT appear at top level. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_variable_used_in_different_contexts() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_contexts.php").unwrap();
let text = concat!(
"<?php\n",
"function process(array $data): void {\n",
" $result = transform($data);\n",
" if ($result !== null) {\n",
" save($result);\n",
" }\n",
" $res\n",
"}\n",
);
let items = complete_at(&backend, &uri, text, 6, 8).await;
let result_items: Vec<_> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE) && i.label == "$result")
.collect();
assert_eq!(
result_items.len(),
1,
"$result should appear exactly once despite multiple uses. Got: {}",
result_items.len()
);
}
#[tokio::test]
async fn test_completion_foreach_variable_not_visible_after_loop() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_foreach_scope.php").unwrap();
let text = concat!(
"<?php\n",
"$items = [1, 2, 3];\n",
"foreach ($items as $key => $value) {\n",
" echo $value;\n",
"}\n",
"$\n",
);
let items = complete_at(&backend, &uri, text, 5, 1).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
!var_labels.contains(&"$value"),
"$value should NOT be visible after the foreach loop. Got: {:?}",
var_labels
);
assert!(
!var_labels.contains(&"$key"),
"$key should NOT be visible after the foreach loop. Got: {:?}",
var_labels
);
assert!(
var_labels.contains(&"$items"),
"$items should still be visible after the foreach loop. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_foreach_variable_not_visible_before_loop() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_foreach_before.php").unwrap();
let text = concat!(
"<?php\n",
"$items = [1, 2, 3];\n",
"$\n",
"foreach ($items as $value) {\n",
" echo $value;\n",
"}\n",
);
let items = complete_at(&backend, &uri, text, 2, 1).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
!var_labels.contains(&"$value"),
"$value should NOT be visible before the foreach loop. Got: {:?}",
var_labels
);
assert!(
var_labels.contains(&"$items"),
"$items should be visible before the foreach loop. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_foreach_variable_visible_inside_loop() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_foreach_inside.php").unwrap();
let text = concat!(
"<?php\n",
"$items = [1, 2, 3];\n",
"foreach ($items as $key => $value) {\n",
" $\n",
"}\n",
);
let items = complete_at(&backend, &uri, text, 3, 5).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$value"),
"$value should be visible inside the foreach loop. Got: {:?}",
var_labels
);
assert!(
var_labels.contains(&"$key"),
"$key should be visible inside the foreach loop. Got: {:?}",
var_labels
);
assert!(
var_labels.contains(&"$items"),
"$items should be visible inside the foreach loop. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_superglobal_not_duplicated() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_sg_dedup.php").unwrap();
let text = concat!("<?php\n", "$name = $_GET['name'];\n", "$_G\n",);
let items = complete_at(&backend, &uri, text, 2, 3).await;
let get_items: Vec<_> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE) && i.label == "$_GET")
.collect();
assert_eq!(
get_items.len(),
1,
"$_GET should appear exactly once even though it's both in the file and in superglobals. Got: {}",
get_items.len()
);
}
#[tokio::test]
async fn test_completion_variables_visible_at_eof_no_newline() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_eof_no_nl.php").unwrap();
let text = concat!(
"<?php\n",
"$items = [1, 2, 3];\n",
"$name = 'hello';\n",
"$",
);
let items = complete_at(&backend, &uri, text, 3, 1).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$items"),
"$items should be visible at end of file. Got: {:?}",
var_labels
);
assert!(
var_labels.contains(&"$name"),
"$name should be visible at end of file. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_variables_visible_at_eof_with_newline() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_eof_nl.php").unwrap();
let text = concat!(
"<?php\n",
"$items = [1, 2, 3];\n",
"$name = 'hello';\n",
"$\n",
);
let items = complete_at(&backend, &uri, text, 3, 1).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$items"),
"$items should be visible at end of file (trailing newline). Got: {:?}",
var_labels
);
assert!(
var_labels.contains(&"$name"),
"$name should be visible at end of file (trailing newline). Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_variables_visible_at_end_of_method() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_eof_method.php").unwrap();
let text = concat!(
"<?php\n",
"class Foo {\n",
" public function bar() {\n",
" $items = [1, 2, 3];\n",
" $\n",
" }\n",
"}\n",
);
let items = complete_at(&backend, &uri, text, 4, 9).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$items"),
"$items should be visible at end of method body. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_variables_visible_after_foreach_at_eof() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_after_foreach_eof.php").unwrap();
let text = concat!(
"<?php\n",
"$items = [1, 2, 3];\n",
"foreach ($items as $item) {\n",
" echo $item;\n",
"}\n",
"$\n",
);
let items = complete_at(&backend, &uri, text, 5, 1).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$items"),
"$items should be visible after foreach at end of file. Got: {:?}",
var_labels
);
assert!(
!var_labels.contains(&"$item"),
"$item should NOT leak out of foreach. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_variables_at_eof_after_class() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_eof_class.php").unwrap();
let text = concat!(
"<?php\n",
"class Foo {\n",
" public function bar(): void {}\n",
"}\n",
"$items = [1, 2, 3];\n",
"$\n",
);
let items = complete_at(&backend, &uri, text, 5, 1).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$items"),
"$items should be visible after class at EOF. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_variables_at_eof_after_function_decl() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_eof_func.php").unwrap();
let text = concat!(
"<?php\n",
"function helper(): mixed { return null; }\n",
"$items = [1, 2, 3];\n",
"$\n",
);
let items = complete_at(&backend, &uri, text, 3, 1).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$items"),
"$items should be visible after function decl at EOF. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_variables_at_eof_class_function_foreach() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_eof_cls_fn_fe.php").unwrap();
let text = concat!(
"<?php\n",
"class User {\n",
" public string $name;\n",
"}\n",
"function helper(): mixed { return null; }\n",
"$items = [1, 2, 3];\n",
"foreach ($items as $item) {\n",
" echo $item;\n",
"}\n",
"$\n",
);
let items = complete_at(&backend, &uri, text, 9, 1).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$items"),
"$items should be visible after foreach at EOF with class+function above. Got: {:?}",
var_labels
);
assert!(
!var_labels.contains(&"$item"),
"$item should NOT leak out of foreach. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_variables_after_foreach_with_classes_above() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_classes_foreach_eof.php").unwrap();
let text = concat!(
"<?php\n",
"class User {\n",
" public string $name;\n",
" public function getEmail(): string {}\n",
"}\n",
"class AdminUser extends User {\n",
" public function grantPermission(string $p): void {}\n",
"}\n",
"function getUnknownValue(): mixed { return null; }\n",
"\n",
"/** @var list<User> $users */\n",
"$users = getUnknownValue();\n",
"foreach ($users as $user) {\n",
" $user->getEmail();\n",
"}\n",
"\n",
"/** @var User[] $members */\n",
"$members = getUnknownValue();\n",
"foreach ($members as $member) {\n",
" $member->getEmail();\n",
"}\n",
"\n",
"/** @var array<int, AdminUser> $admins */\n",
"$admins = getUnknownValue();\n",
"foreach ($admins as $admin) {\n",
" $admin->grantPermission('x');\n",
"}\n",
"$\n",
);
let items = complete_at(&backend, &uri, text, 27, 1).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$users"),
"$users should be visible after all foreach loops. Got: {:?}",
var_labels
);
assert!(
var_labels.contains(&"$members"),
"$members should be visible after all foreach loops. Got: {:?}",
var_labels
);
assert!(
var_labels.contains(&"$admins"),
"$admins should be visible after all foreach loops. Got: {:?}",
var_labels
);
assert!(
!var_labels.contains(&"$user"),
"$user should NOT leak out of foreach. Got: {:?}",
var_labels
);
assert!(
!var_labels.contains(&"$member"),
"$member should NOT leak out of foreach. Got: {:?}",
var_labels
);
assert!(
!var_labels.contains(&"$admin"),
"$admin should NOT leak out of foreach. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_variables_visible_when_cursor_past_last_line() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_past_eof.php").unwrap();
let text = concat!(
"<?php\n",
"$items = [1, 2, 3];\n",
"$name = 'hello';\n",
"$\n",
);
let items_on_dollar = complete_at(&backend, &uri, text, 3, 1).await;
let var_labels: Vec<&str> = items_on_dollar
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$items"),
"$items should be visible on the $ line. Got: {:?}",
var_labels
);
assert!(
var_labels.contains(&"$name"),
"$name should be visible on the $ line. Got: {:?}",
var_labels
);
let uri2 = Url::parse("file:///var_past_eof2.php").unwrap();
let items_past_end = complete_at(&backend, &uri2, text, 4, 0).await;
let var_labels_past: Vec<&str> = items_past_end
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels_past.contains(&"$items"),
"$items should be visible when cursor is past end of file. Got: {:?}",
var_labels_past
);
assert!(
var_labels_past.contains(&"$name"),
"$name should be visible when cursor is past end of file. Got: {:?}",
var_labels_past
);
}
#[tokio::test]
async fn test_completion_variables_at_eof_real_file_scenario() {
let backend = create_test_backend();
let uri = Url::parse("file:///eof_real.php").unwrap();
let text = concat!(
"<?php\n",
"class User {\n",
" public string $name;\n",
" public string $email;\n",
" public function __construct(string $name, string $email) {\n",
" $this->name = $name;\n",
" $this->email = $email;\n",
" }\n",
" public function getEmail(): string { return $this->email; }\n",
" public function getName(): string { return $this->name; }\n",
"}\n",
"class AdminUser extends User {\n",
" public function grantPermission(string $p): void {}\n",
"}\n",
"function getUnknownValue(): mixed { return null; }\n",
"function findOrFail(int $id): User|AdminUser { return new User('a','b'); }\n",
"\n",
"$found = findOrFail(1);\n",
"$found->getName();\n",
"\n",
"if (rand(0, 1)) {\n",
" $ambiguous = new User('x', 'x@x.com');\n",
"} else {\n",
" $ambiguous = new AdminUser('y', 'y@y.com');\n",
"}\n",
"$ambiguous->getName();\n",
"\n",
"$a = findOrFail(1);\n",
"if ($a instanceof AdminUser) {\n",
" $a->grantPermission('x');\n",
"}\n",
"\n",
"/** @var list<User> $users */\n",
"$users = getUnknownValue();\n",
"foreach ($users as $user) {\n",
" $user->getEmail();\n",
"}\n",
"\n",
"/** @var array<int, AdminUser> $admins */\n",
"$admins = getUnknownValue();\n",
"foreach ($admins as $admin) {\n",
" $admin->grantPermission('x');\n",
"}\n",
"$\n",
);
let line_count = text.lines().count() as u32;
let items = complete_at(&backend, &uri, text, line_count - 1, 1).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$found"),
"$found should be visible at EOF. Got: {:?}",
var_labels
);
assert!(
var_labels.contains(&"$ambiguous"),
"$ambiguous should be visible at EOF. Got: {:?}",
var_labels
);
assert!(
var_labels.contains(&"$a"),
"$a should be visible at EOF. Got: {:?}",
var_labels
);
assert!(
var_labels.contains(&"$users"),
"$users should be visible at EOF. Got: {:?}",
var_labels
);
assert!(
var_labels.contains(&"$admins"),
"$admins should be visible at EOF. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_variables_at_eof_of_braced_namespace() {
let backend = create_test_backend();
let uri = Url::parse("file:///braced_ns_eof.php").unwrap();
let text = r#"<?php
namespace Demo {
class User {
public string $email;
public function getEmail(): string { return ''; }
public function getName(): string { return ''; }
}
class AdminUser extends User {
public function grantPermission(string $perm): void {}
}
$found = new User();
$users = [new User()];
$admins = [new AdminUser()];
$
} // end namespace Demo
namespace Other {
class Helper {}
}
"#;
let items = complete_at(&backend, &uri, text, 17, 1).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$found"),
"$found should be visible at end of braced namespace. Got: {:?}",
var_labels
);
assert!(
var_labels.contains(&"$users"),
"$users should be visible at end of braced namespace. Got: {:?}",
var_labels
);
assert!(
var_labels.contains(&"$admins"),
"$admins should be visible at end of braced namespace. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_variables_at_eof_inside_namespace() {
let backend = create_test_backend();
let uri = Url::parse("file:///ns_eof.php").unwrap();
let text = concat!(
"<?php\n",
"namespace App;\n",
"\n",
"class User {\n",
" public function getEmail(): string { return ''; }\n",
"}\n",
"\n",
"function getUnknownValue(): mixed { return null; }\n",
"\n",
"$found = new User();\n",
"$name = 'hello';\n",
"\n",
"/** @var list<User> $users */\n",
"$users = getUnknownValue();\n",
"foreach ($users as $user) {\n",
" $user->getEmail();\n",
"}\n",
"$\n",
);
let line_count = text.lines().count() as u32;
let dollar_line = line_count - 1;
let items = complete_at(&backend, &uri, text, dollar_line, 1).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$found"),
"$found should be visible at EOF inside namespace. Got: {:?}",
var_labels
);
assert!(
var_labels.contains(&"$name"),
"$name should be visible at EOF inside namespace. Got: {:?}",
var_labels
);
assert!(
var_labels.contains(&"$users"),
"$users should be visible at EOF inside namespace. Got: {:?}",
var_labels
);
assert!(
!var_labels.contains(&"$user"),
"$user should NOT leak out of foreach. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_var_docblock_variable_name_suggested() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_docblock_varname.php").unwrap();
let text = concat!(
"<?php\n",
"$existing = 'hello';\n",
"/** @var AdminUser $test */\n",
"$test = getUnknownValue();\n",
"$t\n",
);
let items = complete_at(&backend, &uri, text, 4, 2).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$test"),
"$test should be suggested from @var docblock. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_var_docblock_variable_name_before_assignment() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_docblock_before_assign.php").unwrap();
let text = concat!(
"<?php\n",
"/** @var AdminUser $adminUser */\n",
"$x = getUnknownValue();\n",
"$a\n",
);
let items = complete_at(&backend, &uri, text, 3, 2).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$adminUser"),
"$adminUser should be suggested from @var docblock. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_var_docblock_variable_name_in_method() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_docblock_method.php").unwrap();
let text = concat!(
"<?php\n",
"class Foo {\n",
" public function bar(): void {\n",
" $known = 1;\n",
" /** @var \\App\\User $myUser */\n",
" $m\n",
" }\n",
"}\n",
);
let items = complete_at(&backend, &uri, text, 5, 10).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$myUser"),
"$myUser should be suggested from @var docblock inside method. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_var_docblock_without_name_no_phantom_variable() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_docblock_no_name.php").unwrap();
let text = concat!(
"<?php\n",
"/** @var string */\n",
"$val = getValue();\n",
"$\n",
);
let items = complete_at(&backend, &uri, text, 3, 1).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$val"),
"$val should be suggested. Got: {:?}",
var_labels
);
for label in &var_labels {
assert!(
label.len() > 1,
"Should not have a bare '$' entry. Got: {:?}",
var_labels
);
}
}
#[tokio::test]
async fn test_completion_arrow_function_param_visible_in_body() {
let backend = create_test_backend();
let uri = Url::parse("file:///arrow_param_basic.php").unwrap();
let text = concat!(
"<?php\n", "/** @var list<string> */\n", "$images = [];\n", "array_map(fn(string $image): string => $im, $images);\n", );
let items = complete_at(&backend, &uri, text, 3, 42).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$image"),
"Arrow function parameter $image should be visible inside the body. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_arrow_function_outer_scope_visible_in_body() {
let backend = create_test_backend();
let uri = Url::parse("file:///arrow_outer_scope.php").unwrap();
let text = concat!(
"<?php\n", "$outerVar = 'hello';\n", "array_map(fn($x) => $out, []);\n", );
let items = complete_at(&backend, &uri, text, 2, 24).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$outerVar"),
"Outer-scope $outerVar should be visible inside arrow function body. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_arrow_function_both_param_and_outer_visible() {
let backend = create_test_backend();
{
let uri = Url::parse("file:///arrow_both_scopes_a.php").unwrap();
let text = concat!(
"<?php\n", "$outerVar = 'hello';\n", "array_map(fn(string $image) => $im, $outerVar);\n", );
let items = complete_at(&backend, &uri, text, 2, 34).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$image"),
"Arrow function parameter $image should be visible (probe 1). Got: {:?}",
var_labels
);
}
{
let uri = Url::parse("file:///arrow_both_scopes_b.php").unwrap();
let text = concat!(
"<?php\n", "$outerVar = 'hello';\n", "array_map(fn(string $image) => $out, $outerVar);\n", );
let items = complete_at(&backend, &uri, text, 2, 35).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$outerVar"),
"Outer-scope $outerVar should also be visible inside arrow body (probe 2). Got: {:?}",
var_labels
);
}
}
#[tokio::test]
async fn test_completion_arrow_function_param_visible_inside_method() {
let backend = create_test_backend();
let uri = Url::parse("file:///arrow_in_method.php").unwrap();
let text = concat!(
"<?php\n", "class Demo {\n", " public function run(): void {\n", " $items = [];\n", " array_map(fn(string $item): string => $ite, $items);\n", " }\n", "}\n", );
let items = complete_at(&backend, &uri, text, 4, 50).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$item"),
"Arrow function parameter $item should be visible inside method body. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_closure_in_return_isolates_outer_scope() {
let backend = create_test_backend();
{
let uri = Url::parse("file:///closure_isolation_a.php").unwrap();
let text = concat!(
"<?php\n", "class Mapper {\n", " public function run(): array {\n", " $keywords = [];\n", " $brands = [];\n", " return array_map(function (string $brand) use ($keywords): array {\n", " $bra\n", " }, $brands);\n", " }\n", "}\n", );
let items = complete_at(&backend, &uri, text, 6, 16).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$brand"),
"Closure parameter $brand should be visible. Got: {:?}",
var_labels
);
assert!(
!var_labels.contains(&"$brands"),
"Outer $brands should NOT leak into closure scope. Got: {:?}",
var_labels
);
}
{
let uri = Url::parse("file:///closure_isolation_b.php").unwrap();
let text = concat!(
"<?php\n", "class Mapper {\n", " public function run(): array {\n", " $keywords = [];\n", " $brands = [];\n", " return array_map(function (string $brand) use ($keywords): array {\n", " $key\n", " }, $brands);\n", " }\n", "}\n", );
let items = complete_at(&backend, &uri, text, 6, 16).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$keywords"),
"use-captured $keywords should be visible inside closure. Got: {:?}",
var_labels
);
}
}
#[tokio::test]
async fn test_completion_closure_body_variable_visible() {
let backend = create_test_backend();
let uri = Url::parse("file:///closure_body_var.php").unwrap();
let text = concat!(
"<?php\n", "class Mapper {\n", " public function run(): array {\n", " $outer = 'x';\n", " return array_map(function (string $brand): array {\n", " $local = 'y';\n", " $loc\n", " }, []);\n", " }\n", "}\n", );
let items = complete_at(&backend, &uri, text, 6, 16).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$local"),
"Variable assigned inside closure body should be visible. Got: {:?}",
var_labels
);
assert!(
!var_labels.contains(&"$outer"),
"Outer method local $outer should NOT be visible (not captured). Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_closure_this_visible_in_instance_method() {
let backend = create_test_backend();
{
let uri = Url::parse("file:///closure_this_a.php").unwrap();
let text = concat!(
"<?php\n", "class Processor {\n", " public function process(): array {\n", " $outer = 1;\n", " return array_map(function (string $item): string {\n", " $thi\n", " }, []);\n", " }\n", "}\n", );
let items = complete_at(&backend, &uri, text, 5, 16).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$this"),
"$this should be visible inside a closure in an instance method. Got: {:?}",
var_labels
);
}
{
let uri = Url::parse("file:///closure_this_b.php").unwrap();
let text = concat!(
"<?php\n", "class Processor {\n", " public function process(): array {\n", " $outer = 1;\n", " return array_map(function (string $item): string {\n", " $out\n", " }, []);\n", " }\n", "}\n", );
let items = complete_at(&backend, &uri, text, 5, 16).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
!var_labels.contains(&"$outer"),
"Outer local $outer should NOT leak into closure scope. Got: {:?}",
var_labels
);
}
}
#[tokio::test]
async fn test_completion_closure_inside_if_condition() {
let backend = create_test_backend();
let uri = Url::parse("file:///closure_if_cond.php").unwrap();
let text = concat!(
"<?php\n", "function test_if_cond() {\n", " $outer = 1;\n", " if ($fn = function() {\n", " $inner = 2;\n", " $\n", " }) {\n", " echo 'yes';\n", " }\n", "}\n", );
let items = complete_at(&backend, &uri, text, 5, 9).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$inner"),
"$inner should be visible inside closure in if condition. Got: {:?}",
var_labels
);
assert!(
!var_labels.contains(&"$outer"),
"$outer should NOT leak into closure in if condition. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_closure_inside_foreach_body() {
let backend = create_test_backend();
let uri = Url::parse("file:///closure_foreach.php").unwrap();
let text = concat!(
"<?php\n", "function test_foreach() {\n", " $outer = 1;\n", " foreach ([1,2] as $item) {\n", " $cb = function() {\n", " $local = 'x';\n", " $\n", " };\n", " }\n", "}\n", );
let items = complete_at(&backend, &uri, text, 6, 13).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$local"),
"$local should be visible inside closure in foreach. Got: {:?}",
var_labels
);
assert!(
!var_labels.contains(&"$outer"),
"$outer should NOT leak into closure in foreach. Got: {:?}",
var_labels
);
assert!(
!var_labels.contains(&"$item"),
"$item should NOT leak into closure in foreach. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_closure_inside_for_loop() {
let backend = create_test_backend();
let uri = Url::parse("file:///closure_for.php").unwrap();
let text = concat!(
"<?php\n", "function test_for() {\n", " $outer = 1;\n", " for ($i = 0; $i < 10; $i++) {\n", " $cb = function() {\n", " $inner = 99;\n", " $\n", " };\n", " }\n", "}\n", );
let items = complete_at(&backend, &uri, text, 6, 13).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$inner"),
"$inner should be visible inside closure in for loop. Got: {:?}",
var_labels
);
assert!(
!var_labels.contains(&"$outer"),
"$outer should NOT leak into closure in for loop. Got: {:?}",
var_labels
);
assert!(
!var_labels.contains(&"$i"),
"$i should NOT leak into closure in for loop. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_closure_inside_while_loop() {
let backend = create_test_backend();
let uri = Url::parse("file:///closure_while.php").unwrap();
let text = concat!(
"<?php\n", "function test_while() {\n", " $outer = 1;\n", " while (true) {\n", " $cb = function() {\n", " $wvar = 'w';\n", " $\n", " };\n", " }\n", "}\n", );
let items = complete_at(&backend, &uri, text, 6, 13).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$wvar"),
"$wvar should be visible inside closure in while. Got: {:?}",
var_labels
);
assert!(
!var_labels.contains(&"$outer"),
"$outer should NOT leak into closure in while. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_closure_inside_do_while() {
let backend = create_test_backend();
let uri = Url::parse("file:///closure_dowhile.php").unwrap();
let text = concat!(
"<?php\n", "function test_dowhile() {\n", " $outer = 1;\n", " do {\n", " $cb = function() {\n", " $dwvar = 'd';\n", " $\n", " };\n", " } while (true);\n", "}\n", );
let items = complete_at(&backend, &uri, text, 6, 13).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$dwvar"),
"$dwvar should be visible inside closure in do-while. Got: {:?}",
var_labels
);
assert!(
!var_labels.contains(&"$outer"),
"$outer should NOT leak into closure in do-while. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_closure_inside_try_block() {
let backend = create_test_backend();
let uri = Url::parse("file:///closure_try.php").unwrap();
let text = concat!(
"<?php\n", "function test_try() {\n", " $outer = 1;\n", " try {\n", " $cb = function() {\n", " $tryvar = 't';\n", " $\n", " };\n", " } catch (\\Exception $e) {\n", " }\n", "}\n", );
let items = complete_at(&backend, &uri, text, 6, 13).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$tryvar"),
"$tryvar should be visible inside closure in try. Got: {:?}",
var_labels
);
assert!(
!var_labels.contains(&"$outer"),
"$outer should NOT leak into closure in try. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_closure_inside_catch_block() {
let backend = create_test_backend();
let uri = Url::parse("file:///closure_catch.php").unwrap();
let text = concat!(
"<?php\n", "function test_catch() {\n", " $outer = 1;\n", " try {\n", " throw new \\Exception();\n", " } catch (\\Exception $e) {\n", " $cb = function() {\n", " $catchvar = 'c';\n", " $\n", " };\n", " }\n", "}\n", );
let items = complete_at(&backend, &uri, text, 8, 13).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$catchvar"),
"$catchvar should be visible inside closure in catch. Got: {:?}",
var_labels
);
assert!(
!var_labels.contains(&"$e"),
"Catch variable $e should NOT leak into closure. Got: {:?}",
var_labels
);
assert!(
!var_labels.contains(&"$outer"),
"$outer should NOT leak into closure in catch. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_closure_inside_finally_block() {
let backend = create_test_backend();
let uri = Url::parse("file:///closure_finally.php").unwrap();
let text = concat!(
"<?php\n", "function test_finally() {\n", " $outer = 1;\n", " try {\n", " echo 'try';\n", " } catch (\\Exception $e) {\n", " echo 'catch';\n", " } finally {\n", " $cb = function() {\n", " $finvar = 'f';\n", " $\n", " };\n", " }\n", "}\n", );
let items = complete_at(&backend, &uri, text, 10, 13).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$finvar"),
"$finvar should be visible inside closure in finally. Got: {:?}",
var_labels
);
assert!(
!var_labels.contains(&"$outer"),
"$outer should NOT leak into closure in finally. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_closure_inside_switch_case() {
let backend = create_test_backend();
let uri = Url::parse("file:///closure_switch.php").unwrap();
let text = concat!(
"<?php\n", "function test_switch() {\n", " $outer = 1;\n", " switch ($outer) {\n", " case 1:\n", " $cb = function() {\n", " $swvar = 's';\n", " $\n", " };\n", " break;\n", " }\n", "}\n", );
let items = complete_at(&backend, &uri, text, 7, 17).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$swvar"),
"$swvar should be visible inside closure in switch. Got: {:?}",
var_labels
);
assert!(
!var_labels.contains(&"$outer"),
"$outer should NOT leak into closure in switch. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_closure_inside_return_statement() {
let backend = create_test_backend();
let uri = Url::parse("file:///closure_return.php").unwrap();
let text = concat!(
"<?php\n", "function test_return() {\n", " $outer = 1;\n", " return function() {\n", " $retvar = 'r';\n", " $\n", " };\n", "}\n", );
let items = complete_at(&backend, &uri, text, 5, 9).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$retvar"),
"$retvar should be visible inside closure in return. Got: {:?}",
var_labels
);
assert!(
!var_labels.contains(&"$outer"),
"$outer should NOT leak into closure in return. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_closure_inside_echo() {
let backend = create_test_backend();
let uri = Url::parse("file:///closure_echo.php").unwrap();
let text = concat!(
"<?php\n", "function test_echo() {\n", " $outer = 1;\n", " echo call_user_func(function() {\n", " $echovar = 'e';\n", " $\n", " });\n", "}\n", );
let items = complete_at(&backend, &uri, text, 5, 9).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$echovar"),
"$echovar should be visible inside closure in echo. Got: {:?}",
var_labels
);
assert!(
!var_labels.contains(&"$outer"),
"$outer should NOT leak into closure in echo. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_closure_in_array_value() {
let backend = create_test_backend();
let uri = Url::parse("file:///closure_array.php").unwrap();
let text = concat!(
"<?php\n", "function test_array() {\n", " $outer = 1;\n", " $arr = [function() {\n", " $arrvar = 'a';\n", " $\n", " }];\n", "}\n", );
let items = complete_at(&backend, &uri, text, 5, 9).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$arrvar"),
"$arrvar should be visible inside closure in array. Got: {:?}",
var_labels
);
assert!(
!var_labels.contains(&"$outer"),
"$outer should NOT leak into closure in array. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_closure_in_legacy_array() {
let backend = create_test_backend();
let uri = Url::parse("file:///closure_legacy_array.php").unwrap();
let text = concat!(
"<?php\n", "function test_legacy_array() {\n", " $outer = 1;\n", " $arr = array(function() {\n", " $legvar = 'l';\n", " $\n", " });\n", "}\n", );
let items = complete_at(&backend, &uri, text, 5, 9).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$legvar"),
"$legvar should be visible inside closure in legacy array. Got: {:?}",
var_labels
);
assert!(
!var_labels.contains(&"$outer"),
"$outer should NOT leak into closure in legacy array. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_closure_in_binary_expression() {
let backend = create_test_backend();
let uri = Url::parse("file:///closure_binary.php").unwrap();
let text = concat!(
"<?php\n", "function test_binary() {\n", " $outer = 1;\n", " $flag = true;\n", " $result = $flag && ($fn = function() {\n", " $binvar = 'b';\n", " $\n", " });\n", "}\n", );
let items = complete_at(&backend, &uri, text, 6, 9).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$binvar"),
"$binvar should be visible inside closure in binary expr. Got: {:?}",
var_labels
);
assert!(
!var_labels.contains(&"$outer"),
"$outer should NOT leak into closure in binary expr. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_closure_in_ternary() {
let backend = create_test_backend();
let uri = Url::parse("file:///closure_ternary.php").unwrap();
let text = concat!(
"<?php\n", "function test_ternary() {\n", " $outer = 1;\n", " $x = true ? function() {\n", " $ternvar = 't';\n", " $\n", " } : null;\n", "}\n", );
let items = complete_at(&backend, &uri, text, 5, 9).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$ternvar"),
"$ternvar should be visible inside closure in ternary. Got: {:?}",
var_labels
);
assert!(
!var_labels.contains(&"$outer"),
"$outer should NOT leak into closure in ternary. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_closure_in_parenthesized_expr() {
let backend = create_test_backend();
let uri = Url::parse("file:///closure_paren.php").unwrap();
let text = concat!(
"<?php\n", "function test_paren() {\n", " $outer = 1;\n", " $fn = (function() {\n", " $parenvar = 'p';\n", " $\n", " });\n", "}\n", );
let items = complete_at(&backend, &uri, text, 5, 9).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$parenvar"),
"$parenvar should be visible inside closure in parens. Got: {:?}",
var_labels
);
assert!(
!var_labels.contains(&"$outer"),
"$outer should NOT leak into closure in parens. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_closure_in_method_call_arg() {
let backend = create_test_backend();
let uri = Url::parse("file:///closure_method_arg.php").unwrap();
let text = concat!(
"<?php\n", "function test_method_arg() {\n", " $outer = 1;\n", " $obj = new \\stdClass();\n", " $obj->doSomething(function() {\n", " $methvar = 'm';\n", " $\n", " });\n", "}\n", );
let items = complete_at(&backend, &uri, text, 6, 9).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$methvar"),
"$methvar should be visible inside closure in method arg. Got: {:?}",
var_labels
);
assert!(
!var_labels.contains(&"$outer"),
"$outer should NOT leak into closure in method arg. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_closure_in_nullsafe_method_call_arg() {
let backend = create_test_backend();
let uri = Url::parse("file:///closure_nullsafe.php").unwrap();
let text = concat!(
"<?php\n", "function test_nullsafe() {\n", " $outer = 1;\n", " $obj = new \\stdClass();\n", " $obj?->doSomething(function() {\n", " $nsvar = 'n';\n", " $\n", " });\n", "}\n", );
let items = complete_at(&backend, &uri, text, 6, 9).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$nsvar"),
"$nsvar should be visible inside closure in nullsafe method arg. Got: {:?}",
var_labels
);
assert!(
!var_labels.contains(&"$outer"),
"$outer should NOT leak into closure in nullsafe method arg. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_closure_in_static_method_call_arg() {
let backend = create_test_backend();
let uri = Url::parse("file:///closure_static_call.php").unwrap();
let text = concat!(
"<?php\n", "class StaticHost {\n", " public static function run(callable $cb): void {}\n", "}\n", "function test_static_call() {\n", " $outer = 1;\n", " StaticHost::run(function() {\n", " $stvar = 'st';\n", " $\n", " });\n", "}\n", );
let items = complete_at(&backend, &uri, text, 8, 9).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$stvar"),
"$stvar should be visible inside closure in static call arg. Got: {:?}",
var_labels
);
assert!(
!var_labels.contains(&"$outer"),
"$outer should NOT leak into closure in static call arg. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_arrow_fn_param_in_call_arg() {
let backend = create_test_backend();
let uri = Url::parse("file:///arrow_fn_call_arg.php").unwrap();
let text = concat!(
"<?php\n", "function test_arrow_call() {\n", " $arr = [1, 2, 3];\n", " array_map(fn($item) => $item\n", " , $arr);\n", "}\n", );
let items = complete_at(&backend, &uri, text, 3, 32).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$item"),
"Arrow fn parameter $item should be visible in body. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_global_statement_variable() {
let backend = create_test_backend();
let uri = Url::parse("file:///global_stmt.php").unwrap();
let text = concat!(
"<?php\n", "function test_global() {\n", " global $globalVar;\n", " $\n", "}\n", );
let items = complete_at(&backend, &uri, text, 3, 5).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$globalVar"),
"$globalVar from global statement should be visible. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_static_statement_variable() {
let backend = create_test_backend();
let uri = Url::parse("file:///static_stmt.php").unwrap();
let text = concat!(
"<?php\n", "function test_static() {\n", " static $counter = 0;\n", " $\n", "}\n", );
let items = complete_at(&backend, &uri, text, 3, 5).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$counter"),
"$counter from static statement should be visible. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_var_docblock_standalone() {
let backend = create_test_backend();
let uri = Url::parse("file:///var_docblock_standalone.php").unwrap();
let text = concat!(
"<?php\n", "function test_var_docblock() {\n", " /** @var \\DateTime $myDate */\n", " $myDate = null;\n", " $\n", "}\n", );
let items = complete_at(&backend, &uri, text, 4, 5).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$myDate"),
"$myDate from @var docblock should be visible. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_interface_scope_no_this() {
let backend = create_test_backend();
let uri = Url::parse("file:///iface_scope.php").unwrap();
let text = concat!(
"<?php\n", "interface MyInterface {\n", " public function doStuff(): void;\n", " // $\n", "}\n", );
let items = complete_at(&backend, &uri, text, 3, 7).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
!var_labels.contains(&"$this"),
"$this should NOT be visible in interface scope. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_enum_scope_this_visible() {
let backend = create_test_backend();
let uri = Url::parse("file:///enum_scope.php").unwrap();
let text = concat!(
"<?php\n", "enum Color {\n", " case Red;\n", " case Blue;\n", " public function label(): string {\n", " $\n", " }\n", "}\n", );
let items = complete_at(&backend, &uri, text, 5, 9).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$this"),
"$this should be visible in enum non-static method. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_trait_scope_this_visible() {
let backend = create_test_backend();
let uri = Url::parse("file:///trait_scope.php").unwrap();
let text = concat!(
"<?php\n", "trait MyTrait {\n", " public function doWork(): void {\n", " $traitLocal = 1;\n", " $\n", " }\n", "}\n", );
let items = complete_at(&backend, &uri, text, 4, 9).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$this"),
"$this should be visible in trait non-static method. Got: {:?}",
var_labels
);
assert!(
var_labels.contains(&"$traitLocal"),
"$traitLocal should be visible in trait method. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_switch_body_variable_visible() {
let backend = create_test_backend();
let uri = Url::parse("file:///switch_var.php").unwrap();
let text = concat!(
"<?php\n", "function test_switch_var() {\n", " $val = 1;\n", " switch ($val) {\n", " case 1:\n", " $result = 'one';\n", " break;\n", " case 2:\n", " $other = 'two';\n", " break;\n", " }\n", " $\n", "}\n", );
let items = complete_at(&backend, &uri, text, 11, 5).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$result"),
"$result from switch case should be visible after switch. Got: {:?}",
var_labels
);
assert!(
var_labels.contains(&"$other"),
"$other from switch case should be visible after switch. Got: {:?}",
var_labels
);
assert!(
var_labels.contains(&"$val"),
"$val should still be visible after switch. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_unset_removes_variable() {
let backend = create_test_backend();
let uri = Url::parse("file:///unset_var.php").unwrap();
let text = concat!(
"<?php\n", "function test_unset() {\n", " $keep = 1;\n", " $remove = 2;\n", " unset($remove);\n", " $\n", "}\n", );
let items = complete_at(&backend, &uri, text, 5, 5).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$keep"),
"$keep should still be visible after unset of another var. Got: {:?}",
var_labels
);
assert!(
!var_labels.contains(&"$remove"),
"$remove should NOT be visible after unset($remove). Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_for_loop_init_variable() {
let backend = create_test_backend();
let uri = Url::parse("file:///for_init.php").unwrap();
let text = concat!(
"<?php\n", "function test_for_init() {\n", " for ($i = 0; $i < 10; $i++) {\n", " $\n", " }\n", "}\n", );
let items = complete_at(&backend, &uri, text, 3, 9).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$i"),
"$i from for-loop init should be visible inside loop. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_colon_delimited_if() {
let backend = create_test_backend();
let uri = Url::parse("file:///colon_if.php").unwrap();
let text = concat!(
"<?php\n", "$colbefore = 1;\n", "if (true):\n", " $colvar = 1;\n", "endif;\n", "$\n", );
let items = complete_at(&backend, &uri, text, 5, 1).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$colbefore"),
"$colbefore should be visible after colon-delimited if. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_colon_delimited_for() {
let backend = create_test_backend();
let uri = Url::parse("file:///colon_for.php").unwrap();
let text = concat!(
"<?php\n", "$forbefore = 1;\n", "for ($j = 0; $j < 5; $j++):\n", " $forvar = 'f';\n", "endfor;\n", "$\n", );
let items = complete_at(&backend, &uri, text, 5, 1).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$forbefore"),
"$forbefore should be visible after colon-delimited for. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_colon_delimited_while() {
let backend = create_test_backend();
let uri = Url::parse("file:///colon_while.php").unwrap();
let text = concat!(
"<?php\n", "$wbefore = 1;\n", "while (true):\n", " $whilevar = 'w';\n", "endwhile;\n", "$\n", );
let items = complete_at(&backend, &uri, text, 5, 1).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$wbefore"),
"$wbefore should be visible after colon-delimited while. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_block_statement() {
let backend = create_test_backend();
let uri = Url::parse("file:///block_stmt.php").unwrap();
let text = concat!(
"<?php\n", "function test_block() {\n", " {\n", " $blockvar = 1;\n", " $\n", " }\n", "}\n", );
let items = complete_at(&backend, &uri, text, 4, 9).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$blockvar"),
"$blockvar should be visible inside block statement. Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_closure_this_carried_from_outer_scope() {
let backend = create_test_backend();
let uri = Url::parse("file:///closure_this_carry.php").unwrap();
let text = concat!(
"<?php\n", "class MyService {\n", " public function handle(): void {\n", " $outer = 'x';\n", " $fn = function() {\n", " $inner = 'y';\n", " $\n", " };\n", " }\n", "}\n", );
let items = complete_at(&backend, &uri, text, 6, 13).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$this"),
"$this should be carried into closure from instance method scope. Got: {:?}",
var_labels
);
assert!(
var_labels.contains(&"$inner"),
"$inner should be visible inside closure body. Got: {:?}",
var_labels
);
assert!(
!var_labels.contains(&"$outer"),
"$outer should NOT leak into closure (no use clause). Got: {:?}",
var_labels
);
}
#[tokio::test]
async fn test_completion_namespace_scope_implicit() {
let backend = create_test_backend();
let uri = Url::parse("file:///ns_implicit.php").unwrap();
let text = concat!(
"<?php\n", "namespace App;\n", "$nsvar = 1;\n", "$\n", );
let items = complete_at(&backend, &uri, text, 3, 1).await;
let var_labels: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::VARIABLE))
.map(|i| i.label.as_str())
.collect();
assert!(
var_labels.contains(&"$nsvar"),
"$nsvar should be visible in implicit namespace scope. Got: {:?}",
var_labels
);
}