use crate::common::{create_psr4_workspace, create_test_backend};
use tower_lsp::LanguageServer;
use tower_lsp::lsp_types::*;
async fn complete_at(
backend: &phpantom_lsp::Backend,
uri: &Url,
src: &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: src.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,
None => vec![],
}
}
fn method_names(items: &[CompletionItem]) -> Vec<&str> {
items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::METHOD))
.map(|i| i.filter_text.as_deref().unwrap_or(&i.label))
.collect()
}
fn property_names(items: &[CompletionItem]) -> Vec<&str> {
items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::PROPERTY))
.map(|i| i.filter_text.as_deref().unwrap_or(&i.label))
.collect()
}
#[tokio::test]
async fn test_param_closure_this_instance_method() {
let backend = create_test_backend();
let uri = Url::parse("file:///test/closure_this_tag.php").unwrap();
let src = concat!(
"<?php\n",
"class Route {\n",
" public function middleware(string $m): self { return $this; }\n",
" public function prefix(string $p): self { return $this; }\n",
"}\n",
"class Router {\n",
" /**\n",
" * @param-closure-this Route $callback\n",
" */\n",
" public function group(\\Closure $callback): void {}\n",
"}\n",
"class AppRoutes {\n",
" public function register(): void {\n",
" $router = new Router();\n",
" $router->group(function () {\n",
" $this->\n",
" });\n",
" }\n",
"}\n",
);
let items = complete_at(&backend, &uri, src, 15, 19).await;
let names = method_names(&items);
assert!(
names.contains(&"middleware"),
"Expected 'middleware' from @param-closure-this Route, got: {:?}",
names,
);
assert!(
names.contains(&"prefix"),
"Expected 'prefix' from @param-closure-this Route, got: {:?}",
names,
);
}
#[tokio::test]
async fn test_param_closure_this_dollar_this_type() {
let backend = create_test_backend();
let uri = Url::parse("file:///test/closure_this_self.php").unwrap();
let src = concat!(
"<?php\n",
"class CacheManager {\n",
" public function getDefaultDriver(): string { return ''; }\n",
" /**\n",
" * @param string $driver\n",
" * @param \\Closure $callback\n",
" * @param-closure-this $this $callback\n",
" * @return $this\n",
" */\n",
" public function extend(string $driver, \\Closure $callback): self { return $this; }\n",
"}\n",
"class App {\n",
" public function boot(): void {\n",
" $mgr = new CacheManager();\n",
" $mgr->extend('redis', function () {\n",
" $this->\n",
" });\n",
" }\n",
"}\n",
);
let items = complete_at(&backend, &uri, src, 15, 19).await;
let names = method_names(&items);
assert!(
names.contains(&"getDefaultDriver"),
"Expected 'getDefaultDriver' from @param-closure-this $this (CacheManager), got: {:?}",
names,
);
assert!(
names.contains(&"extend"),
"Expected 'extend' from @param-closure-this $this (CacheManager), got: {:?}",
names,
);
}
#[tokio::test]
async fn test_param_closure_this_static_type() {
let backend = create_test_backend();
let uri = Url::parse("file:///test/closure_this_static.php").unwrap();
let src = concat!(
"<?php\n",
"class Macroable {\n",
" public function getMacros(): array { return []; }\n",
" /**\n",
" * @param string $name\n",
" * @param callable $macro\n",
" * @param-closure-this static $macro\n",
" */\n",
" public static function macro(string $name, callable $macro): void {}\n",
"}\n",
"class App {\n",
" public function run(): void {\n",
" Macroable::macro('test', function () {\n",
" $this->\n",
" });\n",
" }\n",
"}\n",
);
let items = complete_at(&backend, &uri, src, 13, 19).await;
let names = method_names(&items);
assert!(
names.contains(&"getMacros"),
"Expected 'getMacros' from @param-closure-this static (Macroable), got: {:?}",
names,
);
}
#[tokio::test]
async fn test_param_closure_this_standalone_function() {
let backend = create_test_backend();
let uri = Url::parse("file:///test/closure_this_func.php").unwrap();
let src = concat!(
"<?php\n",
"class TestCase {\n",
" public function assertTrue(bool $v): void {}\n",
" public function assertFalse(bool $v): void {}\n",
"}\n",
"/**\n",
" * @param-closure-this TestCase $callback\n",
" */\n",
"function test(string $name, \\Closure $callback): void {}\n",
"class Runner {\n",
" public function go(): void {\n",
" test('example', function () {\n",
" $this->\n",
" });\n",
" }\n",
"}\n",
);
let items = complete_at(&backend, &uri, src, 12, 19).await;
let names = method_names(&items);
assert!(
names.contains(&"assertTrue"),
"Expected 'assertTrue' from @param-closure-this TestCase, got: {:?}",
names,
);
assert!(
names.contains(&"assertFalse"),
"Expected 'assertFalse' from @param-closure-this TestCase, got: {:?}",
names,
);
}
#[tokio::test]
async fn test_param_closure_this_does_not_leak() {
let backend = create_test_backend();
let uri = Url::parse("file:///test/closure_this_no_leak.php").unwrap();
let src = concat!(
"<?php\n",
"class Route {\n",
" public function middleware(string $m): self { return $this; }\n",
"}\n",
"class Router {\n",
" /**\n",
" * @param-closure-this Route $callback\n",
" */\n",
" public function group(\\Closure $callback): void {}\n",
"}\n",
"class AppRoutes {\n",
" public function ownMethod(): string { return ''; }\n",
" public function register(): void {\n",
" $router = new Router();\n",
" $this->\n",
" $router->group(function () {\n",
" // inside closure: $this is Route\n",
" });\n",
" }\n",
"}\n",
);
let items = complete_at(&backend, &uri, src, 14, 15).await;
let names = method_names(&items);
assert!(
names.contains(&"ownMethod"),
"Expected 'ownMethod' from lexical class AppRoutes, got: {:?}",
names,
);
assert!(
!names.contains(&"middleware"),
"Should NOT see Route::middleware outside the closure, got: {:?}",
names,
);
}
#[tokio::test]
async fn test_param_closure_this_property_access() {
let backend = create_test_backend();
let uri = Url::parse("file:///test/closure_this_prop.php").unwrap();
let src = concat!(
"<?php\n",
"class Route {\n",
" /** @var string */\n",
" public string $uri = '';\n",
"}\n",
"class Router {\n",
" /**\n",
" * @param-closure-this Route $callback\n",
" */\n",
" public function group(\\Closure $callback): void {}\n",
"}\n",
"class App {\n",
" public function run(): void {\n",
" $r = new Router();\n",
" $r->group(function () {\n",
" $this->\n",
" });\n",
" }\n",
"}\n",
);
let items = complete_at(&backend, &uri, src, 15, 19).await;
let props = property_names(&items);
assert!(
props.contains(&"uri"),
"Expected 'uri' property from @param-closure-this Route, got: {:?}",
props,
);
}
#[tokio::test]
async fn test_param_closure_this_fqn_type() {
let backend = create_test_backend();
let uri = Url::parse("file:///test/closure_this_fqn.php").unwrap();
let src = concat!(
"<?php\n",
"class Route {\n",
" public function middleware(string $m): self { return $this; }\n",
"}\n",
"class Router {\n",
" /**\n",
" * @param-closure-this \\Route $callback\n",
" */\n",
" public function group(\\Closure $callback): void {}\n",
"}\n",
"class App {\n",
" public function run(): void {\n",
" $r = new Router();\n",
" $r->group(function () {\n",
" $this->\n",
" });\n",
" }\n",
"}\n",
);
let items = complete_at(&backend, &uri, src, 14, 19).await;
let names = method_names(&items);
assert!(
names.contains(&"middleware"),
"Expected 'middleware' from @param-closure-this \\Route, got: {:?}",
names,
);
}
#[tokio::test]
async fn test_param_closure_this_cross_file() {
let (backend, dir) = create_psr4_workspace(
r#"{"autoload": {"psr-4": {"App\\": "src/"}}}"#,
&[
(
"src/Route.php",
"<?php\nnamespace App;\nclass Route {\n public function middleware(string $m): self { return $this; }\n public function prefix(string $p): self { return $this; }\n}\n",
),
(
"src/Router.php",
concat!(
"<?php\nnamespace App;\n",
"class Router {\n",
" /**\n",
" * @param-closure-this \\App\\Route $callback\n",
" */\n",
" public function group(\\Closure $callback): void {}\n",
"}\n",
),
),
],
);
let uri = Url::from_file_path(dir.path().join("src/AppRoutes.php")).unwrap();
let src = concat!(
"<?php\n",
"namespace App;\n",
"class AppRoutes {\n",
" public function register(): void {\n",
" $router = new Router();\n",
" $router->group(function () {\n",
" $this->\n",
" });\n",
" }\n",
"}\n",
);
let items = complete_at(&backend, &uri, src, 6, 19).await;
let names = method_names(&items);
assert!(
names.contains(&"middleware"),
"Expected 'middleware' from cross-file @param-closure-this Route, got: {:?}",
names,
);
assert!(
names.contains(&"prefix"),
"Expected 'prefix' from cross-file @param-closure-this Route, got: {:?}",
names,
);
}
#[tokio::test]
async fn test_param_closure_this_second_param() {
let backend = create_test_backend();
let uri = Url::parse("file:///test/closure_this_second.php").unwrap();
let src = concat!(
"<?php\n",
"class Route {\n",
" public function middleware(string $m): self { return $this; }\n",
"}\n",
"class Router {\n",
" /**\n",
" * @param string $prefix\n",
" * @param \\Closure $callback\n",
" * @param-closure-this Route $callback\n",
" */\n",
" public function group(string $prefix, \\Closure $callback): void {}\n",
"}\n",
"class App {\n",
" public function run(): void {\n",
" $r = new Router();\n",
" $r->group('/api', function () {\n",
" $this->\n",
" });\n",
" }\n",
"}\n",
);
let items = complete_at(&backend, &uri, src, 16, 19).await;
let names = method_names(&items);
assert!(
names.contains(&"middleware"),
"Expected 'middleware' from @param-closure-this Route on second param, got: {:?}",
names,
);
}
#[tokio::test]
async fn test_param_closure_this_method_chain() {
let backend = create_test_backend();
let uri = Url::parse("file:///test/closure_this_chain.php").unwrap();
let src = concat!(
"<?php\n",
"class Route {\n",
" public function middleware(string $m): self { return $this; }\n",
" public function prefix(string $p): self { return $this; }\n",
"}\n",
"class Router {\n",
" /**\n",
" * @param-closure-this Route $callback\n",
" */\n",
" public function group(\\Closure $callback): void {}\n",
"}\n",
"class App {\n",
" public function run(): void {\n",
" $r = new Router();\n",
" $r->group(function () {\n",
" $this->middleware('auth')->\n",
" });\n",
" }\n",
"}\n",
);
let items = complete_at(&backend, &uri, src, 15, 42).await;
let names = method_names(&items);
assert!(
names.contains(&"prefix"),
"Expected 'prefix' from chained call on @param-closure-this Route, got: {:?}",
names,
);
}
#[test]
fn test_extract_param_closure_this_basic() {
use phpantom_lsp::docblock::extract_param_closure_this;
use phpantom_lsp::php_type::PhpType;
let doc = "/**\n * @param-closure-this Route $callback\n */";
let results = extract_param_closure_this(doc);
assert_eq!(results.len(), 1);
assert_eq!(
results[0],
(PhpType::parse("Route"), "$callback".to_string())
);
}
#[test]
fn test_extract_param_closure_this_fqn() {
use phpantom_lsp::docblock::extract_param_closure_this;
use phpantom_lsp::php_type::PhpType;
let doc = "/**\n * @param-closure-this \\Illuminate\\Routing\\Route $callback\n */";
let results = extract_param_closure_this(doc);
assert_eq!(results.len(), 1);
assert_eq!(
results[0],
(
PhpType::parse("\\Illuminate\\Routing\\Route"),
"$callback".to_string()
)
);
}
#[test]
fn test_extract_param_closure_this_dollar_this() {
use phpantom_lsp::docblock::extract_param_closure_this;
use phpantom_lsp::php_type::PhpType;
let doc = "/**\n * @param-closure-this $this $callback\n */";
let results = extract_param_closure_this(doc);
assert_eq!(results.len(), 1);
assert_eq!(
results[0],
(PhpType::parse("$this"), "$callback".to_string())
);
}
#[test]
fn test_extract_param_closure_this_static() {
use phpantom_lsp::docblock::extract_param_closure_this;
use phpantom_lsp::php_type::PhpType;
let doc = "/**\n * @param-closure-this static $macro\n */";
let results = extract_param_closure_this(doc);
assert_eq!(results.len(), 1);
assert_eq!(results[0], (PhpType::parse("static"), "$macro".to_string()));
}
#[test]
fn test_extract_param_closure_this_multiple() {
use phpantom_lsp::docblock::extract_param_closure_this;
use phpantom_lsp::php_type::PhpType;
let doc = concat!(
"/**\n",
" * @param-closure-this Route $callback\n",
" * @param-closure-this TestCase $setup\n",
" */",
);
let results = extract_param_closure_this(doc);
assert_eq!(results.len(), 2);
assert_eq!(
results[0],
(PhpType::parse("Route"), "$callback".to_string())
);
assert_eq!(
results[1],
(PhpType::parse("TestCase"), "$setup".to_string())
);
}
#[test]
fn test_extract_param_closure_this_no_tag() {
use phpantom_lsp::docblock::extract_param_closure_this;
let doc = "/**\n * @param string $name\n * @return void\n */";
let results = extract_param_closure_this(doc);
assert!(results.is_empty());
}
#[test]
fn test_extract_param_closure_this_missing_param_name() {
use phpantom_lsp::docblock::extract_param_closure_this;
let doc = "/**\n * @param-closure-this Route\n */";
let results = extract_param_closure_this(doc);
assert!(results.is_empty());
}
#[test]
fn test_extract_param_closure_this_coexists_with_param() {
use phpantom_lsp::docblock::extract_param_closure_this;
use phpantom_lsp::php_type::PhpType;
let doc = concat!(
"/**\n",
" * @param string $driver\n",
" * @param \\Closure $callback\n",
" *\n",
" * @param-closure-this $this $callback\n",
" *\n",
" * @return $this\n",
" */",
);
let results = extract_param_closure_this(doc);
assert_eq!(results.len(), 1);
assert_eq!(
results[0],
(PhpType::parse("$this"), "$callback".to_string())
);
}