use crate::common::{create_psr4_workspace, create_test_backend};
use tower_lsp::lsp_types::*;
fn get_code_lenses(backend: &phpantom_lsp::Backend, uri: &str, content: &str) -> Vec<CodeLens> {
backend.update_ast(uri, content);
backend.handle_code_lens(uri, content).unwrap_or_default()
}
fn lens_titles(lenses: &[CodeLens]) -> Vec<&str> {
lenses
.iter()
.filter_map(|l| l.command.as_ref().map(|c| c.title.as_str()))
.collect()
}
#[test]
fn parent_class_method_override() {
let backend = create_test_backend();
let content = r#"<?php
class Animal {
public function speak(): string { return ''; }
public function eat(): void {}
}
class Dog extends Animal {
public function speak(): string { return 'woof'; }
}
"#;
let uri = "file:///test.php";
let lenses = get_code_lenses(&backend, uri, content);
let titles = lens_titles(&lenses);
assert_eq!(titles.len(), 1);
assert_eq!(titles[0], "↑ Animal::speak");
}
#[test]
fn interface_method_implementation() {
let backend = create_test_backend();
let content = r#"<?php
interface Greetable {
public function greet(): string;
}
class Greeter implements Greetable {
public function greet(): string { return 'hello'; }
}
"#;
let uri = "file:///test.php";
let lenses = get_code_lenses(&backend, uri, content);
let titles = lens_titles(&lenses);
assert_eq!(titles.len(), 1);
assert_eq!(titles[0], "◆ Greetable::greet");
}
#[test]
fn no_lens_for_methods_without_prototype() {
let backend = create_test_backend();
let content = r#"<?php
class Standalone {
public function doSomething(): void {}
public function doMore(): void {}
}
"#;
let uri = "file:///test.php";
let lenses = get_code_lenses(&backend, uri, content);
assert!(lenses.is_empty());
}
#[test]
fn multiple_overrides_in_one_class() {
let backend = create_test_backend();
let content = r#"<?php
class Base {
public function foo(): void {}
public function bar(): void {}
public function baz(): void {}
}
class Child extends Base {
public function foo(): void {}
public function bar(): void {}
}
"#;
let uri = "file:///test.php";
let lenses = get_code_lenses(&backend, uri, content);
let titles = lens_titles(&lenses);
assert_eq!(titles.len(), 2);
assert!(titles.contains(&"↑ Base::foo"));
assert!(titles.contains(&"↑ Base::bar"));
}
#[test]
fn grandparent_override() {
let backend = create_test_backend();
let content = r#"<?php
class GrandParent_ {
public function legacy(): void {}
}
class Parent_ extends GrandParent_ {
}
class Child extends Parent_ {
public function legacy(): void {}
}
"#;
let uri = "file:///test.php";
let lenses = get_code_lenses(&backend, uri, content);
let titles = lens_titles(&lenses);
assert_eq!(titles.len(), 1);
assert_eq!(titles[0], "↑ GrandParent_::legacy");
}
#[test]
fn parent_overrides_grandparent_lens_points_to_parent() {
let backend = create_test_backend();
let content = r#"<?php
class A {
public function run(): void {}
}
class B extends A {
public function run(): void {}
}
class C extends B {
public function run(): void {}
}
"#;
let uri = "file:///test.php";
let lenses = get_code_lenses(&backend, uri, content);
let b_lens: Vec<_> = lenses
.iter()
.filter(|l| {
let line = l.range.start.line;
line > 5 && line < 9
})
.collect();
let c_lens: Vec<_> = lenses
.iter()
.filter(|l| {
let line = l.range.start.line;
line > 9
})
.collect();
assert_eq!(b_lens.len(), 1);
assert_eq!(b_lens[0].command.as_ref().unwrap().title, "↑ A::run");
assert_eq!(c_lens.len(), 1);
assert_eq!(c_lens[0].command.as_ref().unwrap().title, "↑ B::run");
}
#[test]
fn trait_method_override() {
let backend = create_test_backend();
let content = r#"<?php
trait Loggable {
public function log(string $msg): void {}
}
class Service {
use Loggable;
public function log(string $msg): void {
// custom logging
}
}
"#;
let uri = "file:///test.php";
let lenses = get_code_lenses(&backend, uri, content);
let titles = lens_titles(&lenses);
assert_eq!(titles.len(), 1);
assert_eq!(titles[0], "↑ Loggable::log");
}
#[test]
fn parent_takes_precedence_over_interface() {
let backend = create_test_backend();
let content = r#"<?php
interface Renderable {
public function render(): string;
}
class BaseView implements Renderable {
public function render(): string { return ''; }
}
class ChildView extends BaseView {
public function render(): string { return '<div>child</div>'; }
}
"#;
let uri = "file:///test.php";
let lenses = get_code_lenses(&backend, uri, content);
let base_lenses: Vec<_> = lenses.iter().filter(|l| l.range.start.line < 9).collect();
let child_lenses: Vec<_> = lenses.iter().filter(|l| l.range.start.line >= 9).collect();
assert_eq!(base_lenses.len(), 1);
assert_eq!(
base_lenses[0].command.as_ref().unwrap().title,
"◆ Renderable::render"
);
assert_eq!(child_lenses.len(), 1);
assert_eq!(
child_lenses[0].command.as_ref().unwrap().title,
"↑ BaseView::render"
);
}
#[test]
fn constructor_override() {
let backend = create_test_backend();
let content = r#"<?php
class BaseModel {
public function __construct() {}
}
class User extends BaseModel {
public function __construct(string $name) {
parent::__construct();
}
}
"#;
let uri = "file:///test.php";
let lenses = get_code_lenses(&backend, uri, content);
let titles = lens_titles(&lenses);
assert_eq!(titles.len(), 1);
assert_eq!(titles[0], "↑ BaseModel::__construct");
}
#[test]
fn interface_itself_has_no_lens() {
let backend = create_test_backend();
let content = r#"<?php
interface Cacheable {
public function getCacheKey(): string;
public function getCacheTTL(): int;
}
"#;
let uri = "file:///test.php";
let lenses = get_code_lenses(&backend, uri, content);
assert!(lenses.is_empty());
}
#[test]
fn lens_range_is_on_method_line() {
let backend = create_test_backend();
let content = r#"<?php
class Base {
public function process(): void {}
}
class Handler extends Base {
public function process(): void {}
}
"#;
let uri = "file:///test.php";
let lenses = get_code_lenses(&backend, uri, content);
assert_eq!(lenses.len(), 1);
let lens = &lenses[0];
assert_eq!(lens.range.start.line, 6);
assert_eq!(lens.range.start.character, 0);
}
#[test]
fn lens_command_uses_vscode_open() {
let backend = create_test_backend();
let content = r#"<?php
class Parent_ {
public function action(): void {}
}
class Child extends Parent_ {
public function action(): void {}
}
"#;
let uri = "file:///test.php";
let lenses = get_code_lenses(&backend, uri, content);
assert_eq!(lenses.len(), 1);
let cmd = lenses[0].command.as_ref().unwrap();
assert_eq!(cmd.command, "vscode.open");
assert!(cmd.arguments.is_some());
let args = cmd.arguments.as_ref().unwrap();
assert_eq!(args.len(), 1);
let uri_str = args[0].as_str().unwrap();
assert!(
uri_str.contains("#L"),
"URI should contain a #L fragment for the target position, got: {uri_str}"
);
}
#[test]
fn implements_multiple_interfaces() {
let backend = create_test_backend();
let content = r#"<?php
interface Countable_ {
public function count(): int;
}
interface Serializable_ {
public function serialize(): string;
}
class Collection implements Countable_, Serializable_ {
public function count(): int { return 0; }
public function serialize(): string { return ''; }
}
"#;
let uri = "file:///test.php";
let lenses = get_code_lenses(&backend, uri, content);
let titles = lens_titles(&lenses);
assert_eq!(titles.len(), 2);
assert!(titles.contains(&"◆ Countable_::count"));
assert!(titles.contains(&"◆ Serializable_::serialize"));
}
#[test]
fn interface_extends_interface() {
let backend = create_test_backend();
let content = r#"<?php
interface BaseRepo {
public function find(int $id): ?object;
}
interface UserRepo extends BaseRepo {
public function findByEmail(string $email): ?object;
}
class EloquentUserRepo implements UserRepo {
public function find(int $id): ?object { return null; }
public function findByEmail(string $email): ?object { return null; }
}
"#;
let uri = "file:///test.php";
let lenses = get_code_lenses(&backend, uri, content);
let titles = lens_titles(&lenses);
assert_eq!(titles.len(), 2);
assert!(titles.contains(&"◆ BaseRepo::find"));
assert!(titles.contains(&"◆ UserRepo::findByEmail"));
}
#[test]
fn cross_file_parent_class() {
let (backend, _dir) = create_psr4_workspace(
r#"{ "autoload": { "psr-4": { "App\\": "src/" } } }"#,
&[
(
"src/Base.php",
r#"<?php
namespace App;
class Base {
public function handle(): void {}
}
"#,
),
(
"src/Handler.php",
r#"<?php
namespace App;
class Handler extends Base {
public function handle(): void {}
}
"#,
),
],
);
let base_uri = format!("file://{}", _dir.path().join("src/Base.php").display());
let handler_uri = format!("file://{}", _dir.path().join("src/Handler.php").display());
let base_content = std::fs::read_to_string(_dir.path().join("src/Base.php")).unwrap();
let handler_content = std::fs::read_to_string(_dir.path().join("src/Handler.php")).unwrap();
backend.update_ast(&base_uri, &base_content);
backend.update_ast(&handler_uri, &handler_content);
let lenses = backend
.handle_code_lens(&handler_uri, &handler_content)
.unwrap_or_default();
let titles = lens_titles(&lenses);
assert_eq!(titles.len(), 1);
assert_eq!(titles[0], "↑ Base::handle");
}
#[test]
fn abstract_method_implementation() {
let backend = create_test_backend();
let content = r#"<?php
abstract class Shape {
abstract public function area(): float;
abstract public function perimeter(): float;
}
class Circle extends Shape {
public function area(): float { return 3.14; }
public function perimeter(): float { return 6.28; }
}
"#;
let uri = "file:///test.php";
let lenses = get_code_lenses(&backend, uri, content);
let titles = lens_titles(&lenses);
assert_eq!(titles.len(), 2);
assert!(titles.contains(&"↑ Shape::area"));
assert!(titles.contains(&"↑ Shape::perimeter"));
}
#[test]
fn static_method_override() {
let backend = create_test_backend();
let content = r#"<?php
class Factory {
public static function create(): static { return new static(); }
}
class UserFactory extends Factory {
public static function create(): static { return new static(); }
}
"#;
let uri = "file:///test.php";
let lenses = get_code_lenses(&backend, uri, content);
let titles = lens_titles(&lenses);
assert_eq!(titles.len(), 1);
assert_eq!(titles[0], "↑ Factory::create");
}
#[test]
fn empty_file_returns_none() {
let backend = create_test_backend();
let content = "<?php\n// nothing here\n";
let uri = "file:///test.php";
backend.update_ast(uri, content);
let result = backend.handle_code_lens(uri, content);
assert!(result.is_none());
}
#[test]
fn only_overriding_methods_get_lenses() {
let backend = create_test_backend();
let content = r#"<?php
class Transport {
public function send(): void {}
}
class EmailTransport extends Transport {
public function send(): void {}
public function formatBody(): string { return ''; }
public function addAttachment(): void {}
}
"#;
let uri = "file:///test.php";
let lenses = get_code_lenses(&backend, uri, content);
let titles = lens_titles(&lenses);
assert_eq!(titles.len(), 1);
assert_eq!(titles[0], "↑ Transport::send");
}
#[test]
fn cross_file_interface_implementation() {
let (backend, _dir) = create_psr4_workspace(
r#"{ "autoload": { "psr-4": { "App\\": "src/" } } }"#,
&[
(
"src/Printable.php",
r#"<?php
namespace App;
interface Printable {
public function print(): string;
}
"#,
),
(
"src/Document.php",
r#"<?php
namespace App;
class Document implements Printable {
public function print(): string { return 'doc'; }
}
"#,
),
],
);
let iface_uri = format!("file://{}", _dir.path().join("src/Printable.php").display());
let doc_uri = format!("file://{}", _dir.path().join("src/Document.php").display());
let iface_content = std::fs::read_to_string(_dir.path().join("src/Printable.php")).unwrap();
let doc_content = std::fs::read_to_string(_dir.path().join("src/Document.php")).unwrap();
backend.update_ast(&iface_uri, &iface_content);
backend.update_ast(&doc_uri, &doc_content);
let lenses = backend
.handle_code_lens(&doc_uri, &doc_content)
.unwrap_or_default();
let titles = lens_titles(&lenses);
assert_eq!(titles.len(), 1);
assert_eq!(titles[0], "◆ Printable::print");
}