use crate::common::create_test_backend;
use phpantom_lsp::Backend;
use tower_lsp::lsp_types::*;
fn highlight_at(
backend: &Backend,
uri: &str,
php: &str,
line: u32,
character: u32,
) -> Vec<DocumentHighlight> {
backend.update_ast(uri, php);
backend
.handle_document_highlight(uri, php, Position { line, character })
.unwrap_or_default()
}
fn assert_highlight(
h: &DocumentHighlight,
line: u32,
start_char: u32,
end_char: u32,
kind: DocumentHighlightKind,
) {
assert_eq!(
h.range.start.line, line,
"expected line {}, got {}",
line, h.range.start.line
);
assert_eq!(
h.range.start.character, start_char,
"expected start char {}, got {}",
start_char, h.range.start.character
);
assert_eq!(
h.range.end.character, end_char,
"expected end char {}, got {}",
end_char, h.range.end.character
);
assert_eq!(
h.kind,
Some(kind),
"expected kind {:?}, got {:?}",
kind,
h.kind
);
}
#[test]
fn highlight_variable_in_same_scope() {
let backend = create_test_backend();
let php = r#"<?php
function demo() {
$user = new User();
echo $user->name;
return $user;
}
"#;
let highlights = highlight_at(&backend, "file:///test.php", php, 2, 5);
assert_eq!(highlights.len(), 3);
assert_highlight(&highlights[0], 2, 4, 9, DocumentHighlightKind::WRITE);
assert_highlight(&highlights[1], 3, 9, 14, DocumentHighlightKind::READ);
assert_highlight(&highlights[2], 4, 11, 16, DocumentHighlightKind::READ);
}
#[test]
fn highlight_variable_scoped_to_function() {
let backend = create_test_backend();
let php = r#"<?php
function foo() {
$x = 1;
return $x;
}
function bar() {
$x = 2;
return $x;
}
"#;
let highlights = highlight_at(&backend, "file:///test.php", php, 2, 5);
assert_eq!(highlights.len(), 2);
assert_eq!(highlights[0].range.start.line, 2);
assert_eq!(highlights[1].range.start.line, 3);
}
#[test]
fn highlight_variable_parameter_is_write() {
let backend = create_test_backend();
let php = r#"<?php
function greet(string $name) {
echo $name;
}
"#;
let highlights = highlight_at(&backend, "file:///test.php", php, 2, 10);
assert!(highlights.len() >= 2);
let has_write = highlights
.iter()
.any(|h| h.kind == Some(DocumentHighlightKind::WRITE));
let has_read = highlights
.iter()
.any(|h| h.kind == Some(DocumentHighlightKind::READ));
assert!(
has_write,
"expected a WRITE highlight for the parameter definition"
);
assert!(has_read, "expected a READ highlight for the variable usage");
}
#[test]
fn highlight_class_references() {
let backend = create_test_backend();
let php = r#"<?php
class Foo {
public function bar(): Foo {
return new Foo();
}
}
"#;
let highlights = highlight_at(&backend, "file:///test.php", php, 2, 28);
assert!(
highlights.len() >= 2,
"expected at least 2 highlights for class Foo, got {}",
highlights.len()
);
for h in &highlights {
assert_eq!(h.kind, Some(DocumentHighlightKind::READ));
}
}
#[test]
fn highlight_class_declaration_highlights_all_references() {
let backend = create_test_backend();
let php = r#"<?php
class MyService {
public static function create(): MyService {
return new MyService();
}
}
"#;
let highlights = highlight_at(&backend, "file:///test.php", php, 1, 7);
assert!(
highlights.len() >= 2,
"expected class declaration to highlight references too, got {}",
highlights.len()
);
}
#[test]
fn highlight_method_accesses() {
let backend = create_test_backend();
let php = r#"<?php
class Calculator {
public function add(int $a): int { return $a; }
public function demo() {
$this->add(1);
$this->add(2);
}
}
"#;
let highlights = highlight_at(&backend, "file:///test.php", php, 4, 16);
assert!(
highlights.len() >= 2,
"expected at least 2 highlights for method 'add', got {}",
highlights.len()
);
}
#[test]
fn highlight_member_declaration_matches_accesses() {
let backend = create_test_backend();
let php = r#"<?php
class Dog {
public string $name;
public function greet() {
echo $this->name;
}
}
"#;
let highlights = highlight_at(&backend, "file:///test.php", php, 4, 21);
assert!(
!highlights.is_empty(),
"expected at least 1 highlight for property 'name', got {}",
highlights.len()
);
}
#[test]
fn highlight_function_calls() {
let backend = create_test_backend();
let php = r#"<?php
function helper() {}
helper();
helper();
"#;
let highlights = highlight_at(&backend, "file:///test.php", php, 2, 1);
assert!(
highlights.len() >= 2,
"expected at least 2 highlights for function 'helper', got {}",
highlights.len()
);
for h in &highlights {
assert_eq!(h.kind, Some(DocumentHighlightKind::READ));
}
}
#[test]
fn highlight_constant_references() {
let backend = create_test_backend();
let php = r#"<?php
class Config {
const MAX = 100;
public function check(int $v): bool {
return $v < self::MAX;
}
}
"#;
let highlights = highlight_at(&backend, "file:///test.php", php, 4, 26);
assert!(
!highlights.is_empty(),
"expected at least 1 highlight for constant 'MAX', got {}",
highlights.len()
);
}
#[test]
fn highlight_this_keyword() {
let backend = create_test_backend();
let php = r#"<?php
class Example {
private int $x;
public function setX(int $v): void {
$this->x = $v;
}
public function getX(): int {
return $this->x;
}
}
"#;
let highlights = highlight_at(&backend, "file:///test.php", php, 4, 9);
assert!(
highlights.len() >= 2,
"expected at least 2 highlights for $this, got {}",
highlights.len()
);
for h in &highlights {
assert_eq!(h.kind, Some(DocumentHighlightKind::READ));
}
}
#[test]
fn highlight_self_keyword() {
let backend = create_test_backend();
let php = r#"<?php
class Counter {
private static int $count = 0;
public static function increment(): void {
self::$count++;
}
public static function get(): int {
return self::$count;
}
}
"#;
let highlights = highlight_at(&backend, "file:///test.php", php, 4, 9);
assert!(
highlights.len() >= 2,
"expected at least 2 highlights for self keyword, got {}",
highlights.len()
);
}
#[test]
fn highlight_returns_none_on_whitespace() {
let backend = create_test_backend();
let php = r#"<?php
function foo() {}
"#;
let result = {
backend.update_ast("file:///test.php", php);
backend.handle_document_highlight(
"file:///test.php",
php,
Position {
line: 0,
character: 0,
},
)
};
assert!(
result.is_none(),
"expected None when cursor is on non-navigable token"
);
}
#[test]
fn highlight_foreach_variable() {
let backend = create_test_backend();
let php = r#"<?php
function process(array $items) {
foreach ($items as $item) {
echo $item;
}
}
"#;
let highlights = highlight_at(&backend, "file:///test.php", php, 2, 24);
assert!(
highlights.len() >= 2,
"expected at least 2 highlights for $item, got {}",
highlights.len()
);
let has_write = highlights
.iter()
.any(|h| h.kind == Some(DocumentHighlightKind::WRITE));
assert!(has_write, "foreach binding should be a WRITE highlight");
}
#[test]
fn highlight_variable_assignment_is_write() {
let backend = create_test_backend();
let php = r#"<?php
function test() {
$val = 1;
$val = 2;
echo $val;
}
"#;
let highlights = highlight_at(&backend, "file:///test.php", php, 2, 5);
assert_eq!(highlights.len(), 3);
assert_highlight(&highlights[0], 2, 4, 8, DocumentHighlightKind::WRITE);
assert_highlight(&highlights[1], 3, 4, 8, DocumentHighlightKind::WRITE);
assert_highlight(&highlights[2], 4, 9, 13, DocumentHighlightKind::READ);
}
#[test]
fn highlight_static_method() {
let backend = create_test_backend();
let php = r#"<?php
class Factory {
public static function make(): self { return new self(); }
public function demo() {
self::make();
static::make();
}
}
"#;
let highlights = highlight_at(&backend, "file:///test.php", php, 4, 15);
assert!(
highlights.len() >= 2,
"expected at least 2 highlights for static method 'make', got {}",
highlights.len()
);
}