use crate::common::create_test_backend;
use tower_lsp::lsp_types::*;
fn get_symbols(php: &str) -> Option<DocumentSymbolResponse> {
let backend = create_test_backend();
let uri = "file:///test.php";
backend.update_ast(uri, php);
backend.handle_document_symbol(uri, php)
}
#[allow(deprecated)]
fn flatten_symbols(
symbols: &[DocumentSymbol],
depth: usize,
out: &mut Vec<(String, SymbolKind, usize)>,
) {
for sym in symbols {
out.push((sym.name.clone(), sym.kind, depth));
if let Some(ref children) = sym.children {
flatten_symbols(children, depth + 1, out);
}
}
}
#[allow(deprecated)]
fn unwrap_nested(resp: DocumentSymbolResponse) -> Vec<DocumentSymbol> {
match resp {
DocumentSymbolResponse::Nested(syms) => syms,
_ => panic!("expected Nested response"),
}
}
#[test]
fn class_with_method_property_constant() {
let php = r#"<?php
class User {
const MAX_AGE = 150;
public string $name;
public function getName(): string {
return $this->name;
}
}
"#;
let resp = get_symbols(php).expect("should have symbols");
let symbols = unwrap_nested(resp);
assert_eq!(symbols.len(), 1, "one top-level class");
assert_eq!(symbols[0].name, "User");
assert_eq!(symbols[0].kind, SymbolKind::CLASS);
let children = symbols[0].children.as_ref().expect("class has children");
let names: Vec<&str> = children.iter().map(|c| c.name.as_str()).collect();
assert!(names.contains(&"MAX_AGE"), "constant present: {names:?}");
assert!(names.contains(&"$name"), "property present: {names:?}");
assert!(names.contains(&"getName"), "method present: {names:?}");
}
#[test]
fn interface_symbol_kind() {
let php = r#"<?php
interface Printable {
public function print(): void;
}
"#;
let resp = get_symbols(php).expect("should have symbols");
let symbols = unwrap_nested(resp);
assert_eq!(symbols[0].kind, SymbolKind::INTERFACE);
assert_eq!(symbols[0].name, "Printable");
}
#[test]
fn trait_symbol() {
let php = r#"<?php
trait Timestampable {
public function touch(): void {}
}
"#;
let resp = get_symbols(php).expect("should have symbols");
let symbols = unwrap_nested(resp);
assert_eq!(symbols[0].kind, SymbolKind::CLASS);
assert_eq!(symbols[0].name, "Timestampable");
let children = symbols[0].children.as_ref().unwrap();
assert_eq!(children.len(), 1);
assert_eq!(children[0].name, "touch");
}
#[test]
fn enum_with_cases() {
let php = r#"<?php
enum Status: string {
case Active = 'active';
case Inactive = 'inactive';
}
"#;
let resp = get_symbols(php).expect("should have symbols");
let symbols = unwrap_nested(resp);
assert_eq!(symbols[0].kind, SymbolKind::ENUM);
assert_eq!(symbols[0].name, "Status");
let children = symbols[0].children.as_ref().unwrap();
let case_names: Vec<&str> = children.iter().map(|c| c.name.as_str()).collect();
assert!(case_names.contains(&"Active"), "has Active case");
assert!(case_names.contains(&"Inactive"), "has Inactive case");
for child in children {
assert_eq!(child.kind, SymbolKind::ENUM_MEMBER);
}
}
#[test]
fn constructor_uses_constructor_kind() {
let php = r#"<?php
class Foo {
public function __construct() {}
}
"#;
let resp = get_symbols(php).expect("should have symbols");
let symbols = unwrap_nested(resp);
let children = symbols[0].children.as_ref().unwrap();
assert_eq!(children[0].name, "__construct");
assert_eq!(children[0].kind, SymbolKind::CONSTRUCTOR);
}
#[test]
fn multiple_classes_in_one_file() {
let php = r#"<?php
class Alpha {
public function run(): void {}
}
class Beta {
public int $count;
}
"#;
let resp = get_symbols(php).expect("should have symbols");
let symbols = unwrap_nested(resp);
assert_eq!(symbols.len(), 2);
assert_eq!(symbols[0].name, "Alpha");
assert_eq!(symbols[1].name, "Beta");
}
#[test]
fn standalone_function_appears_as_top_level_symbol() {
let php = r#"<?php
function helper(): string {
return 'hi';
}
"#;
let resp = get_symbols(php).expect("should have symbols");
let symbols = unwrap_nested(resp);
let func_symbols: Vec<_> = symbols
.iter()
.filter(|s| s.kind == SymbolKind::FUNCTION)
.collect();
assert_eq!(func_symbols.len(), 1);
assert_eq!(func_symbols[0].name, "helper");
}
#[test]
fn global_define_constant_appears() {
let php = r#"<?php
define('APP_VERSION', '1.0.0');
"#;
let resp = get_symbols(php);
if let Some(resp) = resp {
let symbols = unwrap_nested(resp);
for sym in &symbols {
if sym.name == "APP_VERSION" {
assert_eq!(sym.kind, SymbolKind::CONSTANT);
}
}
}
}
#[test]
fn empty_file_returns_none() {
let php = "<?php\n";
let resp = get_symbols(php);
assert!(resp.is_none(), "empty file should have no symbols");
}
#[allow(deprecated)]
#[test]
fn method_detail_shows_signature() {
let php = r#"<?php
class Calc {
public static function add(int $a, int $b): int {
return $a + $b;
}
}
"#;
let resp = get_symbols(php).expect("should have symbols");
let symbols = unwrap_nested(resp);
let children = symbols[0].children.as_ref().unwrap();
let add = &children[0];
assert_eq!(add.name, "add");
let detail = add.detail.as_deref().unwrap_or("");
assert!(
detail.contains("$a"),
"detail should contain parameter name: {detail}"
);
assert!(
detail.contains("int"),
"detail should contain type hint: {detail}"
);
assert!(
detail.contains("static"),
"detail should contain static: {detail}"
);
}
#[allow(deprecated)]
#[test]
fn class_detail_shows_extends_and_implements() {
let php = r#"<?php
interface Printable {}
class Base {}
class Child extends Base implements Printable {
public function print(): void {}
}
"#;
let resp = get_symbols(php).expect("should have symbols");
let symbols = unwrap_nested(resp);
let child = symbols.iter().find(|s| s.name == "Child").unwrap();
let detail = child.detail.as_deref().unwrap_or("");
assert!(
detail.contains("extends Base"),
"detail should show parent class: {detail}"
);
assert!(
detail.contains("implements Printable"),
"detail should show interfaces: {detail}"
);
}
#[allow(deprecated)]
#[test]
fn deprecated_class_has_deprecated_tag() {
let php = r#"<?php
/** @deprecated Use NewClass instead */
class OldClass {
public function run(): void {}
}
"#;
let resp = get_symbols(php).expect("should have symbols");
let symbols = unwrap_nested(resp);
let old = &symbols[0];
assert_eq!(old.name, "OldClass");
let tags = old.tags.as_ref().expect("should have tags");
assert!(tags.contains(&SymbolTag::DEPRECATED));
}
#[allow(deprecated)]
#[test]
fn deprecated_method_has_deprecated_tag() {
let php = r#"<?php
class Foo {
/** @deprecated */
public function old(): void {}
}
"#;
let resp = get_symbols(php).expect("should have symbols");
let symbols = unwrap_nested(resp);
let children = symbols[0].children.as_ref().unwrap();
let old_method = children.iter().find(|c| c.name == "old").unwrap();
let tags = old_method.tags.as_ref().expect("should have tags");
assert!(tags.contains(&SymbolTag::DEPRECATED));
}
#[allow(deprecated)]
#[test]
fn property_name_includes_dollar_prefix() {
let php = r#"<?php
class Foo {
public string $bar;
}
"#;
let resp = get_symbols(php).expect("should have symbols");
let symbols = unwrap_nested(resp);
let children = symbols[0].children.as_ref().unwrap();
let prop = children
.iter()
.find(|c| c.kind == SymbolKind::PROPERTY)
.unwrap();
assert_eq!(prop.name, "$bar");
}
#[allow(deprecated)]
#[test]
fn property_detail_shows_type() {
let php = r#"<?php
class Foo {
public string $bar;
}
"#;
let resp = get_symbols(php).expect("should have symbols");
let symbols = unwrap_nested(resp);
let children = symbols[0].children.as_ref().unwrap();
let prop = children
.iter()
.find(|c| c.kind == SymbolKind::PROPERTY)
.unwrap();
let detail = prop.detail.as_deref().unwrap_or("");
assert!(
detail.contains("string"),
"property detail should show type: {detail}"
);
}
#[allow(deprecated)]
#[test]
fn symbols_ordered_by_position() {
let php = r#"<?php
class Alpha {}
class Beta {}
class Gamma {}
"#;
let resp = get_symbols(php).expect("should have symbols");
let symbols = unwrap_nested(resp);
let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
assert_eq!(names, vec!["Alpha", "Beta", "Gamma"]);
}
#[test]
fn virtual_members_excluded_from_outline() {
let php = r#"<?php
/**
* @method string getName()
* @property string $email
*/
class User {
public function getId(): int { return 1; }
}
"#;
let resp = get_symbols(php).expect("should have symbols");
let symbols = unwrap_nested(resp);
let children = symbols[0].children.as_ref().unwrap();
let names: Vec<&str> = children.iter().map(|c| c.name.as_str()).collect();
assert!(names.contains(&"getId"), "real method present");
assert!(!names.contains(&"getName"), "virtual method excluded");
assert!(!names.contains(&"$email"), "virtual property excluded");
}
#[allow(deprecated)]
#[test]
fn symbols_are_hierarchically_nested() {
let php = r#"<?php
class Outer {
public function inner(): void {}
public int $val;
const C = 1;
}
"#;
let resp = get_symbols(php).expect("should have symbols");
let symbols = unwrap_nested(resp);
let mut flat = Vec::new();
flatten_symbols(&symbols, 0, &mut flat);
assert_eq!(flat[0], ("Outer".to_string(), SymbolKind::CLASS, 0));
let depth_1: Vec<_> = flat.iter().filter(|(_, _, d)| *d == 1).collect();
assert_eq!(depth_1.len(), 3, "3 children at depth 1");
}
#[allow(deprecated)]
#[test]
fn private_method_shows_private_in_detail() {
let php = r#"<?php
class Foo {
private function secret(): void {}
}
"#;
let resp = get_symbols(php).expect("should have symbols");
let symbols = unwrap_nested(resp);
let children = symbols[0].children.as_ref().unwrap();
let method = &children[0];
let detail = method.detail.as_deref().unwrap_or("");
assert!(
detail.contains("private"),
"detail should mention private: {detail}"
);
}
#[test]
fn class_and_function_coexist() {
let php = r#"<?php
class Foo {
public function bar(): void {}
}
function standalone(): int { return 1; }
"#;
let resp = get_symbols(php).expect("should have symbols");
let symbols = unwrap_nested(resp);
let class_count = symbols
.iter()
.filter(|s| s.kind == SymbolKind::CLASS)
.count();
let func_count = symbols
.iter()
.filter(|s| s.kind == SymbolKind::FUNCTION)
.count();
assert_eq!(class_count, 1, "one class");
assert_eq!(func_count, 1, "one function");
}
#[allow(deprecated)]
#[test]
fn enum_case_detail_shows_value() {
let php = r#"<?php
enum Color: string {
case Red = 'red';
case Blue = 'blue';
}
"#;
let resp = get_symbols(php).expect("should have symbols");
let symbols = unwrap_nested(resp);
let children = symbols[0].children.as_ref().unwrap();
let red = children.iter().find(|c| c.name == "Red").unwrap();
if let Some(ref detail) = red.detail {
assert!(
detail.contains("red"),
"enum case detail should show value: {detail}"
);
}
}
#[test]
fn abstract_class_appears_in_outline() {
let php = r#"<?php
abstract class Base {
abstract public function run(): void;
public function stop(): void {}
}
"#;
let resp = get_symbols(php).expect("should have symbols");
let symbols = unwrap_nested(resp);
assert_eq!(symbols[0].name, "Base");
assert_eq!(symbols[0].kind, SymbolKind::CLASS);
let children = symbols[0].children.as_ref().unwrap();
let method_names: Vec<&str> = children.iter().map(|c| c.name.as_str()).collect();
assert!(method_names.contains(&"run"), "abstract method present");
assert!(method_names.contains(&"stop"), "concrete method present");
}
#[test]
fn class_name_is_short_name_not_fqn() {
let php = r#"<?php
namespace App\Models;
class User {
public function getId(): int { return 1; }
}
"#;
let resp = get_symbols(php).expect("should have symbols");
let symbols = unwrap_nested(resp);
assert_eq!(symbols[0].name, "User");
}
#[allow(deprecated)]
#[test]
fn selection_range_covers_name_not_full_body() {
let php = r#"<?php
class VeryLongClassName {
public function method(): void {}
}
"#;
let resp = get_symbols(php).expect("should have symbols");
let symbols = unwrap_nested(resp);
let class_sym = &symbols[0];
assert!(
class_sym.range.end.line > class_sym.range.start.line,
"full range should span multiple lines"
);
assert_eq!(
class_sym.selection_range.start.line, class_sym.selection_range.end.line,
"selection range should be on one line"
);
}