use super::*;
use expect_test::expect;
use serde_json::json;
#[tokio::test]
async fn inlay_hints_from_workspace_index_only() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(
tmp.path().join("greeter.php"),
"<?php\nfunction greet(string $name, int $count): void {}\n",
)
.unwrap();
let caller_src = "<?php\ngreet('world', 3);\n";
std::fs::write(tmp.path().join("caller.php"), caller_src).unwrap();
let mut s = TestServer::with_root(tmp.path()).await;
s.wait_for_index_ready().await;
s.open("caller.php", caller_src).await;
let resp = s.inlay_hints("caller.php", 0, 0, 3, 0).await;
expect![[r#"
1:6 name:
1:15 count:"#]]
.assert_eq(&render_inlay_hints(&resp));
}
#[tokio::test]
async fn inlay_hints_cross_file_function_call() {
let mut s = TestServer::new().await;
let out = s
.check_inlay_hints(
r#"//- /caller.php
<?php
greet('world', 3);
//- /greeter.php
<?php
function greet(string $name, int $count): void {}
"#,
)
.await;
expect![[r#"
1:6 name:
1:15 count:"#]]
.assert_eq(&out);
}
#[tokio::test]
async fn inlay_hints_cross_file_constructor_call() {
let mut s = TestServer::new().await;
let out = s
.check_inlay_hints(
r#"//- /caller.php
<?php
$p = new Point(1, 2);
//- /Point.php
<?php
class Point {
public function __construct(int $x, int $y) {}
}
"#,
)
.await;
expect![[r#"
1:15 x:
1:18 y:"#]]
.assert_eq(&out);
}
#[tokio::test]
async fn inlay_hints_cross_file_method_call() {
let mut s = TestServer::new().await;
let out = s
.check_inlay_hints(
r#"//- /caller.php
<?php
$g = new Greeter();
$g->sayHello('World');
//- /Greeter.php
<?php
class Greeter {
public function sayHello(string $name): void {}
}
"#,
)
.await;
expect![[r#"
2:13 name:"#]]
.assert_eq(&out);
}
#[tokio::test]
async fn inlay_hints_for_parameter_names() {
let mut s = TestServer::new().await;
let out = s
.check_inlay_hints(
r#"<?php
function greet(string $name, int $count): void {}
greet('world', 3);
"#,
)
.await;
expect![[r#"
2:6 name:
2:15 count:"#]]
.assert_eq(&out);
}
#[tokio::test]
async fn inlay_hint_resolve_populates_tooltip() {
let mut s = TestServer::new().await;
s.open(
"resolve.php",
"<?php\nfunction add(int $a, int $b): int { return $a + $b; }\nadd(1, 2);\n",
)
.await;
let hints_resp = s.inlay_hints("resolve.php", 0, 0, 4, 0).await;
let hints = hints_resp["result"].as_array().cloned().unwrap_or_default();
let resp = s.inlay_hint_resolve(hints[0].clone()).await;
let out = render_resolved_inlay_hint(&resp);
expect![[r#"
2:4 a:
tooltip: ```php
function add(int $a, int $b): int
```"#]]
.assert_eq(&out);
}
#[tokio::test]
async fn inlay_hint_resolve_with_docblock_includes_docs() {
let mut s = TestServer::new().await;
s.open(
"resolve.php",
"<?php\n/** Adds two integers */\nfunction add(int $a, int $b): int { return $a + $b; }\nadd(1, 2);\n",
)
.await;
let hints_resp = s.inlay_hints("resolve.php", 0, 0, 5, 0).await;
let hints = hints_resp["result"].as_array().cloned().unwrap_or_default();
let resp = s.inlay_hint_resolve(hints[0].clone()).await;
let out = render_resolved_inlay_hint(&resp);
expect![[r#"
3:4 a:
tooltip: ```php
function add(int $a, int $b): int
```
---
Adds two integers"#]]
.assert_eq(&out);
}
#[tokio::test]
async fn inlay_hint_resolve_no_data_field_returns_unchanged() {
let mut s = TestServer::new().await;
s.open("nohint.php", "<?php").await;
let hint = json!({
"position": { "line": 0, "character": 5 },
"label": "$test:",
});
let resp = s.inlay_hint_resolve(hint).await;
let out = render_resolved_inlay_hint(&resp);
expect![[r#"
0:5 $test:
tooltip: <no tooltip>"#]]
.assert_eq(&out);
}
#[tokio::test]
async fn inlay_hint_resolve_existing_tooltip_is_noop() {
let mut s = TestServer::new().await;
s.open("existing.php", "<?php").await;
let hint = json!({
"position": { "line": 1, "character": 10 },
"label": "param:",
"tooltip": {
"kind": "markdown",
"value": "custom tooltip"
}
});
let resp = s.inlay_hint_resolve(hint).await;
let out = render_resolved_inlay_hint(&resp);
expect![[r#"
1:10 param:
tooltip: custom tooltip"#]]
.assert_eq(&out);
}
#[tokio::test]
async fn inlay_hint_resolve_data_without_php_lsp_fn_returns_unchanged() {
let mut s = TestServer::new().await;
s.open("nokey.php", "<?php").await;
let hint = json!({
"position": { "line": 0, "character": 5 },
"label": "param:",
"data": {
"some_other_key": "value"
}
});
let resp = s.inlay_hint_resolve(hint).await;
let out = render_resolved_inlay_hint(&resp);
expect![[r#"
0:5 param:
tooltip: <no tooltip>"#]]
.assert_eq(&out);
}
#[tokio::test]
async fn inlay_hint_resolve_php_lsp_fn_nonexistent_function_returns_unchanged() {
let mut s = TestServer::new().await;
s.open("nofunc.php", "<?php").await;
let hint = json!({
"position": { "line": 2, "character": 8 },
"label": "$x:",
"data": {
"php_lsp_fn": "nonExistentFunctionXyz"
}
});
let resp = s.inlay_hint_resolve(hint).await;
let out = render_resolved_inlay_hint(&resp);
expect![[r#"
2:8 $x:
tooltip: <no tooltip>"#]]
.assert_eq(&out);
}
#[tokio::test]
async fn inlay_hint_resolve_is_idempotent() {
let mut s = TestServer::new().await;
s.open(
"idempotent.php",
"<?php\nfunction add(int $a, int $b): int { return $a + $b; }\nadd(1, 2);\n",
)
.await;
let hints_resp = s.inlay_hints("idempotent.php", 0, 0, 4, 0).await;
let hints = hints_resp["result"].as_array().cloned().unwrap_or_default();
let resolved_once = s.inlay_hint_resolve(hints[0].clone()).await;
let resolved_twice = s.inlay_hint_resolve(resolved_once["result"].clone()).await;
let out1 = render_resolved_inlay_hint(&resolved_once);
let out2 = render_resolved_inlay_hint(&resolved_twice);
assert_eq!(
out1, out2,
"calling resolve twice must return identical results (idempotent)"
);
expect![[r#"
2:4 a:
tooltip: ```php
function add(int $a, int $b): int
```"#]]
.assert_eq(&out1);
}
#[tokio::test]
async fn inlay_hints_nullsafe_method_call() {
let mut s = TestServer::new().await;
let out = s
.check_inlay_hints(
r#"//- /caller.php
<?php
$g = new Greeter();
$g?->sayHello('World');
//- /Greeter.php
<?php
class Greeter {
public function sayHello(string $name): void {}
}
"#,
)
.await;
expect![[r#"
2:14 name:"#]]
.assert_eq(&out);
}
#[tokio::test]
async fn inlay_hints_static_method_call() {
let mut s = TestServer::new().await;
let out = s
.check_inlay_hints(
r#"//- /caller.php
<?php
Greeter::sayHello('world');
//- /Greeter.php
<?php
class Greeter {
public static function sayHello(string $name): void {}
}
"#,
)
.await;
expect![[r#"
1:18 name:"#]]
.assert_eq(&out);
}
#[tokio::test]
async fn inlay_hints_empty_for_file_with_no_calls() {
let mut s = TestServer::new().await;
let out = s
.check_inlay_hints(
r#"<?php
$x = 1;
$y = 2;
"#,
)
.await;
expect!["<no hints>"].assert_eq(&out);
}
#[tokio::test]
async fn inlay_hints_respects_lsp_half_open_range_semantics() {
let mut s = TestServer::new().await;
s.open(
"range_test.php",
"<?php\nfunction f(int $x): void {}\nf(1);\n",
)
.await;
let resp = s.inlay_hints("range_test.php", 2, 0, 2, 2).await;
let out = render_inlay_hints(&resp);
expect!["<no hints>"].assert_eq(&out);
let resp = s.inlay_hints("range_test.php", 2, 0, 2, 3).await;
let out = render_inlay_hints(&resp);
expect![[r#"2:2 x:"#]].assert_eq(&out);
}
#[tokio::test]
async fn inlay_hints_method_name_collision() {
let mut s = TestServer::new().await;
let out = s
.check_inlay_hints(
r#"//- /caller.php
<?php
$processor = new DataProcessor();
$processor->process(1, 2);
//- /DataProcessor.php
<?php
class DataProcessor {
public function process(int $x, int $y): int {
return $x + $y;
}
}
"#,
)
.await;
expect![[r#"
2:20 x:
2:23 y:"#]]
.assert_eq(&out);
}
#[tokio::test]
async fn inlay_hints_method_different_signatures() {
let mut s = TestServer::new().await;
let out = s
.check_inlay_hints(
r#"//- /main.php
<?php
$filter = new TextFilter();
$filter->apply("input", "lowercase");
//- /TextFilter.php
<?php
class TextFilter {
public function apply(string $text, string $mode): string {
return $mode === "lowercase" ? strtolower($text) : strtoupper($text);
}
}
"#,
)
.await;
expect![[r#"
2:15 text:
2:24 mode:"#]]
.assert_eq(&out);
}
#[tokio::test]
async fn inlay_hints_inherited_method_parameters() {
let mut s = TestServer::new().await;
let out = s
.check_inlay_hints(
r#"//- /main.php
<?php
$child = new ChildClass();
$child->compute(10, 20);
//- /Parent.php
<?php
class ParentClass {
public function compute(int $a, int $b): int {
return $a + $b;
}
}
//- /Child.php
<?php
class ChildClass extends ParentClass {
// No override, should inherit parent's signature
}
"#,
)
.await;
expect![[r#"
2:16 a:
2:20 b:"#]]
.assert_eq(&out);
}
#[tokio::test]
async fn inlay_hints_static_method_with_math() {
let mut s = TestServer::new().await;
let out = s
.check_inlay_hints(
r#"//- /main.php
<?php
$result = MathHelper::add(5, 3);
//- /MathHelper.php
<?php
class MathHelper {
public static function add(int $a, int $b): int {
return $a + $b;
}
}
"#,
)
.await;
expect![[r#"
1:26 a:
1:29 b:
1:31 : int"#]]
.assert_eq(&out);
}
#[tokio::test]
async fn inlay_hints_abstract_method_implementation() {
let mut s = TestServer::new().await;
let out = s
.check_inlay_hints(
r#"//- /main.php
<?php
$handler = new ConcreteHandler();
$handler->process("test", 123);
//- /Handler.php
<?php
abstract class AbstractHandler {
abstract public function process(string $input, int $code): void;
}
//- /ConcreteHandler.php
<?php
class ConcreteHandler extends AbstractHandler {
public function process(string $input, int $code): void {
// implementation
}
}
"#,
)
.await;
expect![[r#"
2:18 input:
2:26 code:"#]]
.assert_eq(&out);
}
#[tokio::test]
async fn inlay_hints_unknown_function_no_hints() {
let mut s = TestServer::new().await;
let out = s
.check_inlay_hints(
r#"<?php
unknownFn(1, 2);
"#,
)
.await;
expect!["<no hints>"].assert_eq(&out);
}
#[tokio::test]
async fn inlay_hints_zero_param_call_no_hints() {
let mut s = TestServer::new().await;
let out = s
.check_inlay_hints(
r#"<?php
function init(): void {}
init();
"#,
)
.await;
expect!["<no hints>"].assert_eq(&out);
}
#[tokio::test]
async fn inlay_hints_skips_named_arguments() {
let mut s = TestServer::new().await;
let out = s
.check_inlay_hints(
r#"<?php
function greet(string $name): void {}
greet(name: 'Alice');
"#,
)
.await;
expect!["<no hints>"].assert_eq(&out);
}
#[tokio::test]
async fn inlay_hints_fewer_args_than_params() {
let mut s = TestServer::new().await;
let out = s
.check_inlay_hints(
r#"<?php
function add(int $a, int $b): int { return $a + $b; }
add(1);
"#,
)
.await;
expect![[r#"
2:4 a:"#]]
.assert_eq(&out);
}
#[tokio::test]
async fn inlay_hints_more_args_than_params() {
let mut s = TestServer::new().await;
let out = s
.check_inlay_hints(
r#"<?php
function f(int $x): void {}
f(1, 2, 3);
"#,
)
.await;
expect![[r#"
2:2 x:"#]]
.assert_eq(&out);
}
#[tokio::test]
async fn inlay_hints_return_type_for_assignment() {
let mut s = TestServer::new().await;
let out = s
.check_inlay_hints(
r#"<?php
function make(): string { return 'x'; }
$s = make();
"#,
)
.await;
expect![[r#"
2:11 : string"#]]
.assert_eq(&out);
}
#[tokio::test]
async fn inlay_hints_void_return_type_suppressed() {
let mut s = TestServer::new().await;
let out = s
.check_inlay_hints(
r#"<?php
function init(): void {}
$x = init();
"#,
)
.await;
expect!["<no hints>"].assert_eq(&out);
}
#[tokio::test]
async fn inlay_hints_function_inside_namespace() {
let mut s = TestServer::new().await;
let out = s
.check_inlay_hints(
r#"<?php
namespace App;
function greet(string $name): void {}
greet('Alice');
"#,
)
.await;
expect![[r#"
3:6 name:"#]]
.assert_eq(&out);
}
#[tokio::test]
async fn inlay_hints_closure_variable_call() {
let mut s = TestServer::new().await;
let out = s
.check_inlay_hints(
r#"<?php
$greet = function(string $name, int $times): void {};
$greet('Alice', 3);
"#,
)
.await;
expect![[r#"
2:7 name:
2:16 times:"#]]
.assert_eq(&out);
}
#[tokio::test]
async fn inlay_hints_arrow_function_variable_call() {
let mut s = TestServer::new().await;
let out = s
.check_inlay_hints(
r#"<?php
$double = fn(int $n): int => $n * 2;
$result = $double(5);
"#,
)
.await;
expect![[r#"
2:18 n:"#]]
.assert_eq(&out);
}
#[tokio::test]
async fn inlay_hints_call_inside_closure_body() {
let mut s = TestServer::new().await;
let out = s
.check_inlay_hints(
r#"<?php
function add(int $a, int $b): int { return $a + $b; }
$fn = function() { add(1, 2); };
"#,
)
.await;
expect![[r#"
2:23 a:
2:26 b:"#]]
.assert_eq(&out);
}
#[tokio::test]
async fn inlay_hints_trait_method_call() {
let mut s = TestServer::new().await;
let out = s
.check_inlay_hints(
r#"<?php
trait Logging {
public function log(string $msg, int $level): void {}
}
class AppLogger {
use Logging;
}
$logger = new AppLogger();
$logger->log('hello', 3);
"#,
)
.await;
expect![[r#"
8:13 msg:
8:22 level:"#]]
.assert_eq(&out);
}
#[tokio::test]
async fn inlay_hints_for_loop_calls() {
let mut s = TestServer::new().await;
let out = s
.check_inlay_hints(
r#"<?php
function tick(int $n): void {}
for (tick(1); $i < 10; tick(2)) {}
"#,
)
.await;
expect![[r#"
2:10 n:
2:28 n:"#]]
.assert_eq(&out);
}
#[tokio::test]
async fn inlay_hints_new_without_constructor_no_hints() {
let mut s = TestServer::new().await;
let out = s
.check_inlay_hints(
r#"<?php
class Foo {}
$f = new Foo();
"#,
)
.await;
expect!["<no hints>"].assert_eq(&out);
}
#[tokio::test]
async fn inlay_hints_calls_inside_trait_method_body() {
let mut s = TestServer::new().await;
let out = s
.check_inlay_hints(
r#"<?php
function write(string $msg): void {}
trait Logger {
public function log(): void { write('hello'); }
}
"#,
)
.await;
expect![[r#"
3:40 msg:"#]]
.assert_eq(&out);
}
#[tokio::test]
async fn inlay_hints_calls_inside_enum_method_body() {
let mut s = TestServer::new().await;
let out = s
.check_inlay_hints(
r#"<?php
function write(string $msg): void {}
enum Status {
case Active;
public function log(): void { write('hello'); }
}
"#,
)
.await;
expect![[r#"
4:40 msg:"#]]
.assert_eq(&out);
}
#[tokio::test]
async fn inlay_hints_enum_method_call() {
let mut s = TestServer::new().await;
let out = s
.check_inlay_hints(
r#"<?php
enum Status {
case Active;
public function label(string $prefix, int $pad): string { return ''; }
}
label('x', 2);
"#,
)
.await;
expect![[r#"
5:6 prefix:
5:11 pad:"#]]
.assert_eq(&out);
}
#[tokio::test]
async fn inlay_hints_foreach_type_hint() {
let mut s = TestServer::new().await;
let out = s
.check_inlay_hints(
r#"<?php
class User {}
$users = array_map(fn($x): User => $x, []);
foreach ($users as $user) {
$user;
}
"#,
)
.await;
expect![[r#"
3:24 : User"#]]
.assert_eq(&out);
}
#[tokio::test]
async fn inlay_hints_foreach_no_type_hint_when_unknown() {
let mut s = TestServer::new().await;
let out = s
.check_inlay_hints(
r#"<?php
foreach ($items as $item) {
$item;
}
"#,
)
.await;
expect!["<no hints>"].assert_eq(&out);
}
#[tokio::test]
async fn inlay_hints_variadic_all_args() {
let mut s = TestServer::new().await;
let out = s
.check_inlay_hints(
r#"<?php
function record(string ...$messages): void {}
record('a', 'b', 'c');
"#,
)
.await;
expect![[r#"
2:7 messages:
2:12 messages:
2:17 messages:"#]]
.assert_eq(&out);
}
#[tokio::test]
async fn inlay_hints_variadic_after_regular_params() {
let mut s = TestServer::new().await;
let out = s
.check_inlay_hints(
r#"<?php
function push(string $key, int ...$values): void {}
push('bucket', 1, 2, 3);
"#,
)
.await;
expect![[r#"
2:5 key:
2:15 values:
2:18 values:
2:21 values:"#]]
.assert_eq(&out);
}
#[tokio::test]
async fn inlay_hints_arrow_function_declared_return_type() {
let mut s = TestServer::new().await;
let out = s
.check_inlay_hints(
r#"<?php
$double = fn(int $n): int => $n * 2;
"#,
)
.await;
expect!["<no hints>"].assert_eq(&out);
}
#[tokio::test]
async fn inlay_hints_arrow_function_no_return_type_annotation() {
let mut s = TestServer::new().await;
let out = s
.check_inlay_hints(
r#"<?php
$double = fn(int $n) => $n * 2;
"#,
)
.await;
expect!["<no hints>"].assert_eq(&out);
}
#[tokio::test]
async fn inlay_hints_constructor_promoted_properties() {
let mut s = TestServer::new().await;
let out = s
.check_inlay_hints(
r#"<?php
class User {
public function __construct(
public readonly string $name,
public int $age,
) {}
}
$u = new User('Alice', 30);
"#,
)
.await;
expect![[r#"
7:14 name:
7:23 age:"#]]
.assert_eq(&out);
}
#[tokio::test]
async fn inlay_hints_foreach_key_value_type_hint() {
let mut s = TestServer::new().await;
let out = s
.check_inlay_hints(
r#"<?php
class User {}
$users = array_map(fn($x): User => $x, []);
foreach ($users as $k => $user) {
$user;
}
"#,
)
.await;
expect![[r#"
3:30 : User"#]]
.assert_eq(&out);
}
#[tokio::test]
async fn inlay_hints_try_catch_finally_walk() {
let mut s = TestServer::new().await;
let out = s
.check_inlay_hints(
r#"<?php
function cleanup(string $resource): void {}
try {
cleanup('db');
} catch (Exception $e) {
cleanup('conn');
} finally {
cleanup('log');
}
"#,
)
.await;
expect![[r#"
3:12 resource:
5:12 resource:
7:12 resource:"#]]
.assert_eq(&out);
}
#[tokio::test]
async fn inlay_hints_call_inside_arrow_function_body() {
let mut s = TestServer::new().await;
let out = s
.check_inlay_hints(
r#"<?php
function greet(string $name): void {}
$fn = fn($x) => greet($x);
"#,
)
.await;
expect![[r#"
2:22 name:"#]]
.assert_eq(&out);
}
#[tokio::test]
async fn inlay_hints_refresh_after_did_change() {
let mut s = TestServer::new().await;
s.open(
"main.php",
"<?php\nfunction greet(string $name): void {}\ngreet('Alice');\n",
)
.await;
let resp = s.inlay_hints("main.php", 0, 0, 4, 0).await;
expect![[r#"
2:6 name:"#]]
.assert_eq(&render_inlay_hints(&resp));
s.change(
"main.php",
2,
"<?php\nfunction greet(string $name): void {}\nfunction add(int $a, int $b): int { return $a + $b; }\ngreet('Alice');\nadd(1, 2);\n",
)
.await;
let resp = s.inlay_hints("main.php", 0, 0, 6, 0).await;
expect![[r#"
3:6 name:
4:4 a:
4:7 b:"#]]
.assert_eq(&render_inlay_hints(&resp));
}
#[tokio::test]
async fn inlay_hints_server_advertises_resolve_provider() {
let (_, init_resp) =
TestServer::new_with_options(json!({ "diagnostics": { "enabled": true } })).await;
let resolve_provider =
init_resp["result"]["capabilities"]["inlayHintProvider"]["resolveProvider"]
.as_bool()
.unwrap_or(false);
assert!(
resolve_provider,
"inlayHintProvider must advertise resolveProvider: true, got: {}",
init_resp["result"]["capabilities"]["inlayHintProvider"]
);
}
#[tokio::test]
async fn inlay_hints_kind_field_values() {
let mut s = TestServer::new().await;
s.open(
"kinds.php",
"<?php\nclass User {}\nfunction greet(string $name): void {}\n$users = array_map(fn($x): User => $x, []);\nforeach ($users as $u) {}\ngreet('Alice');\n",
)
.await;
let resp = s.inlay_hints("kinds.php", 0, 0, 7, 0).await;
let hints = resp["result"].as_array().expect("result must be an array");
let param_hint = hints
.iter()
.find(|h| {
h["label"]
.as_str()
.map(|l| l.ends_with(':'))
.unwrap_or(false)
})
.expect("expected at least one parameter hint");
assert_eq!(
param_hint["kind"].as_u64(),
Some(2),
"parameter hint must have kind=2 (LSP InlayHintKind.Parameter), got: {}",
param_hint["kind"]
);
let type_hint = hints
.iter()
.find(|h| {
h["label"]
.as_str()
.map(|l| l.starts_with(": "))
.unwrap_or(false)
})
.expect("expected at least one type hint");
assert_eq!(
type_hint["kind"].as_u64(),
Some(1),
"type hint must have kind=1 (LSP InlayHintKind.Type), got: {}",
type_hint["kind"]
);
}
#[tokio::test]
async fn inlay_hints_empty_file_returns_array_not_null() {
let mut s = TestServer::new().await;
s.open("empty.php", "<?php\n$x = 1;\n").await;
let resp = s.inlay_hints("empty.php", 0, 0, 3, 0).await;
expect!["<no hints>"].assert_eq(&render_inlay_hints(&resp));
}