use super::docblock::is_navigable_type;
use super::extraction::extract_symbol_map;
use super::*;
fn make_span(start: u32, end: u32, name: &str) -> SymbolSpan {
SymbolSpan {
start,
end,
kind: SymbolKind::ClassReference {
name: name.to_string(),
is_fqn: false,
},
}
}
#[test]
fn lookup_empty_map_returns_none() {
let map = SymbolMap::default();
assert!(map.lookup(0).is_none());
assert!(map.lookup(100).is_none());
}
#[test]
fn lookup_hit_at_start() {
let map = SymbolMap {
spans: vec![make_span(10, 15, "Foo")],
..Default::default()
};
assert!(map.lookup(10).is_some());
assert_eq!(map.lookup(10).unwrap().start, 10);
}
#[test]
fn lookup_hit_at_end_minus_one() {
let map = SymbolMap {
spans: vec![make_span(10, 15, "Foo")],
..Default::default()
};
assert!(map.lookup(14).is_some());
}
#[test]
fn lookup_miss_at_end() {
let map = SymbolMap {
spans: vec![make_span(10, 15, "Foo")],
..Default::default()
};
assert!(map.lookup(15).is_none());
}
#[test]
fn lookup_miss_before_first_span() {
let map = SymbolMap {
spans: vec![make_span(10, 15, "Foo")],
..Default::default()
};
assert!(map.lookup(5).is_none());
}
#[test]
fn lookup_miss_in_gap() {
let map = SymbolMap {
spans: vec![make_span(10, 15, "Foo"), make_span(20, 25, "Bar")],
..Default::default()
};
assert!(map.lookup(17).is_none());
}
#[test]
fn lookup_correct_span_in_sequence() {
let map = SymbolMap {
spans: vec![
make_span(10, 15, "Foo"),
make_span(20, 25, "Bar"),
make_span(30, 35, "Baz"),
],
..Default::default()
};
let result = map.lookup(22).unwrap();
if let SymbolKind::ClassReference { ref name, .. } = result.kind {
assert_eq!(name, "Bar");
} else {
panic!("Expected ClassReference");
}
}
#[test]
fn scalar_types_are_not_navigable() {
assert!(!is_navigable_type("int"));
assert!(!is_navigable_type("string"));
assert!(!is_navigable_type("bool"));
assert!(!is_navigable_type("void"));
assert!(!is_navigable_type("null"));
assert!(!is_navigable_type("mixed"));
assert!(!is_navigable_type("array"));
assert!(!is_navigable_type("callable"));
assert!(!is_navigable_type("float"));
assert!(!is_navigable_type("never"));
assert!(!is_navigable_type("iterable"));
assert!(!is_navigable_type("true"));
assert!(!is_navigable_type("false"));
assert!(!is_navigable_type("resource"));
assert!(!is_navigable_type("object"));
}
#[test]
fn class_names_are_navigable() {
assert!(is_navigable_type("Foo"));
assert!(is_navigable_type("Collection"));
assert!(is_navigable_type("App\\Models\\User"));
assert!(is_navigable_type("ResponseInterface"));
}
#[test]
fn case_insensitive_scalar_check() {
assert!(!is_navigable_type("INT"));
assert!(!is_navigable_type("String"));
assert!(!is_navigable_type("BOOL"));
}
#[test]
fn empty_name_is_not_navigable() {
assert!(!is_navigable_type(""));
}
fn parse_and_extract(php: &str) -> SymbolMap {
let arena = bumpalo::Bump::new();
let file_id = mago_database::file::FileId::new("test.php");
let program = mago_syntax::parser::parse_file_content(&arena, file_id, php);
extract_symbol_map(program, php)
}
#[test]
fn class_declaration_produces_class_declaration() {
let php = "<?php\nclass Foo {}\n";
let map = parse_and_extract(php);
let hit = map.lookup(php.find("Foo").unwrap() as u32);
assert!(hit.is_some());
if let SymbolKind::ClassDeclaration { ref name } = hit.unwrap().kind {
assert_eq!(name, "Foo");
} else {
panic!("Expected ClassDeclaration, got {:?}", hit.unwrap().kind);
}
}
#[test]
fn extends_produces_class_reference() {
let php = "<?php\nclass Foo extends Bar {}\n";
let map = parse_and_extract(php);
let bar_offset = php.find("Bar").unwrap() as u32;
let hit = map.lookup(bar_offset);
assert!(hit.is_some());
if let SymbolKind::ClassReference { ref name, .. } = hit.unwrap().kind {
assert_eq!(name, "Bar");
} else {
panic!("Expected ClassReference for Bar");
}
}
#[test]
fn extends_fqn_sets_is_fqn() {
let php = "<?php\nclass Foo extends \\App\\Bar {}\n";
let map = parse_and_extract(php);
let fqn_offset = php.find("\\App\\Bar").unwrap() as u32;
let hit = map.lookup(fqn_offset);
assert!(hit.is_some(), "Should have a span at the FQN");
if let SymbolKind::ClassReference { ref name, is_fqn } = hit.unwrap().kind {
assert_eq!(name, "App\\Bar");
assert!(is_fqn, "FQN should be marked as is_fqn");
} else {
panic!(
"Expected ClassReference for FQN, got {:?}",
hit.unwrap().kind
);
}
}
#[test]
fn implements_produces_class_reference() {
let php = "<?php\nclass Foo implements Baz, Qux {}\n";
let map = parse_and_extract(php);
let baz_offset = php.find("Baz").unwrap() as u32;
let hit = map.lookup(baz_offset);
assert!(hit.is_some());
if let SymbolKind::ClassReference { ref name, .. } = hit.unwrap().kind {
assert_eq!(name, "Baz");
} else {
panic!("Expected ClassReference for Baz");
}
let qux_offset = php.find("Qux").unwrap() as u32;
let hit = map.lookup(qux_offset);
assert!(hit.is_some());
if let SymbolKind::ClassReference { ref name, .. } = hit.unwrap().kind {
assert_eq!(name, "Qux");
} else {
panic!("Expected ClassReference for Qux");
}
}
#[test]
fn variable_produces_variable_span() {
let php = "<?php\nfunction test() { $foo = 1; }\n";
let map = parse_and_extract(php);
let offset = php.find("$foo").unwrap() as u32;
let hit = map.lookup(offset);
assert!(hit.is_some());
if let SymbolKind::Variable { ref name } = hit.unwrap().kind {
assert_eq!(name, "foo");
} else {
panic!("Expected Variable");
}
}
#[test]
fn function_call_produces_function_call_span() {
let php = "<?php\nfunction test() { strlen('hello'); }\n";
let map = parse_and_extract(php);
let offset = php.find("strlen").unwrap() as u32;
let hit = map.lookup(offset);
assert!(hit.is_some());
if let SymbolKind::FunctionCall { ref name, .. } = hit.unwrap().kind {
assert_eq!(name, "strlen");
} else {
panic!("Expected FunctionCall, got {:?}", hit.unwrap().kind);
}
}
#[test]
fn method_call_produces_member_access() {
let php = "<?php\nclass Foo { function test() { $this->bar(); } }\n";
let map = parse_and_extract(php);
let bar_offset = php.find("bar").unwrap() as u32;
let hit = map.lookup(bar_offset);
assert!(hit.is_some());
if let SymbolKind::MemberAccess {
ref subject_text,
ref member_name,
is_static,
is_method_call,
..
} = hit.unwrap().kind
{
assert_eq!(member_name, "bar");
assert_eq!(subject_text, "$this");
assert!(!is_static);
assert!(is_method_call);
} else {
panic!("Expected MemberAccess");
}
}
#[test]
fn static_method_call_produces_member_access() {
let php = "<?php\nclass Foo { function test() { self::create(); } }\n";
let map = parse_and_extract(php);
let offset = php.find("create").unwrap() as u32;
let hit = map.lookup(offset);
assert!(hit.is_some());
if let SymbolKind::MemberAccess {
ref subject_text,
ref member_name,
is_static,
is_method_call,
..
} = hit.unwrap().kind
{
assert_eq!(member_name, "create");
assert_eq!(subject_text, "self");
assert!(is_static);
assert!(is_method_call);
} else {
panic!("Expected MemberAccess");
}
}
#[test]
fn property_access_produces_member_access() {
let php = "<?php\nclass Foo { function test() { $this->name; } }\n";
let map = parse_and_extract(php);
let arrow_pos = php.find("->name").unwrap();
let name_offset = (arrow_pos + 2) as u32;
let hit = map.lookup(name_offset);
assert!(hit.is_some());
if let SymbolKind::MemberAccess {
ref member_name,
is_method_call,
..
} = hit.unwrap().kind
{
assert_eq!(member_name, "name");
assert!(!is_method_call);
} else {
panic!("Expected MemberAccess");
}
}
#[test]
fn type_hint_produces_class_reference() {
let php = "<?php\nfunction test(Foo $x): Bar { }\n";
let map = parse_and_extract(php);
let foo_offset = php.find("Foo").unwrap() as u32;
let hit = map.lookup(foo_offset);
assert!(hit.is_some());
if let SymbolKind::ClassReference { ref name, .. } = hit.unwrap().kind {
assert_eq!(name, "Foo");
} else {
panic!("Expected ClassReference for Foo");
}
let bar_offset = php.find("Bar").unwrap() as u32;
let hit = map.lookup(bar_offset);
assert!(hit.is_some());
if let SymbolKind::ClassReference { ref name, .. } = hit.unwrap().kind {
assert_eq!(name, "Bar");
} else {
panic!("Expected ClassReference for Bar");
}
}
#[test]
fn scalar_type_hint_not_in_map() {
let php = "<?php\nfunction test(int $x): string { }\n";
let map = parse_and_extract(php);
let int_offset = php.find("int").unwrap() as u32;
assert!(map.lookup(int_offset).is_none());
let string_offset = php.find("string").unwrap() as u32;
assert!(map.lookup(string_offset).is_none());
}
#[test]
fn new_expression_produces_class_reference() {
let php = "<?php\nfunction test() { $x = new Foo(); }\n";
let map = parse_and_extract(php);
let foo_offset = php.find("Foo").unwrap() as u32;
let hit = map.lookup(foo_offset);
assert!(hit.is_some());
if let SymbolKind::ClassReference { ref name, .. } = hit.unwrap().kind {
assert_eq!(name, "Foo");
} else {
panic!("Expected ClassReference for Foo");
}
}
#[test]
fn catch_type_produces_class_reference() {
let php = "<?php\ntry {} catch (RuntimeException $e) {}\n";
let map = parse_and_extract(php);
let offset = php.find("RuntimeException").unwrap() as u32;
let hit = map.lookup(offset);
assert!(hit.is_some());
if let SymbolKind::ClassReference { ref name, .. } = hit.unwrap().kind {
assert_eq!(name, "RuntimeException");
} else {
panic!("Expected ClassReference");
}
}
#[test]
fn self_keyword_produces_self_static_parent() {
let php = "<?php\nclass Foo { function test(): self { } }\n";
let map = parse_and_extract(php);
let offset = php.find("self").unwrap() as u32;
let hit = map.lookup(offset);
assert!(hit.is_some());
if let SymbolKind::SelfStaticParent(kind) = &hit.unwrap().kind {
assert_eq!(*kind, SelfStaticParentKind::Self_);
} else {
panic!("Expected SelfStaticParent");
}
}
#[test]
fn whitespace_offset_returns_none() {
let php = "<?php\nclass Foo {}\n";
let map = parse_and_extract(php);
let foo_end = php.find("Foo").unwrap() + 3;
let hit = map.lookup((foo_end + 1) as u32);
assert!(hit.is_none());
}
#[test]
fn string_interior_not_navigable() {
let php = "<?php\n$x = 'SomeClass';\n";
let map = parse_and_extract(php);
let some_offset = php.find("SomeClass").unwrap() as u32;
let hit = map.lookup(some_offset);
if let Some(span) = hit
&& let SymbolKind::ClassReference { .. } = &span.kind
{
panic!("Should not produce ClassReference inside a string literal");
}
}
#[test]
fn chained_method_call_subject_text() {
let php = "<?php\nclass Foo { function test() { $this->getService()->find(); } }\n";
let map = parse_and_extract(php);
let find_offset = php.find("find").unwrap() as u32;
let hit = map.lookup(find_offset);
assert!(hit.is_some());
if let SymbolKind::MemberAccess {
ref subject_text,
ref member_name,
..
} = hit.unwrap().kind
{
assert_eq!(member_name, "find");
assert_eq!(subject_text, "$this->getService()");
} else {
panic!("Expected MemberAccess");
}
}
#[test]
fn class_constant_access_produces_member_access() {
let php = "<?php\nclass Foo { function test() { self::MY_CONST; } }\n";
let map = parse_and_extract(php);
let offset = php.find("MY_CONST").unwrap() as u32;
let hit = map.lookup(offset);
assert!(hit.is_some());
if let SymbolKind::MemberAccess {
ref member_name,
is_static,
..
} = hit.unwrap().kind
{
assert_eq!(member_name, "MY_CONST");
assert!(is_static);
} else {
panic!("Expected MemberAccess");
}
}
#[test]
fn trait_use_produces_class_reference() {
let php = "<?php\nclass Foo { use SomeTrait; }\n";
let map = parse_and_extract(php);
let offset = php.find("SomeTrait").unwrap() as u32;
let hit = map.lookup(offset);
assert!(hit.is_some());
if let SymbolKind::ClassReference { ref name, .. } = hit.unwrap().kind {
assert_eq!(name, "SomeTrait");
} else {
panic!("Expected ClassReference");
}
}
#[test]
fn docblock_param_class_reference() {
let php = concat!(
"<?php\n",
"class Foo {\n",
" /**\n",
" * @param UserService $service\n",
" * @return ResponseInterface\n",
" */\n",
" public function test($service) {}\n",
"}\n",
);
let map = parse_and_extract(php);
let user_service_offset = php.find("UserService").unwrap() as u32;
let hit = map.lookup(user_service_offset);
assert!(hit.is_some(), "Should find UserService in docblock");
if let SymbolKind::ClassReference { ref name, .. } = hit.unwrap().kind {
assert_eq!(name, "UserService");
} else {
panic!("Expected ClassReference for UserService");
}
let response_offset = php.find("ResponseInterface").unwrap() as u32;
let hit = map.lookup(response_offset);
assert!(hit.is_some(), "Should find ResponseInterface in docblock");
if let SymbolKind::ClassReference { ref name, .. } = hit.unwrap().kind {
assert_eq!(name, "ResponseInterface");
} else {
panic!("Expected ClassReference for ResponseInterface");
}
}
#[test]
fn docblock_scalar_param_not_navigable() {
let php = concat!(
"<?php\n",
"/**\n",
" * @param string $name\n",
" */\n",
"function test($name) {}\n",
);
let map = parse_and_extract(php);
let string_offset = php.find("string").unwrap() as u32;
let hit = map.lookup(string_offset);
if let Some(span) = hit
&& let SymbolKind::ClassReference { .. } = &span.kind
{
panic!("Scalar type 'string' should not produce a ClassReference");
}
}
#[test]
fn nullable_type_hint_produces_class_reference() {
let php = "<?php\nfunction test(?Foo $x) {}\n";
let map = parse_and_extract(php);
let foo_offset = php.find("Foo").unwrap() as u32;
let hit = map.lookup(foo_offset);
assert!(hit.is_some());
if let SymbolKind::ClassReference { ref name, .. } = hit.unwrap().kind {
assert_eq!(name, "Foo");
} else {
panic!("Expected ClassReference for nullable Foo");
}
}
#[test]
fn interface_declaration_produces_declaration() {
let php = "<?php\ninterface Serializable {}\n";
let map = parse_and_extract(php);
let offset = php.find("Serializable").unwrap() as u32;
let hit = map.lookup(offset);
assert!(hit.is_some());
if let SymbolKind::ClassDeclaration { ref name } = hit.unwrap().kind {
assert_eq!(name, "Serializable");
} else {
panic!("Expected ClassDeclaration, got {:?}", hit.unwrap().kind);
}
}
#[test]
fn enum_declaration_produces_declaration() {
let php = "<?php\nenum Color { case Red; case Blue; }\n";
let map = parse_and_extract(php);
let offset = php.find("Color").unwrap() as u32;
let hit = map.lookup(offset);
assert!(hit.is_some());
if let SymbolKind::ClassDeclaration { ref name } = hit.unwrap().kind {
assert_eq!(name, "Color");
} else {
panic!("Expected ClassDeclaration, got {:?}", hit.unwrap().kind);
}
}
#[test]
fn enum_case_produces_member_declaration() {
let php = "<?php\nenum Color { case Red; case Blue; }\n";
let map = parse_and_extract(php);
let red_offset = php.find("Red").unwrap() as u32;
let hit = map.lookup(red_offset);
assert!(hit.is_some(), "Expected a symbol span for enum case Red");
if let SymbolKind::MemberDeclaration {
ref name,
is_static,
} = hit.unwrap().kind
{
assert_eq!(name, "Red");
assert!(is_static, "Enum cases are accessed statically");
} else {
panic!(
"Expected MemberDeclaration for enum case Red, got {:?}",
hit.unwrap().kind
);
}
let blue_offset = php.find("Blue").unwrap() as u32;
let hit = map.lookup(blue_offset);
assert!(hit.is_some(), "Expected a symbol span for enum case Blue");
if let SymbolKind::MemberDeclaration {
ref name,
is_static,
} = hit.unwrap().kind
{
assert_eq!(name, "Blue");
assert!(is_static, "Enum cases are accessed statically");
} else {
panic!(
"Expected MemberDeclaration for enum case Blue, got {:?}",
hit.unwrap().kind
);
}
}
#[test]
fn backed_enum_case_produces_member_declaration() {
let php = "<?php\nenum TaskType: int { case Task = 1; case Issue = 2; }\n";
let map = parse_and_extract(php);
let issue_offset = php.find("Issue").unwrap() as u32;
let hit = map.lookup(issue_offset);
assert!(
hit.is_some(),
"Expected a symbol span for backed enum case Issue"
);
if let SymbolKind::MemberDeclaration {
ref name,
is_static,
} = hit.unwrap().kind
{
assert_eq!(name, "Issue");
assert!(is_static, "Enum cases are accessed statically");
} else {
panic!(
"Expected MemberDeclaration for backed enum case Issue, got {:?}",
hit.unwrap().kind
);
}
}
#[test]
fn closure_param_type_hint() {
let php = "<?php\n$f = function(Foo $x): Bar {};\n";
let map = parse_and_extract(php);
let foo_offset = php.find("Foo").unwrap() as u32;
let hit = map.lookup(foo_offset);
assert!(hit.is_some());
if let SymbolKind::ClassReference { ref name, .. } = hit.unwrap().kind {
assert_eq!(name, "Foo");
} else {
panic!("Expected ClassReference for Foo");
}
}
#[test]
fn instanceof_rhs_produces_class_reference() {
let php = "<?php\nfunction test($x) { if ($x instanceof Foo) {} }\n";
let map = parse_and_extract(php);
let foo_offset = php.find("Foo").unwrap() as u32;
let hit = map.lookup(foo_offset);
assert!(hit.is_some());
if let SymbolKind::ClassReference { ref name, .. } = hit.unwrap().kind {
assert_eq!(name, "Foo");
} else {
panic!("Expected ClassReference for instanceof Foo");
}
}
#[test]
fn docblock_union_type_produces_multiple_references() {
let php = concat!(
"<?php\n",
"/**\n",
" * @return Foo|Bar\n",
" */\n",
"function test() {}\n",
);
let map = parse_and_extract(php);
let foo_offset = php.find("Foo").unwrap() as u32;
let hit = map.lookup(foo_offset);
assert!(hit.is_some(), "Should find Foo in union return type");
if let SymbolKind::ClassReference { ref name, .. } = hit.unwrap().kind {
assert_eq!(name, "Foo");
}
let bar_offset = php.find("Bar").unwrap() as u32;
let hit = map.lookup(bar_offset);
assert!(hit.is_some(), "Should find Bar in union return type");
if let SymbolKind::ClassReference { ref name, .. } = hit.unwrap().kind {
assert_eq!(name, "Bar");
}
}
#[test]
fn docblock_nullable_type() {
let php = concat!(
"<?php\n",
"/**\n",
" * @return ?Foo\n",
" */\n",
"function test() {}\n",
);
let map = parse_and_extract(php);
let foo_offset = php.find("Foo").unwrap() as u32;
let hit = map.lookup(foo_offset);
assert!(hit.is_some(), "Should find Foo in nullable return type");
if let SymbolKind::ClassReference { ref name, .. } = hit.unwrap().kind {
assert_eq!(name, "Foo");
}
}
#[test]
fn docblock_fqn_type() {
let php = concat!(
"<?php\n",
"/**\n",
" * @return \\App\\Models\\User\n",
" */\n",
"function test() {}\n",
);
let map = parse_and_extract(php);
let user_offset = php.find("\\App\\Models\\User").unwrap() as u32;
let hit = map.lookup(user_offset);
assert!(hit.is_some(), "Should find FQN type in docblock");
if let SymbolKind::ClassReference { ref name, is_fqn } = hit.unwrap().kind {
assert_eq!(name, "App\\Models\\User");
assert!(is_fqn, "Docblock FQN type should have is_fqn = true");
} else {
panic!("Expected ClassReference, got {:?}", hit.unwrap().kind);
}
}
#[test]
fn docblock_this_produces_self_static_parent() {
let php = concat!(
"<?php\n",
"class Foo {\n",
" /**\n",
" * @return Collection<Item, $this>\n",
" */\n",
" public function items() {}\n",
"}\n",
);
let map = parse_and_extract(php);
let this_offset = php.find("$this").unwrap() as u32;
let hit = map.lookup(this_offset);
assert!(hit.is_some(), "Should find $this in docblock generic arg");
if let SymbolKind::SelfStaticParent(kind) = &hit.unwrap().kind {
assert_eq!(*kind, SelfStaticParentKind::This);
} else {
panic!(
"Expected SelfStaticParent for $this, got {:?}",
hit.unwrap().kind
);
}
}
#[test]
fn attribute_class_reference() {
let php = concat!(
"<?php\n",
"#[\\Illuminate\\Database\\Eloquent\\Attributes\\CollectedBy(ReviewCollection::class)]\n",
"class Review {}\n",
);
let map = parse_and_extract(php);
let attr_offset = php
.find("\\Illuminate\\Database\\Eloquent\\Attributes\\CollectedBy")
.unwrap() as u32;
let hit = map.lookup(attr_offset);
assert!(hit.is_some(), "Should find attribute class reference");
if let SymbolKind::ClassReference { ref name, is_fqn } = hit.unwrap().kind {
assert_eq!(
name,
"Illuminate\\Database\\Eloquent\\Attributes\\CollectedBy"
);
assert!(is_fqn, "Attribute FQN should have is_fqn = true");
} else {
panic!(
"Expected ClassReference for attribute, got {:?}",
hit.unwrap().kind
);
}
let rc_offset = php.find("ReviewCollection").unwrap() as u32;
let hit = map.lookup(rc_offset);
assert!(
hit.is_some(),
"Should find ReviewCollection in attribute argument"
);
if let SymbolKind::ClassReference { ref name, .. } = hit.unwrap().kind {
assert_eq!(name, "ReviewCollection");
} else {
panic!(
"Expected ClassReference for ReviewCollection, got {:?}",
hit.unwrap().kind
);
}
let review_offset = php.find("class Review").unwrap() as u32 + 6; let hit = map.lookup(review_offset);
assert!(hit.is_some(), "Should find Review declaration");
if let SymbolKind::ClassDeclaration { ref name } = hit.unwrap().kind {
assert_eq!(name, "Review");
} else {
panic!(
"Expected ClassDeclaration for Review, got {:?}",
hit.unwrap().kind
);
}
}
#[test]
fn fqn_type_hint_in_parameter() {
let php = "<?php\nfunction test(\\Illuminate\\Support\\Collection $c) {}\n";
let map = parse_and_extract(php);
let fqn_offset = php.find("\\Illuminate\\Support\\Collection").unwrap() as u32;
let hit = map.lookup(fqn_offset);
assert!(hit.is_some(), "Should find FQN type hint in parameter");
if let SymbolKind::ClassReference { ref name, is_fqn } = hit.unwrap().kind {
assert_eq!(name, "Illuminate\\Support\\Collection");
assert!(is_fqn, "FQN parameter type hint should have is_fqn = true");
} else {
panic!(
"Expected ClassReference for FQN param hint, got {:?}",
hit.unwrap().kind
);
}
}
#[test]
fn fqn_extends_class_reference() {
let php = "<?php\nclass Review extends \\Illuminate\\Database\\Eloquent\\Model {}\n";
let map = parse_and_extract(php);
let fqn_offset = php.find("\\Illuminate\\Database\\Eloquent\\Model").unwrap() as u32;
let hit = map.lookup(fqn_offset);
assert!(hit.is_some(), "Should find FQN in extends clause");
if let SymbolKind::ClassReference { ref name, is_fqn } = hit.unwrap().kind {
assert_eq!(name, "Illuminate\\Database\\Eloquent\\Model");
assert!(is_fqn, "FQN extends should have is_fqn = true");
} else {
panic!(
"Expected ClassReference for FQN extends, got {:?}",
hit.unwrap().kind
);
}
}
#[test]
fn fqn_lookup_at_middle_of_name() {
let php = concat!(
"<?php\n",
"#[\\Illuminate\\Database\\Eloquent\\Attributes\\CollectedBy(ReviewCollection::class)]\n",
"class Review extends \\Illuminate\\Database\\Eloquent\\Model\n",
"{\n",
" /** @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany<Review, $this> */\n",
" public function replies(): mixed { return $this->hasMany(Review::class); }\n",
"}\n",
);
let map = parse_and_extract(php);
let cb_offset = php.find("CollectedBy").unwrap() as u32;
let hit = map.lookup(cb_offset);
assert!(
hit.is_some(),
"Should find attribute FQN when cursor is on 'CollectedBy'"
);
if let SymbolKind::ClassReference { ref name, is_fqn } = hit.unwrap().kind {
assert_eq!(
name,
"Illuminate\\Database\\Eloquent\\Attributes\\CollectedBy"
);
assert!(is_fqn);
} else {
panic!("Expected ClassReference, got {:?}", hit.unwrap().kind);
}
let db_attr_offset = php.find("Database").unwrap() as u32;
let hit = map.lookup(db_attr_offset);
assert!(
hit.is_some(),
"Should find attribute FQN when cursor is on 'Database'"
);
if let SymbolKind::ClassReference { ref name, is_fqn } = hit.unwrap().kind {
assert_eq!(
name,
"Illuminate\\Database\\Eloquent\\Attributes\\CollectedBy"
);
assert!(is_fqn);
} else {
panic!("Expected ClassReference, got {:?}", hit.unwrap().kind);
}
let model_offset = php.find("Model\n").unwrap() as u32;
let hit = map.lookup(model_offset);
assert!(
hit.is_some(),
"Should find extends FQN when cursor is on 'Model'"
);
if let SymbolKind::ClassReference { ref name, is_fqn } = hit.unwrap().kind {
assert_eq!(name, "Illuminate\\Database\\Eloquent\\Model");
assert!(is_fqn);
} else {
panic!("Expected ClassReference, got {:?}", hit.unwrap().kind);
}
let extends_line_start = php.find("class Review extends").unwrap();
let eloquent_in_extends =
php[extends_line_start..].find("Eloquent").unwrap() + extends_line_start;
let hit = map.lookup(eloquent_in_extends as u32);
assert!(
hit.is_some(),
"Should find extends FQN when cursor is on 'Eloquent'"
);
if let SymbolKind::ClassReference { ref name, is_fqn } = hit.unwrap().kind {
assert_eq!(name, "Illuminate\\Database\\Eloquent\\Model");
assert!(is_fqn);
} else {
panic!("Expected ClassReference, got {:?}", hit.unwrap().kind);
}
let hm_offset = php.find("HasMany").unwrap() as u32;
let hit = map.lookup(hm_offset);
assert!(
hit.is_some(),
"Should find docblock FQN when cursor is on 'HasMany'"
);
if let SymbolKind::ClassReference { ref name, is_fqn } = hit.unwrap().kind {
assert_eq!(name, "Illuminate\\Database\\Eloquent\\Relations\\HasMany");
assert!(is_fqn);
} else {
panic!("Expected ClassReference, got {:?}", hit.unwrap().kind);
}
let rel_offset = php.find("Relations").unwrap() as u32;
let hit = map.lookup(rel_offset);
assert!(
hit.is_some(),
"Should find docblock FQN when cursor is on 'Relations'"
);
if let SymbolKind::ClassReference { ref name, is_fqn } = hit.unwrap().kind {
assert_eq!(name, "Illuminate\\Database\\Eloquent\\Relations\\HasMany");
assert!(is_fqn);
} else {
panic!("Expected ClassReference, got {:?}", hit.unwrap().kind);
}
let docblock_start = php.find("/** @return").unwrap();
let this_in_doc = php[docblock_start..].find("$this").unwrap() + docblock_start;
let hit = map.lookup(this_in_doc as u32);
assert!(hit.is_some(), "Should find $this in docblock generic arg");
if let SymbolKind::SelfStaticParent(kind) = &hit.unwrap().kind {
assert_eq!(*kind, SelfStaticParentKind::This);
} else {
panic!(
"Expected SelfStaticParent for $this, got {:?}",
hit.unwrap().kind
);
}
}
#[test]
fn template_tag_bound_type_produces_class_reference() {
let php = concat!(
"<?php\n",
"/**\n",
" * @template-covariant TNode of AstNode\n",
" */\n",
"class Foo {}\n",
);
let map = parse_and_extract(php);
let ast_offset = php.find("AstNode").unwrap() as u32;
let hit = map.lookup(ast_offset);
assert!(
hit.is_some(),
"Should find bound type AstNode in @template tag"
);
if let SymbolKind::ClassReference { ref name, is_fqn } = hit.unwrap().kind {
assert_eq!(name, "AstNode");
assert!(!is_fqn);
} else {
panic!(
"Expected ClassReference for AstNode, got {:?}",
hit.unwrap().kind
);
}
}
#[test]
fn template_tag_without_bound_produces_no_span() {
let php = concat!(
"<?php\n",
"/**\n",
" * @template T\n",
" */\n",
"class Foo {}\n",
);
let map = parse_and_extract(php);
let t_offset = php.find(" T\n").unwrap() as u32 + 1; let hit = map.lookup(t_offset);
assert!(
hit.is_none(),
"Template parameter name should not be navigable"
);
}
#[test]
fn template_tag_fqn_bound() {
let php = concat!(
"<?php\n",
"/**\n",
" * @template T of \\App\\Contracts\\Renderable\n",
" */\n",
"class Foo {}\n",
);
let map = parse_and_extract(php);
let r_offset = php.find("\\App\\Contracts\\Renderable").unwrap() as u32;
let hit = map.lookup(r_offset);
assert!(hit.is_some(), "Should find FQN bound type");
if let SymbolKind::ClassReference { ref name, is_fqn } = hit.unwrap().kind {
assert_eq!(name, "App\\Contracts\\Renderable");
assert!(is_fqn);
} else {
panic!("Expected ClassReference, got {:?}", hit.unwrap().kind);
}
}
#[test]
fn template_covariant_and_contravariant_tags() {
let php = concat!(
"<?php\n",
"/**\n",
" * @template-covariant TOut of Output\n",
" * @template-contravariant TIn of Input\n",
" */\n",
"class Foo {}\n",
);
let map = parse_and_extract(php);
let out_offset = php.find("Output").unwrap() as u32;
let hit = map.lookup(out_offset);
assert!(hit.is_some(), "Should find Output bound");
if let SymbolKind::ClassReference { ref name, .. } = hit.unwrap().kind {
assert_eq!(name, "Output");
} else {
panic!("Expected ClassReference for Output");
}
let in_offset = php.find("Input").unwrap() as u32;
let hit = map.lookup(in_offset);
assert!(hit.is_some(), "Should find Input bound");
if let SymbolKind::ClassReference { ref name, .. } = hit.unwrap().kind {
assert_eq!(name, "Input");
} else {
panic!("Expected ClassReference for Input");
}
}
#[test]
fn phpstan_template_tag() {
let php = concat!(
"<?php\n",
"/**\n",
" * @phpstan-template T of Collection\n",
" */\n",
"class Foo {}\n",
);
let map = parse_and_extract(php);
let c_offset = php.find("Collection").unwrap() as u32;
let hit = map.lookup(c_offset);
assert!(
hit.is_some(),
"Should find Collection bound from @phpstan-template"
);
if let SymbolKind::ClassReference { ref name, .. } = hit.unwrap().kind {
assert_eq!(name, "Collection");
} else {
panic!("Expected ClassReference for Collection");
}
}
#[test]
fn var_def_assignment_in_function() {
let php = "<?php\nfunction foo() {\n $x = 42;\n}\n";
let map = parse_and_extract(php);
assert!(
!map.var_defs.is_empty(),
"Should have at least one VarDefSite"
);
let def = map
.var_defs
.iter()
.find(|d| d.name == "x")
.expect("Should find $x def");
assert_eq!(def.kind, VarDefKind::Assignment);
assert_ne!(
def.scope_start, 0,
"scope_start should be function body brace, not top-level"
);
}
#[test]
fn var_def_parameter_in_function() {
let php = "<?php\nfunction greet(string $name) {\n echo $name;\n}\n";
let map = parse_and_extract(php);
let def = map
.var_defs
.iter()
.find(|d| d.name == "name" && d.kind == VarDefKind::Parameter);
assert!(def.is_some(), "Should find parameter $name as VarDefSite");
}
#[test]
fn var_def_foreach_key_and_value() {
let php = "<?php\nfunction f() {\n foreach ($items as $key => $val) { }\n}\n";
let map = parse_and_extract(php);
let key_def = map
.var_defs
.iter()
.find(|d| d.name == "key" && d.kind == VarDefKind::Foreach);
let val_def = map
.var_defs
.iter()
.find(|d| d.name == "val" && d.kind == VarDefKind::Foreach);
assert!(key_def.is_some(), "Should find foreach key $key");
assert!(val_def.is_some(), "Should find foreach value $val");
}
#[test]
fn var_def_catch_variable() {
let php = "<?php\nfunction f() {\n try { } catch (Exception $e) { }\n}\n";
let map = parse_and_extract(php);
let def = map
.var_defs
.iter()
.find(|d| d.name == "e" && d.kind == VarDefKind::Catch);
assert!(def.is_some(), "Should find catch variable $e");
}
#[test]
fn var_def_static_variable() {
let php = "<?php\nfunction f() {\n static $counter = 0;\n}\n";
let map = parse_and_extract(php);
let def = map
.var_defs
.iter()
.find(|d| d.name == "counter" && d.kind == VarDefKind::StaticDecl);
assert!(def.is_some(), "Should find static variable $counter");
}
#[test]
fn var_def_global_variable() {
let php = "<?php\nfunction f() {\n global $db;\n}\n";
let map = parse_and_extract(php);
let def = map
.var_defs
.iter()
.find(|d| d.name == "db" && d.kind == VarDefKind::GlobalDecl);
assert!(def.is_some(), "Should find global variable $db");
}
#[test]
fn var_def_array_destructuring() {
let php = "<?php\nfunction f() {\n [$a, $b] = explode(',', $str);\n}\n";
let map = parse_and_extract(php);
let a_def = map
.var_defs
.iter()
.find(|d| d.name == "a" && d.kind == VarDefKind::ArrayDestructuring);
let b_def = map
.var_defs
.iter()
.find(|d| d.name == "b" && d.kind == VarDefKind::ArrayDestructuring);
assert!(a_def.is_some(), "Should find $a from array destructuring");
assert!(b_def.is_some(), "Should find $b from array destructuring");
}
#[test]
fn var_def_list_destructuring() {
let php = "<?php\nfunction f() {\n list($a, $b) = func();\n}\n";
let map = parse_and_extract(php);
let a_def = map
.var_defs
.iter()
.find(|d| d.name == "a" && d.kind == VarDefKind::ListDestructuring);
let b_def = map
.var_defs
.iter()
.find(|d| d.name == "b" && d.kind == VarDefKind::ListDestructuring);
assert!(a_def.is_some(), "Should find $a from list destructuring");
assert!(b_def.is_some(), "Should find $b from list destructuring");
}
#[test]
fn var_def_method_parameter() {
let php =
"<?php\nclass Foo {\n public function bar(int $x) {\n return $x;\n }\n}\n";
let map = parse_and_extract(php);
let def = map
.var_defs
.iter()
.find(|d| d.name == "x" && d.kind == VarDefKind::Parameter);
assert!(def.is_some(), "Should find method parameter $x");
}
#[test]
fn var_def_closure_parameter() {
let php = "<?php\nfunction f() {\n $fn = function (string $s) { return $s; };\n}\n";
let map = parse_and_extract(php);
let def = map
.var_defs
.iter()
.find(|d| d.name == "s" && d.kind == VarDefKind::Parameter);
assert!(def.is_some(), "Should find closure parameter $s");
}
#[test]
fn var_def_arrow_function_parameter() {
let php = "<?php\nfunction f() {\n $fn = fn(int $n) => $n * 2;\n}\n";
let map = parse_and_extract(php);
let def = map
.var_defs
.iter()
.find(|d| d.name == "n" && d.kind == VarDefKind::Parameter);
assert!(def.is_some(), "Should find arrow function parameter $n");
}
#[test]
fn scopes_populated_for_function() {
let php = "<?php\nfunction foo() {\n $x = 1;\n}\n";
let map = parse_and_extract(php);
assert!(
!map.scopes.is_empty(),
"Should have at least one scope for the function body"
);
}
#[test]
fn scopes_populated_for_method() {
let php = "<?php\nclass A {\n public function m() {\n $y = 2;\n }\n}\n";
let map = parse_and_extract(php);
assert!(
!map.scopes.is_empty(),
"Should have at least one scope for the method body"
);
}
#[test]
fn scopes_populated_for_closure() {
let php = "<?php\nfunction f() {\n $fn = function () { $z = 3; };\n}\n";
let map = parse_and_extract(php);
assert!(
map.scopes.len() >= 2,
"Should have scopes for both function and closure"
);
}
#[test]
fn find_enclosing_scope_top_level() {
let php = "<?php\n$x = 1;\n";
let map = parse_and_extract(php);
assert_eq!(map.find_enclosing_scope(7), 0);
}
#[test]
fn find_enclosing_scope_inside_function() {
let php = "<?php\nfunction foo() {\n $x = 1;\n}\n";
let map = parse_and_extract(php);
let body_offset = php.find('{').unwrap() as u32;
let x_offset = php.find("$x").unwrap() as u32;
let scope = map.find_enclosing_scope(x_offset);
assert_eq!(
scope, body_offset,
"Should find the function body as the enclosing scope"
);
}
#[test]
fn find_var_definition_returns_most_recent() {
let php = "<?php\nfunction f() {\n $x = 1;\n $x = 2;\n echo $x;\n}\n";
let map = parse_and_extract(php);
let echo_x_offset = php.rfind("$x").unwrap() as u32;
let scope = map.find_enclosing_scope(echo_x_offset);
let def = map.find_var_definition("x", echo_x_offset, scope);
assert!(def.is_some(), "Should find a definition for $x");
let second_assign_offset = php.find("$x = 2").unwrap() as u32;
assert_eq!(
def.unwrap().offset,
second_assign_offset,
"Should find the second assignment"
);
}
#[test]
fn find_var_definition_parameter_found() {
let php = "<?php\nfunction greet(string $name) {\n echo $name;\n}\n";
let map = parse_and_extract(php);
let echo_name_offset = php.rfind("$name").unwrap() as u32;
let scope = map.find_enclosing_scope(echo_name_offset);
let def = map.find_var_definition("name", echo_name_offset, scope);
assert!(def.is_some(), "Should find parameter $name");
assert_eq!(def.unwrap().kind, VarDefKind::Parameter);
}
#[test]
fn find_var_definition_none_when_no_def() {
let php = "<?php\nfunction f() {\n echo $undefined;\n}\n";
let map = parse_and_extract(php);
let offset = php.find("$undefined").unwrap() as u32;
let scope = map.find_enclosing_scope(offset);
let def = map.find_var_definition("undefined", offset, scope);
assert!(def.is_none(), "Should return None for undefined variable");
}
#[test]
fn find_var_definition_respects_scope() {
let php = concat!(
"<?php\n",
"function outer() {\n",
" $x = 'outer';\n",
" $fn = function () {\n",
" echo $x;\n", " };\n",
"}\n",
);
let map = parse_and_extract(php);
let echo_x_offset = php.rfind("$x").unwrap() as u32;
let scope = map.find_enclosing_scope(echo_x_offset);
let def = map.find_var_definition("x", echo_x_offset, scope);
assert!(def.is_none(), "Should not find $x from a different scope");
}
#[test]
fn assignment_effective_from_excludes_rhs() {
let php = concat!(
"<?php\n",
"function f() {\n",
" $x = 10;\n",
" $x = $x + 1;\n",
"}\n",
);
let map = parse_and_extract(php);
let rhs_x_offset = php.rfind("$x + 1").unwrap() as u32;
let scope = map.find_enclosing_scope(rhs_x_offset);
let def = map.find_var_definition("x", rhs_x_offset, scope);
assert!(def.is_some(), "Should find a definition for RHS $x");
let first_assign_offset = php.find("$x = 10").unwrap() as u32;
assert_eq!(
def.unwrap().offset,
first_assign_offset,
"RHS $x should see the first assignment, not the one being written"
);
}
#[test]
fn assignment_effective_from_excludes_rhs_in_constructor_args() {
let php = concat!(
"<?php\n",
"function f(Foo $request) {\n",
" $request = new Bar(\n",
" name: $request->uuid,\n",
" );\n",
"}\n",
);
let map = parse_and_extract(php);
let rhs_request_offset = php.find("$request->uuid").unwrap() as u32;
let scope = map.find_enclosing_scope(rhs_request_offset);
let def = map.find_var_definition("request", rhs_request_offset, scope);
assert!(def.is_some(), "Should find a definition for RHS $request");
let param_offset = php.find("$request)").unwrap() as u32;
assert_eq!(
def.unwrap().offset,
param_offset,
"RHS $request inside constructor args should see the parameter, not the assignment"
);
assert_eq!(def.unwrap().kind, VarDefKind::Parameter);
}
#[test]
fn is_at_var_definition_on_assignment_lhs() {
let php = "<?php\nfunction f() {\n $x = 42;\n}\n";
let map = parse_and_extract(php);
let x_offset = php.find("$x = 42").unwrap() as u32;
assert!(
map.is_at_var_definition("x", x_offset),
"Should detect cursor on assignment LHS as at-definition"
);
assert!(
map.is_at_var_definition("x", x_offset + 1),
"Should detect cursor on 'x' of '$x' as at-definition"
);
}
#[test]
fn is_at_var_definition_on_parameter() {
let php = "<?php\nfunction greet(string $name) {\n echo $name;\n}\n";
let map = parse_and_extract(php);
let param_offset = php.find("$name)").unwrap() as u32;
assert!(
map.is_at_var_definition("name", param_offset),
"Should detect cursor on parameter as at-definition"
);
}
#[test]
fn is_at_var_definition_false_on_usage() {
let php = "<?php\nfunction f() {\n $x = 42;\n echo $x;\n}\n";
let map = parse_and_extract(php);
let echo_x_offset = php.rfind("$x").unwrap() as u32;
assert!(
!map.is_at_var_definition("x", echo_x_offset),
"Should NOT detect cursor on variable usage as at-definition"
);
}
#[test]
fn nested_array_destructuring_var_defs() {
let php = "<?php\nfunction f() {\n [[$a, $b], $c] = getData();\n}\n";
let map = parse_and_extract(php);
let a_def = map
.var_defs
.iter()
.find(|d| d.name == "a" && d.kind == VarDefKind::ArrayDestructuring);
let b_def = map
.var_defs
.iter()
.find(|d| d.name == "b" && d.kind == VarDefKind::ArrayDestructuring);
let c_def = map
.var_defs
.iter()
.find(|d| d.name == "c" && d.kind == VarDefKind::ArrayDestructuring);
assert!(a_def.is_some(), "Should find $a from nested destructuring");
assert!(b_def.is_some(), "Should find $b from nested destructuring");
assert!(c_def.is_some(), "Should find $c from outer destructuring");
}
#[test]
fn var_defs_sorted_by_scope_start_then_offset() {
let php = concat!(
"<?php\n",
"function a() {\n",
" $x = 1;\n",
" $y = 2;\n",
"}\n",
"function b() {\n",
" $z = 3;\n",
"}\n",
);
let map = parse_and_extract(php);
for window in map.var_defs.windows(2) {
let (a, b) = (&window[0], &window[1]);
assert!(
(a.scope_start, a.offset) <= (b.scope_start, b.offset),
"var_defs should be sorted by (scope_start, offset): ({}, {}) vs ({}, {})",
a.scope_start,
a.offset,
b.scope_start,
b.offset,
);
}
}
#[test]
fn top_level_var_def_has_scope_start_zero() {
let php = "<?php\n$global = 'hello';\n";
let map = parse_and_extract(php);
let def = map.var_defs.iter().find(|d| d.name == "global");
assert!(def.is_some(), "Should find top-level $global");
assert_eq!(
def.unwrap().scope_start,
0,
"Top-level definitions should have scope_start 0"
);
}
#[test]
fn template_param_def_recorded_for_class() {
let php = concat!(
"<?php\n",
"/**\n",
" * @template TKey of array-key\n",
" * @template TModel\n",
" */\n",
"class Collection {\n",
" /** @return array<TKey, TModel> */\n",
" public function all(): array { return []; }\n",
"}\n",
);
let map = parse_and_extract(php);
assert!(
map.template_defs.len() >= 2,
"Should record at least 2 template defs, got {}",
map.template_defs.len()
);
let tkey = map.template_defs.iter().find(|d| d.name == "TKey");
assert!(tkey.is_some(), "Should find TKey template def");
let tkey = tkey.unwrap();
assert_eq!(
&php[tkey.name_offset as usize..(tkey.name_offset + 4) as usize],
"TKey",
"name_offset should point to the TKey text"
);
assert_eq!(
tkey.bound,
Some(crate::php_type::PhpType::parse("array-key")),
"TKey should have bound 'array-key'"
);
let tmodel = map.template_defs.iter().find(|d| d.name == "TModel");
assert!(tmodel.is_some(), "Should find TModel template def");
assert_eq!(tmodel.unwrap().bound, None, "TModel should have no bound");
}
#[test]
fn template_param_def_lookup_in_same_class() {
let php = concat!(
"<?php\n",
"/**\n",
" * @template TKey of array-key\n",
" * @template TModel\n",
" */\n",
"class Collection {\n",
" /** @return array<TKey, TModel> */\n",
" public function all(): array { return []; }\n",
"}\n",
);
let map = parse_and_extract(php);
let return_line_offset = php.find("@return").unwrap() as u32;
let found = map.find_template_def("TKey", return_line_offset);
assert!(
found.is_some(),
"Should find TKey from within the class body"
);
assert_eq!(found.unwrap().name, "TKey");
let found = map.find_template_def("TModel", return_line_offset);
assert!(
found.is_some(),
"Should find TModel from within the class body"
);
}
#[test]
fn template_param_def_not_found_outside_scope() {
let php = concat!(
"<?php\n",
"/**\n",
" * @template T\n",
" */\n",
"class Foo {}\n",
"class Bar {}\n",
);
let map = parse_and_extract(php);
let bar_offset = php.find("class Bar").unwrap() as u32;
let found = map.find_template_def("T", bar_offset);
assert!(found.is_none(), "T should NOT be found outside Foo's scope");
}
#[test]
fn template_param_def_method_level() {
let php = concat!(
"<?php\n",
"class Mapper {\n",
" /**\n",
" * @template T\n",
" * @param T $item\n",
" * @return T\n",
" */\n",
" public function wrap(object $item): object { return $item; }\n",
"}\n",
);
let map = parse_and_extract(php);
let t_def = map.template_defs.iter().find(|d| d.name == "T");
assert!(t_def.is_some(), "Should find method-level template T");
let param_line = php.find("@param T").unwrap() as u32;
let found = map.find_template_def("T", param_line);
assert!(
found.is_some(),
"Should find T from within the method docblock"
);
}
#[test]
fn callable_param_type_spans_are_tight() {
let php = concat!(
"<?php\n",
"/**\n",
" * @template TKey of array-key\n",
" * @template TValue\n",
" */\n",
"class Col {\n",
" /**\n",
" * @param callable(TValue, TKey): mixed $callback\n",
" * @return static\n",
" */\n",
" public function each(callable $callback): static { return $this; }\n",
"}\n",
);
let map = parse_and_extract(php);
let param_line = php.find("@param callable(TValue, TKey): mixed").unwrap();
let tkey_offset = php[param_line..].find("TKey").unwrap() + param_line;
let span = map
.spans
.iter()
.find(|s| s.start == tkey_offset as u32)
.unwrap_or_else(|| {
panic!(
"Should find a span starting at TKey offset {}; spans: {:?}",
tkey_offset,
map.spans
.iter()
.filter(|s| {
matches!(
&s.kind,
SymbolKind::ClassReference { name, .. } if name == "TKey" || name == "TValue"
)
})
.collect::<Vec<_>>()
)
});
assert_eq!(
span.end - span.start,
4,
"TKey span should be exactly 4 bytes wide, but was {} ({}..{}), covering {:?}",
span.end - span.start,
span.start,
span.end,
&php[span.start as usize..span.end as usize],
);
assert!(
matches!(&span.kind, SymbolKind::ClassReference { name, .. } if name == "TKey"),
"span at TKey offset should be a ClassReference for TKey, got {:?}",
span.kind,
);
}
#[test]
fn this_variable_emits_self_static_parent() {
let php = concat!(
"<?php\n",
"class Foo {\n",
" public function bar(): void {\n",
" $this->baz();\n",
" }\n",
"}\n",
);
let map = parse_and_extract(php);
let this_offset = php.find("$this->baz").unwrap() as u32;
let hit = map.lookup(this_offset);
assert!(hit.is_some(), "Should find a span at $this");
match &hit.unwrap().kind {
SymbolKind::SelfStaticParent(kind) => {
assert_eq!(
*kind,
SelfStaticParentKind::This,
"$this should map to This variant"
);
}
other => panic!("Expected SelfStaticParent for $this, got {:?}", other),
}
}
#[test]
fn this_variable_standalone_emits_self_static_parent() {
let php = concat!(
"<?php\n",
"class Foo {\n",
" public function bar(): self {\n",
" return $this;\n",
" }\n",
"}\n",
);
let map = parse_and_extract(php);
let this_offset = php.find("$this;").unwrap() as u32;
let hit = map.lookup(this_offset);
assert!(hit.is_some(), "Should find a span at standalone $this");
match &hit.unwrap().kind {
SymbolKind::SelfStaticParent(kind) => {
assert_eq!(*kind, SelfStaticParentKind::This);
}
other => panic!(
"Expected SelfStaticParent for standalone $this, got {:?}",
other
),
}
}
#[test]
fn regular_variable_still_emits_variable() {
let php = "<?php\nfunction f() { $x = 1; }\n";
let map = parse_and_extract(php);
let x_offset = php.find("$x").unwrap() as u32;
let hit = map.lookup(x_offset);
assert!(hit.is_some());
match &hit.unwrap().kind {
SymbolKind::Variable { name } => {
assert_eq!(name, "x", "$x should still emit Variable");
}
other => panic!("Expected Variable for $x, got {:?}", other),
}
}
#[test]
fn docblock_array_suffix_type_produces_class_reference() {
let php = concat!(
"<?php\n",
"class AstNode {}\n",
"class Foo {\n",
" /** @return AstNode[] */\n",
" public function getChildren(): array { return []; }\n",
"}\n",
);
let map = parse_and_extract(php);
let docblock_start = php.find("/** @return").unwrap();
let ast_in_doc = php[docblock_start..].find("AstNode").unwrap() + docblock_start;
let hit = map.lookup(ast_in_doc as u32);
assert!(hit.is_some(), "Should find AstNode in @return AstNode[]");
if let SymbolKind::ClassReference { ref name, .. } = hit.unwrap().kind {
assert_eq!(
name, "AstNode",
"Name should be 'AstNode' without [] suffix"
);
} else {
panic!(
"Expected ClassReference for AstNode[], got {:?}",
hit.unwrap().kind
);
}
}
#[test]
fn docblock_array_suffix_span_excludes_brackets() {
let php = concat!(
"<?php\n",
"class Item {}\n",
"class Holder {\n",
" /** @var Item[] */\n",
" public array $items = [];\n",
"}\n",
);
let map = parse_and_extract(php);
let docblock_start = php.find("/** @var").unwrap();
let item_in_doc = php[docblock_start..].find("Item").unwrap() + docblock_start;
let hit = map.lookup(item_in_doc as u32);
assert!(hit.is_some(), "Should find Item in @var Item[]");
let span = hit.unwrap();
let span_text = &php[span.start as usize..span.end as usize];
assert_eq!(
span_text, "Item",
"Span should cover 'Item' only, not 'Item[]'"
);
}
#[test]
fn conditional_return_type_all_parts_get_spans() {
let php = concat!(
"<?php\n",
"class Container {\n",
" /**\n",
" * @template TClass\n",
" * @param string|null $abstract\n",
" * @return ($abstract is class-string<TClass> ? TClass : Container)\n",
" */\n",
" public function make($abstract) {}\n",
"}\n",
);
let map = parse_and_extract(php);
let return_line_start = php.find("@return").unwrap();
let first_tclass_offset = php[return_line_start..].find("TClass").unwrap() + return_line_start;
let hit1 = map.lookup(first_tclass_offset as u32);
assert!(
hit1.is_some(),
"Should find first TClass (inside class-string<TClass>)"
);
let span1 = hit1.unwrap();
assert_eq!(&php[span1.start as usize..span1.end as usize], "TClass");
let after_first = first_tclass_offset + "TClass".len();
let second_tclass_offset = php[after_first..].find("TClass").unwrap() + after_first;
let hit2 = map.lookup(second_tclass_offset as u32);
assert!(
hit2.is_some(),
"Should find second TClass (true branch of conditional)"
);
let span2 = hit2.unwrap();
assert_eq!(&php[span2.start as usize..span2.end as usize], "TClass");
assert_ne!(
span1.start, span2.start,
"The two TClass spans should be at different offsets"
);
let container_in_return =
php[return_line_start..].find("Container").unwrap() + return_line_start;
let hit3 = map.lookup(container_in_return as u32);
assert!(
hit3.is_some(),
"Should find Container (false branch of conditional)"
);
let span3 = hit3.unwrap();
assert_eq!(&php[span3.start as usize..span3.end as usize], "Container");
}
#[test]
fn conditional_return_type_with_not_keyword() {
let php = concat!(
"<?php\n",
"class Foo {\n",
" /**\n",
" * @return ($x is not null ? Foo : Bar)\n",
" */\n",
" public function test($x) {}\n",
"}\n",
"class Bar {}\n",
);
let map = parse_and_extract(php);
let return_start = php.find("@return").unwrap();
let foo_in_return = php[return_start..].find("Foo").unwrap() + return_start;
let bar_in_return = php[return_start..].find("Bar").unwrap() + return_start;
let hit_foo = map.lookup(foo_in_return as u32);
assert!(
hit_foo.is_some(),
"Should find Foo in true branch of conditional with 'is not'"
);
let hit_bar = map.lookup(bar_in_return as u32);
assert!(
hit_bar.is_some(),
"Should find Bar in false branch of conditional with 'is not'"
);
}
#[test]
fn var_def_kind_at_returns_parameter() {
let php = concat!(
"<?php\n",
"class Ctrl {\n",
" public function handle(Request $req) {}\n",
"}\n",
);
let map = parse_and_extract(php);
let dollar_offset = php.find("$req").unwrap() as u32;
let kind = map.var_def_kind_at("req", dollar_offset);
assert_eq!(
kind,
Some(&VarDefKind::Parameter),
"Should detect $req as Parameter"
);
let kind2 = map.var_def_kind_at("req", dollar_offset + 1);
assert_eq!(
kind2,
Some(&VarDefKind::Parameter),
"Should detect cursor on 'r' as Parameter"
);
}
#[test]
fn var_def_kind_at_returns_catch() {
let php = concat!(
"<?php\n",
"function f() {\n",
" try {} catch (\\Exception $e) {}\n",
"}\n",
);
let map = parse_and_extract(php);
let dollar_offset = php.find("$e)").unwrap() as u32;
let kind = map.var_def_kind_at("e", dollar_offset);
assert_eq!(kind, Some(&VarDefKind::Catch), "Should detect $e as Catch");
}
#[test]
fn var_def_kind_at_returns_foreach() {
let php = concat!(
"<?php\n",
"function f() {\n",
" foreach ($items as $item) {}\n",
"}\n",
);
let map = parse_and_extract(php);
let dollar_offset = php.find("$item)").unwrap() as u32;
let kind = map.var_def_kind_at("item", dollar_offset);
assert_eq!(
kind,
Some(&VarDefKind::Foreach),
"Should detect $item as Foreach"
);
}
#[test]
fn var_def_kind_at_returns_none_on_usage() {
let php = concat!(
"<?php\n",
"function f() {\n",
" $x = 1;\n",
" echo $x;\n",
"}\n",
);
let map = parse_and_extract(php);
let echo_x_offset = php.rfind("$x").unwrap() as u32;
let kind = map.var_def_kind_at("x", echo_x_offset);
assert!(kind.is_none(), "Should return None for variable usage site");
}
#[test]
fn docblock_callable_return_type_produces_class_reference() {
let php = concat!(
"<?php\n",
"class Pencil {}\n",
"class Factory {\n",
" /** @var \\Closure(): Pencil $supplier */\n",
" private $supplier;\n",
"}\n",
);
let map = parse_and_extract(php);
let docblock_start = php.find("/** @var").unwrap();
let pencil_in_doc = php[docblock_start..].find("Pencil").unwrap() + docblock_start;
let hit = map.lookup(pencil_in_doc as u32);
assert!(hit.is_some(), "Should find Pencil in callable return type");
if let SymbolKind::ClassReference { ref name, .. } = hit.unwrap().kind {
assert_eq!(name, "Pencil");
} else {
panic!(
"Expected ClassReference for Pencil, got {:?}",
hit.unwrap().kind
);
}
}
#[test]
fn docblock_callable_param_types_produce_class_references() {
let php = concat!(
"<?php\n",
"class Request {}\n",
"class Response {}\n",
"class Handler {\n",
" /** @var callable(Request): Response $handler */\n",
" private $handler;\n",
"}\n",
);
let map = parse_and_extract(php);
let docblock_start = php.find("/** @var").unwrap();
let request_in_doc = php[docblock_start..].find("Request").unwrap() + docblock_start;
let hit = map.lookup(request_in_doc as u32);
assert!(hit.is_some(), "Should find Request in callable param type");
if let SymbolKind::ClassReference { ref name, .. } = hit.unwrap().kind {
assert_eq!(name, "Request");
} else {
panic!(
"Expected ClassReference for Request, got {:?}",
hit.unwrap().kind
);
}
let response_in_doc = php[docblock_start..].find("Response").unwrap() + docblock_start;
let hit = map.lookup(response_in_doc as u32);
assert!(
hit.is_some(),
"Should find Response in callable return type"
);
if let SymbolKind::ClassReference { ref name, .. } = hit.unwrap().kind {
assert_eq!(name, "Response");
} else {
panic!(
"Expected ClassReference for Response, got {:?}",
hit.unwrap().kind
);
}
}
#[test]
fn docblock_closure_fqn_callable_produces_class_reference() {
let php = concat!(
"<?php\n",
"class Result {}\n",
"class Worker {\n",
" /** @param \\Closure(int): Result $cb */\n",
" public function run($cb) {}\n",
"}\n",
);
let map = parse_and_extract(php);
let docblock_start = php.find("/** @param").unwrap();
let closure_in_doc = php[docblock_start..].find("\\Closure").unwrap() + docblock_start;
let hit = map.lookup(closure_in_doc as u32);
assert!(hit.is_some(), "Should find \\Closure as a ClassReference");
if let SymbolKind::ClassReference { ref name, is_fqn } = hit.unwrap().kind {
assert_eq!(name, "Closure");
assert!(is_fqn, "\\Closure should be FQN");
} else {
panic!("Expected ClassReference for Closure");
}
let result_in_doc = php[docblock_start..].find("Result").unwrap() + docblock_start;
let hit = map.lookup(result_in_doc as u32);
assert!(hit.is_some(), "Should find Result in callable return type");
if let SymbolKind::ClassReference { ref name, .. } = hit.unwrap().kind {
assert_eq!(name, "Result");
} else {
panic!("Expected ClassReference for Result");
}
}
#[test]
fn template_param_in_use_tag_generic_arg() {
let php = concat!(
"<?php\n",
"/**\n",
" * @template TModel of \\stdClass\n",
" */\n",
"class Builder {\n",
" /** @use SomeTrait<TModel> */\n",
" use SomeTrait;\n",
"}\n",
);
let map = parse_and_extract(php);
let use_line_offset = php.find("@use").unwrap();
let tmodel_in_use = php[use_line_offset..].find("TModel").unwrap() + use_line_offset;
let hit = map.lookup(tmodel_in_use as u32);
assert!(
hit.is_some(),
"Should find TModel in @use generic arg in symbol map"
);
if let SymbolKind::ClassReference { ref name, .. } = hit.unwrap().kind {
assert_eq!(name, "TModel");
} else {
panic!(
"Expected ClassReference for TModel, got {:?}",
hit.unwrap().kind
);
}
let found = map.find_template_def("TModel", tmodel_in_use as u32);
assert!(
found.is_some(),
"Template def for TModel should cover the @use line"
);
}
#[test]
fn static_keyword_in_generic_arg_produces_span() {
let php = concat!(
"<?php\n",
"class Model {\n",
" /** @return Builder<static> */\n",
" public static function query() {}\n",
"}\n",
);
let map = parse_and_extract(php);
let return_line_offset = php.find("@return").unwrap();
let static_in_generic = php[return_line_offset..].find("static").unwrap() + return_line_offset;
let hit = map.lookup(static_in_generic as u32);
assert!(
hit.is_some(),
"Should find `static` in generic arg in symbol map"
);
match &hit.unwrap().kind {
SymbolKind::SelfStaticParent(kind) => {
assert_eq!(*kind, SelfStaticParentKind::Static);
}
SymbolKind::ClassReference { name, .. } => {
panic!(
"static should be SelfStaticParent, not ClassReference({})",
name
);
}
other => {
panic!("Expected SelfStaticParent for static, got {:?}", other);
}
}
}
#[test]
fn docblock_parenthesized_callable_in_union_produces_class_reference() {
let php = concat!(
"<?php\n",
"class Builder {\n",
" /**\n",
" * @param (\\Closure(static): mixed)|string|array $column\n",
" * @return $this\n",
" */\n",
" public function where($column) {}\n",
"}\n",
);
let map = parse_and_extract(php);
let docblock_start = php.find("/**").unwrap();
let closure_in_doc = php[docblock_start..].find("\\Closure").unwrap() + docblock_start;
let hit = map.lookup(closure_in_doc as u32);
assert!(
hit.is_some(),
"Should find \\Closure inside parenthesized callable as ClassReference"
);
if let SymbolKind::ClassReference { ref name, is_fqn } = hit.unwrap().kind {
assert_eq!(name, "Closure");
assert!(is_fqn, "\\Closure should be FQN");
let span = hit.unwrap();
let span_text = &php[span.start as usize..span.end as usize];
assert_eq!(span_text, "\\Closure", "Span should cover only \\Closure");
} else {
panic!(
"Expected ClassReference for \\Closure, got {:?}",
hit.unwrap().kind
);
}
}
#[test]
fn class_const_class_in_property_default_produces_class_reference() {
let php = concat!(
"<?php\n",
"class Foo {}\n",
"class Bar {\n",
" protected $casts = [\n",
" 'icing' => Foo::class,\n",
" ];\n",
"}\n",
);
let map = parse_and_extract(php);
let foo_in_casts = php.find("Foo::class").unwrap();
let hit = map.lookup(foo_in_casts as u32);
assert!(
hit.is_some(),
"Should find Foo in Foo::class as a ClassReference"
);
if let SymbolKind::ClassReference { name, .. } = &hit.unwrap().kind {
assert_eq!(name, "Foo");
} else {
panic!(
"Expected ClassReference for Foo, got {:?}",
hit.unwrap().kind
);
}
}
#[test]
fn multiline_docblock_generic_arg_produces_class_reference() {
let php = concat!(
"<?php\n",
"class SomeCollection {}\n",
"/**\n",
" * @template TValue\n",
" */\n",
"class Demo {\n",
" /**\n",
" * @return array<\n",
" * string,\n",
" * SomeCollection<int, TValue>\n",
" * >\n",
" */\n",
" public function grouped() {}\n",
"}\n",
);
let map = parse_and_extract(php);
let doc_start = php.find(" * @return").unwrap();
let some_coll = php[doc_start..].find("SomeCollection").unwrap() + doc_start;
let hit = map.lookup(some_coll as u32);
assert!(
hit.is_some(),
"Should find SomeCollection in multiline @return generic arg"
);
let span = hit.unwrap();
if let SymbolKind::ClassReference { name, .. } = &span.kind {
assert_eq!(name, "SomeCollection");
let span_text = &php[span.start as usize..span.end as usize];
assert_eq!(
span_text, "SomeCollection",
"Span text should be exactly 'SomeCollection', got {:?}",
span_text
);
} else {
panic!(
"Expected ClassReference for SomeCollection, got {:?}",
span.kind
);
}
let tvalue_pos = php[doc_start..].find("TValue").unwrap() + doc_start;
let hit2 = map.lookup(tvalue_pos as u32);
assert!(
hit2.is_some(),
"Should find TValue in multiline @return generic arg"
);
let span2 = hit2.unwrap();
if let SymbolKind::ClassReference { name, .. } = &span2.kind {
assert_eq!(name, "TValue");
let span_text = &php[span2.start as usize..span2.end as usize];
assert_eq!(
span_text, "TValue",
"Span text should be exactly 'TValue', got {:?}",
span_text
);
} else {
panic!("Expected ClassReference for TValue, got {:?}", span2.kind);
}
let found = map.find_template_def("TValue", tvalue_pos as u32);
assert!(
found.is_some(),
"Template def for TValue should cover the multiline @return usage"
);
}
#[test]
fn phpstan_assert_tag_produces_class_reference() {
let php = concat!(
"<?php\n",
"class Rock {}\n",
"class Checker {\n",
" /** @phpstan-assert-if-false Rock $value */\n",
" public function isValid($value): bool { return true; }\n",
"}\n",
);
let map = parse_and_extract(php);
let doc_start = php.find("@phpstan-assert-if-false").unwrap();
let rock_pos = php[doc_start..].find("Rock").unwrap() + doc_start;
let hit = map.lookup(rock_pos as u32);
assert!(
hit.is_some(),
"Should find Rock in @phpstan-assert-if-false as a ClassReference"
);
let span = hit.unwrap();
if let SymbolKind::ClassReference { name, .. } = &span.kind {
assert_eq!(name, "Rock");
let span_text = &php[span.start as usize..span.end as usize];
assert_eq!(span_text, "Rock");
} else {
panic!("Expected ClassReference for Rock, got {:?}", span.kind);
}
}
#[test]
fn top_level_const_value_produces_class_reference() {
let php = "<?php\nconst MY_CLASS = Foo::class;\n";
let map = parse_and_extract(php);
let foo_offset = php.find("Foo").unwrap() as u32;
let hit = map.lookup(foo_offset);
assert!(hit.is_some(), "Should find Foo in const value expression");
if let SymbolKind::ClassReference { ref name, .. } = hit.unwrap().kind {
assert_eq!(name, "Foo");
} else {
panic!(
"Expected ClassReference for Foo, got {:?}",
hit.unwrap().kind
);
}
}
#[test]
fn top_level_const_multiple_items() {
let php = "<?php\nconst A = Foo::class, B = Bar::class;\n";
let map = parse_and_extract(php);
let foo_offset = php.find("Foo").unwrap() as u32;
let hit_foo = map.lookup(foo_offset);
assert!(hit_foo.is_some(), "Should find Foo in first const item");
if let SymbolKind::ClassReference { ref name, .. } = hit_foo.unwrap().kind {
assert_eq!(name, "Foo");
} else {
panic!("Expected ClassReference for Foo");
}
let bar_offset = php.find("Bar").unwrap() as u32;
let hit_bar = map.lookup(bar_offset);
assert!(hit_bar.is_some(), "Should find Bar in second const item");
if let SymbolKind::ClassReference { ref name, .. } = hit_bar.unwrap().kind {
assert_eq!(name, "Bar");
} else {
panic!("Expected ClassReference for Bar");
}
}
#[test]
fn anonymous_class_extends_produces_class_reference() {
let php = concat!(
"<?php\n",
"function test() {\n",
" return new class extends Foo implements Bar {};\n",
"}\n",
);
let map = parse_and_extract(php);
let foo_offset = php.find("Foo").unwrap() as u32;
let hit = map.lookup(foo_offset);
assert!(hit.is_some(), "Should find Foo in anonymous class extends");
if let SymbolKind::ClassReference { ref name, .. } = hit.unwrap().kind {
assert_eq!(name, "Foo");
} else {
panic!("Expected ClassReference for Foo");
}
let bar_offset = php.find("Bar").unwrap() as u32;
let hit2 = map.lookup(bar_offset);
assert!(
hit2.is_some(),
"Should find Bar in anonymous class implements"
);
if let SymbolKind::ClassReference { ref name, .. } = hit2.unwrap().kind {
assert_eq!(name, "Bar");
} else {
panic!("Expected ClassReference for Bar");
}
}
#[test]
fn anonymous_class_members_are_extracted() {
let php = concat!(
"<?php\n",
"function test() {\n",
" return new class {\n",
" public function run(Baz $b) {}\n",
" };\n",
"}\n",
);
let map = parse_and_extract(php);
let baz_offset = php.find("Baz").unwrap() as u32;
let hit = map.lookup(baz_offset);
assert!(
hit.is_some(),
"Should find Baz in anonymous class method param type"
);
if let SymbolKind::ClassReference { ref name, .. } = hit.unwrap().kind {
assert_eq!(name, "Baz");
} else {
panic!("Expected ClassReference for Baz");
}
}
#[test]
fn anonymous_class_constructor_args_are_extracted() {
let php = concat!(
"<?php\n",
"function test() {\n",
" return new class(new Qux()) {};\n",
"}\n",
);
let map = parse_and_extract(php);
let qux_offset = php.find("Qux").unwrap() as u32;
let hit = map.lookup(qux_offset);
assert!(
hit.is_some(),
"Should find Qux in anonymous class constructor arg"
);
if let SymbolKind::ClassReference { ref name, .. } = hit.unwrap().kind {
assert_eq!(name, "Qux");
} else {
panic!("Expected ClassReference for Qux");
}
}
#[test]
fn construct_isset_extracts_inner_expressions() {
let php = "<?php\nfunction t() { isset($foo->bar); }\n";
let map = parse_and_extract(php);
let bar_offset = php.find("bar").unwrap() as u32;
let hit = map.lookup(bar_offset);
assert!(hit.is_some(), "Should find bar in isset()");
if let SymbolKind::MemberAccess {
ref member_name, ..
} = hit.unwrap().kind
{
assert_eq!(member_name, "bar");
} else {
panic!("Expected MemberAccess for bar, got {:?}", hit.unwrap().kind);
}
}
#[test]
fn construct_empty_extracts_inner_expression() {
let php = "<?php\nfunction t() { empty($foo->name); }\n";
let map = parse_and_extract(php);
let name_offset = php.find("name").unwrap() as u32;
let hit = map.lookup(name_offset);
assert!(hit.is_some(), "Should find name in empty()");
if let SymbolKind::MemberAccess {
ref member_name, ..
} = hit.unwrap().kind
{
assert_eq!(member_name, "name");
} else {
panic!(
"Expected MemberAccess for name, got {:?}",
hit.unwrap().kind
);
}
}
#[test]
fn construct_print_extracts_inner_expression() {
let php = "<?php\nfunction t(Foo $x) { print $x->label; }\n";
let map = parse_and_extract(php);
let label_offset = php.find("label").unwrap() as u32;
let hit = map.lookup(label_offset);
assert!(hit.is_some(), "Should find label in print expression");
if let SymbolKind::MemberAccess {
ref member_name, ..
} = hit.unwrap().kind
{
assert_eq!(member_name, "label");
} else {
panic!("Expected MemberAccess for label");
}
}
#[test]
fn composite_string_extracts_variable() {
let php = "<?php\nfunction t() { $x = 1; echo \"val={$x}\"; }\n";
let map = parse_and_extract(php);
let first_x = php.find("$x").unwrap();
let second_x = php[first_x + 2..].find("$x").unwrap() + first_x + 2;
let hit = map.lookup(second_x as u32);
assert!(hit.is_some(), "Should find $x inside interpolated string");
if let SymbolKind::Variable { ref name } = hit.unwrap().kind {
assert_eq!(name, "x");
} else {
panic!("Expected Variable for $x, got {:?}", hit.unwrap().kind);
}
}
#[test]
fn composite_string_braced_expression_extracts_member_access() {
let php = "<?php\nfunction t() { echo \"name={$obj->name}\"; }\n";
let map = parse_and_extract(php);
let name_offset = php.find("name}").unwrap() as u32;
let hit = map.lookup(name_offset);
assert!(
hit.is_some(),
"Should find member access inside braced string expression"
);
if let SymbolKind::MemberAccess {
ref member_name, ..
} = hit.unwrap().kind
{
assert_eq!(member_name, "name");
} else {
panic!(
"Expected MemberAccess for name, got {:?}",
hit.unwrap().kind
);
}
}
#[test]
fn array_append_extracts_array_variable() {
let php = "<?php\nfunction t() { $arr[] = 1; }\n";
let map = parse_and_extract(php);
let arr_offset = php.find("$arr").unwrap() as u32;
let hit = map.lookup(arr_offset);
assert!(hit.is_some(), "Should find $arr in array append LHS");
if let SymbolKind::Variable { ref name } = hit.unwrap().kind {
assert_eq!(name, "arr");
} else {
panic!("Expected Variable for $arr, got {:?}", hit.unwrap().kind);
}
}
#[test]
fn constant_access_produces_constant_reference() {
let php = "<?php\nfunction t() { echo PHP_EOL; }\n";
let map = parse_and_extract(php);
let eol_offset = php.find("PHP_EOL").unwrap() as u32;
let hit = map.lookup(eol_offset);
assert!(hit.is_some(), "Should find PHP_EOL as ConstantReference");
if let SymbolKind::ConstantReference { ref name } = hit.unwrap().kind {
assert_eq!(name, "PHP_EOL");
} else {
panic!(
"Expected ConstantReference for PHP_EOL, got {:?}",
hit.unwrap().kind
);
}
}
#[test]
fn static_class_constant_access_produces_class_reference() {
let php = "<?php\nfunction t() { echo \\App\\MyClass::FOO; }\n";
let map = parse_and_extract(php);
let class_offset = php.find("\\App\\MyClass").unwrap() as u32;
let hit = map.lookup(class_offset);
assert!(hit.is_some(), "Should find \\App\\MyClass");
if let SymbolKind::ClassReference { ref name, is_fqn } = hit.unwrap().kind {
assert_eq!(name, "App\\MyClass");
assert!(is_fqn);
} else {
panic!(
"Expected ClassReference for App\\MyClass, got {:?}",
hit.unwrap().kind
);
}
}
#[test]
fn namespaced_standalone_constant_produces_constant_reference() {
let php = "<?php\nfunction t() { echo \\PHPStan\\PHP_VERSION_ID; }\n";
let map = parse_and_extract(php);
let const_offset = php.find("\\PHPStan\\PHP_VERSION_ID").unwrap() as u32;
let hit = map.lookup(const_offset);
assert!(
hit.is_some(),
"Should find \\PHPStan\\PHP_VERSION_ID as ConstantReference"
);
if let SymbolKind::ConstantReference { ref name } = hit.unwrap().kind {
assert_eq!(name, "PHPStan\\PHP_VERSION_ID");
} else {
panic!(
"Expected ConstantReference for PHPStan\\PHP_VERSION_ID, got {:?}",
hit.unwrap().kind
);
}
}
#[test]
fn first_class_callable_function_produces_function_call() {
let php = "<?php\nfunction t() { $fn = strlen(...); }\n";
let map = parse_and_extract(php);
let strlen_offset = php.find("strlen").unwrap() as u32;
let hit = map.lookup(strlen_offset);
assert!(hit.is_some(), "Should find strlen in first-class callable");
if let SymbolKind::FunctionCall { ref name, .. } = hit.unwrap().kind {
assert_eq!(name, "strlen");
} else {
panic!(
"Expected FunctionCall for strlen, got {:?}",
hit.unwrap().kind
);
}
}
#[test]
fn first_class_callable_static_method_produces_member_access() {
let php = "<?php\nfunction t() { $fn = Foo::bar(...); }\n";
let map = parse_and_extract(php);
let foo_offset = php.find("Foo").unwrap() as u32;
let hit_foo = map.lookup(foo_offset);
assert!(hit_foo.is_some(), "Should find Foo class reference");
if let SymbolKind::ClassReference { ref name, .. } = hit_foo.unwrap().kind {
assert_eq!(name, "Foo");
} else {
panic!("Expected ClassReference for Foo");
}
let bar_offset = php.find("bar").unwrap() as u32;
let hit_bar = map.lookup(bar_offset);
assert!(
hit_bar.is_some(),
"Should find bar in static first-class callable"
);
if let SymbolKind::MemberAccess {
ref member_name,
is_static,
is_method_call,
..
} = hit_bar.unwrap().kind
{
assert_eq!(member_name, "bar");
assert!(is_static);
assert!(is_method_call);
} else {
panic!(
"Expected MemberAccess for bar, got {:?}",
hit_bar.unwrap().kind
);
}
}
#[test]
fn first_class_callable_instance_method_produces_member_access() {
let php = "<?php\nfunction t($obj) { $fn = $obj->baz(...); }\n";
let map = parse_and_extract(php);
let baz_offset = php.find("baz").unwrap() as u32;
let hit = map.lookup(baz_offset);
assert!(
hit.is_some(),
"Should find baz in instance first-class callable"
);
if let SymbolKind::MemberAccess {
ref member_name,
is_static,
is_method_call,
..
} = hit.unwrap().kind
{
assert_eq!(member_name, "baz");
assert!(!is_static);
assert!(is_method_call);
} else {
panic!("Expected MemberAccess for baz, got {:?}", hit.unwrap().kind);
}
}
#[test]
fn declare_statement_body_extracts_symbols() {
let php = concat!(
"<?php\n",
"declare(strict_types=1);\n",
"function test(Foo $x) {}\n",
);
let map = parse_and_extract(php);
let foo_offset = php.find("Foo").unwrap() as u32;
let hit = map.lookup(foo_offset);
assert!(hit.is_some(), "Should find Foo after declare statement");
if let SymbolKind::ClassReference { ref name, .. } = hit.unwrap().kind {
assert_eq!(name, "Foo");
} else {
panic!("Expected ClassReference for Foo");
}
}
#[test]
fn docblock_array_shape_value_type_produces_class_reference() {
let php = concat!(
"<?php\n",
"class Pen {}\n",
"/**\n",
" * @return array{logger: Pen, debug: bool}\n",
" */\n",
"function getConfig(): array { return []; }\n",
);
let map = parse_and_extract(php);
let pen_offset = php.find("Pen, debug").unwrap() as u32;
let hit = map.lookup(pen_offset);
assert!(
hit.is_some(),
"Should find Pen inside array shape value type"
);
if let SymbolKind::ClassReference { ref name, .. } = hit.unwrap().kind {
assert_eq!(name, "Pen");
} else {
panic!(
"Expected ClassReference for Pen, got {:?}",
hit.unwrap().kind
);
}
}
#[test]
fn docblock_array_shape_multiple_class_values() {
let php = concat!(
"<?php\n",
"class Logger {}\n",
"class Mailer {}\n",
"/**\n",
" * @return array{log: Logger, mail: Mailer}\n",
" */\n",
"function services(): array { return []; }\n",
);
let map = parse_and_extract(php);
let logger_offset = php.find("Logger, mail").unwrap() as u32;
let hit = map.lookup(logger_offset);
assert!(hit.is_some(), "Should find Logger in array shape");
if let SymbolKind::ClassReference { ref name, .. } = hit.unwrap().kind {
assert_eq!(name, "Logger");
} else {
panic!("Expected ClassReference for Logger");
}
let mailer_offset = php.find("Mailer}").unwrap() as u32;
let hit = map.lookup(mailer_offset);
assert!(hit.is_some(), "Should find Mailer in array shape");
if let SymbolKind::ClassReference { ref name, .. } = hit.unwrap().kind {
assert_eq!(name, "Mailer");
} else {
panic!("Expected ClassReference for Mailer");
}
}
#[test]
fn docblock_object_shape_value_type_produces_class_reference() {
let php = concat!(
"<?php\n",
"class User {}\n",
"/**\n",
" * @return object{owner: User, active: bool}\n",
" */\n",
"function getProfile(): object { return (object)[]; }\n",
);
let map = parse_and_extract(php);
let user_offset = php.find("User, active").unwrap() as u32;
let hit = map.lookup(user_offset);
assert!(
hit.is_some(),
"Should find User inside object shape value type"
);
if let SymbolKind::ClassReference { ref name, .. } = hit.unwrap().kind {
assert_eq!(name, "User");
} else {
panic!("Expected ClassReference for User");
}
}
#[test]
fn docblock_array_shape_scalar_value_not_navigable() {
let php = concat!(
"<?php\n",
"/**\n",
" * @return array{name: string, count: int}\n",
" */\n",
"function getData(): array { return []; }\n",
);
let map = parse_and_extract(php);
let string_offset = php.find("string, count").unwrap() as u32;
let hit = map.lookup(string_offset);
assert!(
hit.is_none(),
"Scalar 'string' inside array shape should not be navigable"
);
}
#[test]
fn docblock_array_shape_optional_key_value_produces_class_reference() {
let php = concat!(
"<?php\n",
"class Widget {}\n",
"/**\n",
" * @return array{item?: Widget}\n",
" */\n",
"function maybeWidget(): array { return []; }\n",
);
let map = parse_and_extract(php);
let widget_offset = php.find("Widget}").unwrap() as u32;
let hit = map.lookup(widget_offset);
assert!(
hit.is_some(),
"Should find Widget in optional array shape entry"
);
if let SymbolKind::ClassReference { ref name, .. } = hit.unwrap().kind {
assert_eq!(name, "Widget");
} else {
panic!("Expected ClassReference for Widget");
}
}
#[test]
fn docblock_array_shape_nested_generic_value() {
let php = concat!(
"<?php\n",
"class Item {}\n",
"/**\n",
" * @return array{items: list<Item>, total: int}\n",
" */\n",
"function paginated(): array { return []; }\n",
);
let map = parse_and_extract(php);
let item_offset = php.find("Item>, total").unwrap() as u32;
let hit = map.lookup(item_offset);
assert!(
hit.is_some(),
"Should find Item inside generic within array shape"
);
if let SymbolKind::ClassReference { ref name, .. } = hit.unwrap().kind {
assert_eq!(name, "Item");
} else {
panic!("Expected ClassReference for Item");
}
}
#[test]
fn arrow_fn_body_scope_suppresses_outer_call_site() {
let src = concat!(
"<?php\n",
"class Collection {\n",
" /** @param callable(mixed): mixed $callback */\n",
" public function each(callable $callback): static { return $this; }\n",
"}\n",
"class ReviewModel {\n",
" public float $percentage = 0.0;\n",
" public int $count = 0;\n",
"}\n",
"$ratings = new Collection();\n",
"$total = 10;\n",
"$ratings->each(fn(ReviewModel $model): float => $model->percentage = $total > 0 ? ($model->count / $total) * 100 : 0.0);\n",
);
let sm = parse_and_extract(src);
let each_cs = sm
.call_sites
.iter()
.find(|cs| cs.call_expression.contains("each"))
.expect("should have a call site for each()");
let arrow_body_marker = src.find("$model->percentage").unwrap() as u32;
let has_nested = sm.is_inside_nested_scope_of_call(arrow_body_marker, each_cs);
assert!(
has_nested,
"Cursor at offset {} should be inside a nested body scope of each() call (args {}..{}), body_scopes: {:?}",
arrow_body_marker, each_cs.args_start, each_cs.args_end, sm.body_scopes,
);
}
#[test]
fn see_tag_class_reference() {
let php = concat!(
"<?php\n",
"/**\n",
" * @see UserService\n",
" */\n",
"class Foo {}\n",
);
let map = parse_and_extract(php);
let offset = php.find("UserService").unwrap() as u32;
let hit = map.lookup(offset);
assert!(hit.is_some(), "Should find UserService from @see tag");
if let SymbolKind::ClassReference { ref name, .. } = hit.unwrap().kind {
assert_eq!(name, "UserService");
} else {
panic!("Expected ClassReference for @see UserService");
}
}
#[test]
fn see_tag_fqn_class_reference() {
let php = concat!(
"<?php\n",
"/**\n",
" * @see \\App\\Models\\User\n",
" */\n",
"class Foo {}\n",
);
let map = parse_and_extract(php);
let offset = php.find("\\App\\Models\\User").unwrap() as u32;
let hit = map.lookup(offset);
assert!(hit.is_some(), "Should find FQN class from @see tag");
if let SymbolKind::ClassReference {
ref name, is_fqn, ..
} = hit.unwrap().kind
{
assert_eq!(name, "App\\Models\\User");
assert!(is_fqn, "Leading backslash should set is_fqn");
} else {
panic!("Expected ClassReference for @see \\App\\Models\\User");
}
}
#[test]
fn see_tag_member_method() {
let php = concat!(
"<?php\n",
"/**\n",
" * @see Order::getTotal()\n",
" */\n",
"class Foo {}\n",
);
let map = parse_and_extract(php);
let class_offset = php.find("Order").unwrap() as u32;
let hit = map.lookup(class_offset);
assert!(
hit.is_some(),
"Should find Order from @see Order::getTotal()"
);
if let SymbolKind::ClassReference { ref name, .. } = hit.unwrap().kind {
assert_eq!(name, "Order");
} else {
panic!("Expected ClassReference for Order");
}
let member_offset = php.find("getTotal").unwrap() as u32;
let hit = map.lookup(member_offset);
assert!(
hit.is_some(),
"Should find getTotal from @see Order::getTotal()"
);
if let SymbolKind::MemberAccess {
ref subject_text,
ref member_name,
is_static,
..
} = hit.unwrap().kind
{
assert_eq!(subject_text, "Order");
assert_eq!(member_name, "getTotal");
assert!(is_static, "@see members are treated as static access");
} else {
panic!("Expected MemberAccess for getTotal");
}
}
#[test]
fn see_tag_member_property() {
let php = concat!(
"<?php\n",
"/**\n",
" * @see Order::$channel_type\n",
" */\n",
"class Foo {}\n",
);
let map = parse_and_extract(php);
let class_offset = php.find("Order").unwrap() as u32;
let hit = map.lookup(class_offset);
assert!(hit.is_some(), "Should find Order from @see tag");
if let SymbolKind::ClassReference { ref name, .. } = hit.unwrap().kind {
assert_eq!(name, "Order");
} else {
panic!("Expected ClassReference for Order");
}
let prop_offset = php.find("$channel_type").unwrap() as u32;
let hit = map.lookup(prop_offset);
assert!(hit.is_some(), "Should find $channel_type from @see tag");
if let SymbolKind::MemberAccess {
ref subject_text,
ref member_name,
is_static,
..
} = hit.unwrap().kind
{
assert_eq!(subject_text, "Order");
assert_eq!(member_name, "channel_type");
assert!(is_static);
} else {
panic!("Expected MemberAccess for $channel_type");
}
}
#[test]
fn see_tag_member_constant() {
let php = concat!(
"<?php\n",
"/**\n",
" * @see Order::STATUS_PENDING\n",
" */\n",
"class Foo {}\n",
);
let map = parse_and_extract(php);
let const_offset = php.find("STATUS_PENDING").unwrap() as u32;
let hit = map.lookup(const_offset);
assert!(
hit.is_some(),
"Should find STATUS_PENDING from @see Order::STATUS_PENDING"
);
if let SymbolKind::MemberAccess {
ref subject_text,
ref member_name,
is_static,
..
} = hit.unwrap().kind
{
assert_eq!(subject_text, "Order");
assert_eq!(member_name, "STATUS_PENDING");
assert!(is_static);
} else {
panic!("Expected MemberAccess for STATUS_PENDING");
}
}
#[test]
fn see_tag_url_skipped() {
let php = concat!(
"<?php\n",
"/**\n",
" * @see https://example.com/docs\n",
" */\n",
"class Foo {}\n",
);
let map = parse_and_extract(php);
let offset = php.find("https://example.com").unwrap() as u32;
let hit = map.lookup(offset);
assert!(
hit.is_none()
|| !matches!(
hit.unwrap().kind,
SymbolKind::ClassReference { .. } | SymbolKind::FunctionCall { .. }
),
"URL in @see should not produce a navigable symbol"
);
}
#[test]
fn see_tag_function_reference() {
let php = concat!(
"<?php\n",
"/**\n",
" * @see fixture()\n",
" */\n",
"class Foo {}\n",
);
let map = parse_and_extract(php);
let offset = php.find("fixture").unwrap() as u32;
let hit = map.lookup(offset);
assert!(hit.is_some(), "Should find fixture from @see fixture()");
if let SymbolKind::FunctionCall { ref name, .. } = hit.unwrap().kind {
assert_eq!(name, "fixture");
} else {
panic!(
"Expected FunctionCall for fixture, got {:?}",
hit.unwrap().kind
);
}
}
#[test]
fn see_tag_inline_form() {
let php = concat!(
"<?php\n",
"/**\n",
" * Wraps {@see UserService} with caching.\n",
" */\n",
"class Foo {}\n",
);
let map = parse_and_extract(php);
let offset = php.find("UserService").unwrap() as u32;
let hit = map.lookup(offset);
assert!(hit.is_some(), "Should find UserService from inline @see");
if let SymbolKind::ClassReference { ref name, .. } = hit.unwrap().kind {
assert_eq!(name, "UserService");
} else {
panic!("Expected ClassReference for inline @see UserService");
}
}
#[test]
fn see_tag_inline_function_reference() {
let php = concat!(
"<?php\n",
"/**\n",
" * Wraps {@see fixture()} with extra logic.\n",
" */\n",
"class Foo {}\n",
);
let map = parse_and_extract(php);
let offset = php.find("fixture").unwrap() as u32;
let hit = map.lookup(offset);
assert!(
hit.is_some(),
"Should find fixture from inline {{@see fixture()}}"
);
if let SymbolKind::FunctionCall { ref name, .. } = hit.unwrap().kind {
assert_eq!(name, "fixture");
} else {
panic!(
"Expected FunctionCall for inline @see fixture(), got {:?}",
hit.unwrap().kind
);
}
}
#[test]
fn see_tag_inline_member_reference() {
let php = concat!(
"<?php\n",
"/**\n",
" * Uses {@see Order::getTotal()} internally.\n",
" */\n",
"class Foo {}\n",
);
let map = parse_and_extract(php);
let class_offset = php.find("Order").unwrap() as u32;
let hit = map.lookup(class_offset);
assert!(hit.is_some(), "Should find Order from inline @see");
if let SymbolKind::ClassReference { ref name, .. } = hit.unwrap().kind {
assert_eq!(name, "Order");
} else {
panic!("Expected ClassReference for Order in inline @see");
}
let member_offset = php.find("getTotal").unwrap() as u32;
let hit = map.lookup(member_offset);
assert!(hit.is_some(), "Should find getTotal from inline @see");
if let SymbolKind::MemberAccess {
ref member_name, ..
} = hit.unwrap().kind
{
assert_eq!(member_name, "getTotal");
} else {
panic!("Expected MemberAccess for getTotal in inline @see");
}
}
#[test]
fn see_tag_scalar_type_not_navigable() {
let php = concat!(
"<?php\n",
"/**\n",
" * @see string\n",
" */\n",
"class Foo {}\n",
);
let map = parse_and_extract(php);
let see_line = php.find("@see string").unwrap();
let string_offset = (see_line + "@see ".len()) as u32;
let hit = map.lookup(string_offset);
assert!(
hit.is_none()
|| !matches!(
hit.unwrap().kind,
SymbolKind::ClassReference { .. } | SymbolKind::FunctionCall { .. }
),
"Scalar type 'string' in @see should not produce a navigable symbol"
);
}
#[test]
fn see_tag_multiple_on_same_docblock() {
let php = concat!(
"<?php\n",
"/**\n",
" * @see UserService\n",
" * @see OrderService\n",
" */\n",
"class Foo {}\n",
);
let map = parse_and_extract(php);
let user_offset = php.find("UserService").unwrap() as u32;
let hit = map.lookup(user_offset);
assert!(hit.is_some(), "Should find UserService");
if let SymbolKind::ClassReference { ref name, .. } = hit.unwrap().kind {
assert_eq!(name, "UserService");
} else {
panic!("Expected ClassReference for UserService");
}
let order_offset = php.find("OrderService").unwrap() as u32;
let hit = map.lookup(order_offset);
assert!(hit.is_some(), "Should find OrderService");
if let SymbolKind::ClassReference { ref name, .. } = hit.unwrap().kind {
assert_eq!(name, "OrderService");
} else {
panic!("Expected ClassReference for OrderService");
}
}
#[test]
fn see_tag_on_method_docblock() {
let php = concat!(
"<?php\n",
"class Foo {\n",
" /**\n",
" * @see BarService\n",
" */\n",
" public function test() {}\n",
"}\n",
);
let map = parse_and_extract(php);
let offset = php.find("BarService").unwrap() as u32;
let hit = map.lookup(offset);
assert!(hit.is_some(), "Should find BarService on method docblock");
if let SymbolKind::ClassReference { ref name, .. } = hit.unwrap().kind {
assert_eq!(name, "BarService");
} else {
panic!("Expected ClassReference for BarService");
}
}
#[test]
fn see_tag_on_property_docblock() {
let php = concat!(
"<?php\n",
"class Foo {\n",
" /**\n",
" * @see CacheDriver\n",
" */\n",
" private $cache;\n",
"}\n",
);
let map = parse_and_extract(php);
let offset = php.find("CacheDriver").unwrap() as u32;
let hit = map.lookup(offset);
assert!(
hit.is_some(),
"Should find CacheDriver on property docblock"
);
if let SymbolKind::ClassReference { ref name, .. } = hit.unwrap().kind {
assert_eq!(name, "CacheDriver");
} else {
panic!("Expected ClassReference for CacheDriver");
}
}
#[test]
fn property_attribute_class_reference() {
let php = concat!(
"<?php\n",
"class Foo {\n",
" #[\\Assert\\NotBlank]\n",
" public string $name;\n",
"}\n",
);
let map = parse_and_extract(php);
let attr_offset = php.find("\\Assert\\NotBlank").unwrap() as u32;
let hit = map.lookup(attr_offset);
assert!(
hit.is_some(),
"Should find attribute class reference on property"
);
if let SymbolKind::ClassReference { ref name, is_fqn } = hit.unwrap().kind {
assert_eq!(name, "Assert\\NotBlank");
assert!(is_fqn);
} else {
panic!(
"Expected ClassReference for property attribute, got {:?}",
hit.unwrap().kind
);
}
}
#[test]
fn hooked_property_attribute_class_reference() {
let php = concat!(
"<?php\n",
"class Foo {\n",
" #[\\Assert\\Email]\n",
" public string $email {\n",
" set { $this->email = strtolower($value); }\n",
" }\n",
"}\n",
);
let map = parse_and_extract(php);
let attr_offset = php.find("\\Assert\\Email").unwrap() as u32;
let hit = map.lookup(attr_offset);
assert!(
hit.is_some(),
"Should find attribute class reference on hooked property"
);
if let SymbolKind::ClassReference { ref name, .. } = hit.unwrap().kind {
assert_eq!(name, "Assert\\Email");
} else {
panic!(
"Expected ClassReference for hooked property attribute, got {:?}",
hit.unwrap().kind
);
}
}
#[test]
fn class_constant_attribute_class_reference() {
let php = concat!(
"<?php\n",
"class Foo {\n",
" #[\\Deprecated]\n",
" const OLD_NAME = 'old';\n",
"}\n",
);
let map = parse_and_extract(php);
let attr_offset = php.find("\\Deprecated").unwrap() as u32;
let hit = map.lookup(attr_offset);
assert!(
hit.is_some(),
"Should find attribute class reference on class constant"
);
if let SymbolKind::ClassReference { ref name, .. } = hit.unwrap().kind {
assert_eq!(name, "Deprecated");
} else {
panic!(
"Expected ClassReference for constant attribute, got {:?}",
hit.unwrap().kind
);
}
}
#[test]
fn parameter_attribute_class_reference() {
let php = concat!(
"<?php\n",
"function handle(#[\\SensitiveParameter] string $secret): void {}\n",
);
let map = parse_and_extract(php);
let attr_offset = php.find("\\SensitiveParameter").unwrap() as u32;
let hit = map.lookup(attr_offset);
assert!(
hit.is_some(),
"Should find attribute class reference on parameter"
);
if let SymbolKind::ClassReference { ref name, .. } = hit.unwrap().kind {
assert_eq!(name, "SensitiveParameter");
} else {
panic!(
"Expected ClassReference for parameter attribute, got {:?}",
hit.unwrap().kind
);
}
}
#[test]
fn method_parameter_attribute_class_reference() {
let php = concat!(
"<?php\n",
"class Svc {\n",
" public function run(#[\\Autowire] Logger $log): void {}\n",
"}\n",
);
let map = parse_and_extract(php);
let attr_offset = php.find("\\Autowire").unwrap() as u32;
let hit = map.lookup(attr_offset);
assert!(
hit.is_some(),
"Should find attribute class reference on method parameter"
);
if let SymbolKind::ClassReference { ref name, .. } = hit.unwrap().kind {
assert_eq!(name, "Autowire");
} else {
panic!(
"Expected ClassReference for method parameter attribute, got {:?}",
hit.unwrap().kind
);
}
}
#[test]
fn enum_case_attribute_class_reference() {
let php = concat!(
"<?php\n",
"enum Status: string {\n",
" #[\\Deprecated]\n",
" case Legacy = 'legacy';\n",
" case Active = 'active';\n",
"}\n",
);
let map = parse_and_extract(php);
let attr_offset = php.find("\\Deprecated").unwrap() as u32;
let hit = map.lookup(attr_offset);
assert!(
hit.is_some(),
"Should find attribute class reference on enum case"
);
if let SymbolKind::ClassReference { ref name, .. } = hit.unwrap().kind {
assert_eq!(name, "Deprecated");
} else {
panic!(
"Expected ClassReference for enum case attribute, got {:?}",
hit.unwrap().kind
);
}
}
#[test]
fn multiple_property_attributes() {
let php = concat!(
"<?php\n",
"class Foo {\n",
" #[\\Assert\\NotBlank, \\Assert\\Length(max: 255)]\n",
" public string $title;\n",
"}\n",
);
let map = parse_and_extract(php);
let nb_offset = php.find("\\Assert\\NotBlank").unwrap() as u32;
let hit = map.lookup(nb_offset);
assert!(hit.is_some(), "Should find first property attribute");
if let SymbolKind::ClassReference { ref name, .. } = hit.unwrap().kind {
assert_eq!(name, "Assert\\NotBlank");
} else {
panic!("Expected ClassReference for first attribute");
}
let len_offset = php.find("\\Assert\\Length").unwrap() as u32;
let hit = map.lookup(len_offset);
assert!(hit.is_some(), "Should find second property attribute");
if let SymbolKind::ClassReference { ref name, .. } = hit.unwrap().kind {
assert_eq!(name, "Assert\\Length");
} else {
panic!("Expected ClassReference for second attribute");
}
}
#[test]
fn docblock_star_wildcard_in_generic_produces_correct_spans() {
let php = concat!(
"<?php\n",
"class Foo {\n",
" /**\n",
" * @param Relation<Model, *, *>|string $rel\n",
" */\n",
" public function bar($rel): void {}\n",
"}\n",
);
let map = parse_and_extract(php);
let relation_offset = php.find("Relation<").unwrap() as u32;
let hit = map.lookup(relation_offset);
assert!(
hit.is_some(),
"Should find ClassReference for Relation at offset {}",
relation_offset
);
if let SymbolKind::ClassReference { ref name, .. } = hit.unwrap().kind {
assert_eq!(name, "Relation", "Base class name should be 'Relation'");
} else {
panic!(
"Expected ClassReference for Relation, got {:?}",
hit.unwrap().kind
);
}
let model_offset = php.find("Model, *").unwrap() as u32;
let hit = map.lookup(model_offset);
assert!(
hit.is_some(),
"Should find ClassReference for Model at offset {}",
model_offset
);
if let SymbolKind::ClassReference { ref name, .. } = hit.unwrap().kind {
assert_eq!(name, "Model", "Generic arg should be 'Model'");
} else {
panic!(
"Expected ClassReference for Model, got {:?}",
hit.unwrap().kind
);
}
for span in &map.spans {
if let SymbolKind::ClassReference { ref name, .. } = span.kind {
assert!(
!name.contains('<') && !name.contains('*'),
"ClassReference name must not contain raw generic syntax or wildcards, got: {}",
name
);
}
}
}