mod common;
use common::TestServer;
#[tokio::test]
async fn hover_resolves_method_inherited_from_trait() {
let src = r#"<?php
trait Greeting {
public function sayHello(string $name): string {
return "Hello, {$name}";
}
}
class Greeter {
use Greeting;
public function run(): string {
return $this->sayHello('world');
}
}
"#;
let mut server = TestServer::new().await;
server.open("trait_hover.php", src).await;
let resp = server.hover("trait_hover.php", 9, 22).await;
let contents = resp["result"]["contents"].to_string();
assert!(
contents.contains("sayHello"),
"hover on trait-inherited method must return its signature, got: {contents}"
);
}
#[tokio::test]
async fn goto_definition_on_trait_use_resolves_to_trait_decl() {
let src = r#"<?php
trait Greeting {
public function sayHello(string $name): string {
return "Hello, {$name}";
}
}
class Greeter {
use Greeting;
}
"#;
let mut server = TestServer::new().await;
server.open("trait_def.php", src).await;
let resp = server.definition("trait_def.php", 7, 8).await;
let result = &resp["result"];
assert!(!result.is_null(), "expected a location, got null: {resp:?}");
let loc = if result.is_array() {
&result[0]
} else {
result
};
let line = loc["range"]["start"]["line"].as_u64().unwrap();
assert_eq!(
line, 1,
"definition must land on trait declaration (line 1), got line {line}"
);
}
#[tokio::test]
async fn goto_definition_jumps_to_trait_method() {
let src = r#"<?php
trait Greeting {
public function sayHello(string $name): string {
return "Hello, {$name}";
}
}
class Greeter {
use Greeting;
public function run(): string {
return $this->sayHello('world');
}
}
"#;
let mut server = TestServer::new().await;
server.open("trait_def2.php", src).await;
let resp = server.definition("trait_def2.php", 9, 22).await;
assert!(!resp["result"].is_null(), "expected a location: {resp:?}");
}
#[tokio::test]
async fn hover_resolves_methods_from_multiple_traits() {
let src = r#"<?php
trait A {
public function alpha(): int { return 1; }
}
trait B {
public function beta(): int { return 2; }
}
class Both {
use A;
use B;
public function run(): int {
return $this->alpha() + $this->beta();
}
}
"#;
let mut server = TestServer::new().await;
server.open("multi_trait.php", src).await;
let resp = server.hover("multi_trait.php", 11, 22).await;
let contents = resp["result"]["contents"].to_string();
assert!(
contents.contains("alpha"),
"hover on alpha() must mention it, got: {contents}"
);
let resp = server.hover("multi_trait.php", 11, 39).await;
let contents = resp["result"]["contents"].to_string();
assert!(
contents.contains("beta"),
"hover on beta() must mention it, got: {contents}"
);
}
#[tokio::test]
#[ignore = "$this-> completion does not yet surface trait members — tracked as gap"]
async fn completion_on_this_arrow_includes_trait_methods() {
let src = r#"<?php
trait Counter {
public function tick(): void {}
public function reset(): void {}
}
class Timer {
use Counter;
public function run(): void {
$this->t;
}
}
"#;
let mut server = TestServer::new().await;
server.open("trait_complete.php", src).await;
let resp = server.completion("trait_complete.php", 8, 16).await;
let items = resp["result"]["items"]
.as_array()
.or_else(|| resp["result"].as_array())
.expect("completion items");
let labels: Vec<&str> = items.iter().filter_map(|i| i["label"].as_str()).collect();
assert!(
labels.iter().any(|l| *l == "tick"),
"expected trait method `tick` in completions, got: {labels:?}"
);
}