use mago_span::HasSpan;
use mago_syntax::ast::class_like::constant::ClassLikeConstant;
use mago_syntax::ast::class_like::member::ClassLikeMember;
use mago_syntax::ast::class_like::method::Method;
use mago_syntax::ast::class_like::property::Property;
use mago_syntax::ast::function_like::function::Function;
use mago_syntax::ast::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ClassLikeContextKind {
Class,
Interface,
Trait,
Enum,
}
#[derive(Debug)]
pub(crate) enum MemberContext<'a> {
Method(&'a Method<'a>, bool),
Property(&'a Property<'a>),
Constant(&'a ClassLikeConstant<'a>),
TraitUse,
EnumCase,
None,
}
#[derive(Debug)]
pub(crate) enum CursorContext<'a> {
InClassLike {
kind: ClassLikeContextKind,
class_readonly: bool,
member: MemberContext<'a>,
all_members: &'a Sequence<'a, ClassLikeMember<'a>>,
},
InFunction(&'a Function<'a>, bool),
None,
}
pub(crate) fn find_cursor_context<'a>(
statements: &'a Sequence<'a, Statement<'a>>,
cursor: u32,
) -> CursorContext<'a> {
for stmt in statements.iter() {
let ctx = find_in_statement(stmt, cursor);
if !matches!(ctx, CursorContext::None) {
return ctx;
}
}
CursorContext::None
}
fn find_in_statement<'a>(stmt: &'a Statement<'a>, cursor: u32) -> CursorContext<'a> {
match stmt {
Statement::Namespace(ns) => {
for s in ns.statements().iter() {
let ctx = find_in_statement(s, cursor);
if !matches!(ctx, CursorContext::None) {
return ctx;
}
}
CursorContext::None
}
Statement::Function(func) => {
let span = func.span();
if cursor >= span.start.offset && cursor <= span.end.offset {
let body_start = func.body.left_brace.start.offset;
let in_body = cursor >= body_start;
CursorContext::InFunction(func, in_body)
} else {
CursorContext::None
}
}
Statement::Class(class) => {
let span = class.span();
if cursor >= span.start.offset && cursor <= span.end.offset {
let member = find_member_at_cursor(class.members.iter(), cursor);
CursorContext::InClassLike {
kind: ClassLikeContextKind::Class,
class_readonly: class.modifiers.contains_readonly(),
member,
all_members: &class.members,
}
} else {
CursorContext::None
}
}
Statement::Interface(iface) => {
let span = iface.span();
if cursor >= span.start.offset && cursor <= span.end.offset {
let member = find_member_at_cursor(iface.members.iter(), cursor);
CursorContext::InClassLike {
kind: ClassLikeContextKind::Interface,
class_readonly: false,
member,
all_members: &iface.members,
}
} else {
CursorContext::None
}
}
Statement::Trait(tr) => {
let span = tr.span();
if cursor >= span.start.offset && cursor <= span.end.offset {
let member = find_member_at_cursor(tr.members.iter(), cursor);
CursorContext::InClassLike {
kind: ClassLikeContextKind::Trait,
class_readonly: false,
member,
all_members: &tr.members,
}
} else {
CursorContext::None
}
}
Statement::Enum(en) => {
let span = en.span();
if cursor >= span.start.offset && cursor <= span.end.offset {
let member = find_member_at_cursor(en.members.iter(), cursor);
CursorContext::InClassLike {
kind: ClassLikeContextKind::Enum,
class_readonly: false,
member,
all_members: &en.members,
}
} else {
CursorContext::None
}
}
_ => CursorContext::None,
}
}
fn find_member_at_cursor<'a>(
members: impl Iterator<Item = &'a ClassLikeMember<'a>>,
cursor: u32,
) -> MemberContext<'a> {
for member in members {
let member_span = member.span();
if cursor < member_span.start.offset || cursor > member_span.end.offset {
continue;
}
return match member {
ClassLikeMember::Method(method) => {
let body_start = method.body.span().start.offset;
let in_body = cursor >= body_start;
MemberContext::Method(method, in_body)
}
ClassLikeMember::Property(property) => MemberContext::Property(property),
ClassLikeMember::Constant(constant) => MemberContext::Constant(constant),
ClassLikeMember::TraitUse(_) => MemberContext::TraitUse,
ClassLikeMember::EnumCase(_) => MemberContext::EnumCase,
};
}
MemberContext::None
}
#[cfg(test)]
mod tests {
use super::*;
use bumpalo::Bump;
fn ctx_at(php: &str, offset: u32) -> CursorContext<'_> {
let arena = Box::leak(Box::new(Bump::new()));
let file_id = mago_database::file::FileId::new("input.php");
let program = mago_syntax::parser::parse_file_content(arena, file_id, php);
find_cursor_context(&program.statements, offset)
}
#[test]
fn finds_class() {
let php = "<?php\nclass Foo {\n public function bar() {}\n}";
let pos = php.find("public function").unwrap() as u32;
let ctx = ctx_at(php, pos);
assert!(matches!(ctx, CursorContext::InClassLike { .. }));
}
#[test]
fn finds_interface() {
let php = "<?php\ninterface Foo {\n public function bar(): void;\n}";
let pos = php.find("public function").unwrap() as u32;
let ctx = ctx_at(php, pos);
assert!(matches!(ctx, CursorContext::InClassLike { .. }));
}
#[test]
fn finds_trait() {
let php = "<?php\ntrait Foo {\n protected function bar() {}\n}";
let pos = php.find("protected function").unwrap() as u32;
let ctx = ctx_at(php, pos);
assert!(matches!(ctx, CursorContext::InClassLike { .. }));
}
#[test]
fn finds_enum() {
let php = "<?php\nenum Foo {\n public function bar(): void {}\n}";
let pos = php.find("public function").unwrap() as u32;
let ctx = ctx_at(php, pos);
assert!(matches!(ctx, CursorContext::InClassLike { .. }));
}
#[test]
fn finds_method_on_signature() {
let php = "<?php\nclass Foo {\n public function bar() {}\n}";
let pos = php.find("bar").unwrap() as u32;
let ctx = ctx_at(php, pos);
match &ctx {
CursorContext::InClassLike {
member: MemberContext::Method(method, false),
..
} => assert_eq!(method.name.value, "bar"),
_ => panic!("should find method on signature"),
}
}
#[test]
fn detects_cursor_in_method_body() {
let php = "<?php\nclass Foo {\n public function bar() {\n $x = 1;\n }\n}";
let pos = php.find("$x = 1").unwrap() as u32;
let ctx = ctx_at(php, pos);
match &ctx {
CursorContext::InClassLike {
member: MemberContext::Method(method, true),
..
} => assert_eq!(method.name.value, "bar"),
_ => panic!("should find method with in_body=true"),
}
}
#[test]
fn finds_property() {
let php = "<?php\nclass Foo {\n protected string $bar;\n}";
let pos = php.find("protected string").unwrap() as u32;
let ctx = ctx_at(php, pos);
assert!(matches!(
ctx,
CursorContext::InClassLike {
member: MemberContext::Property(_),
..
}
));
}
#[test]
fn finds_constant() {
let php = "<?php\nclass Foo {\n private const BAR = 1;\n}";
let pos = php.find("private const").unwrap() as u32;
let ctx = ctx_at(php, pos);
assert!(matches!(
ctx,
CursorContext::InClassLike {
member: MemberContext::Constant(_),
..
}
));
}
#[test]
fn finds_class_in_namespace() {
let php = "<?php\nnamespace App;\nclass Foo {\n public function bar() {}\n}";
let pos = php.find("public function").unwrap() as u32;
let ctx = ctx_at(php, pos);
assert!(matches!(ctx, CursorContext::InClassLike { .. }));
}
#[test]
fn finds_class_in_braced_namespace() {
let php = "<?php\nnamespace App {\nclass Foo {\n private function bar() {}\n}\n}";
let pos = php.find("private function").unwrap() as u32;
let ctx = ctx_at(php, pos);
assert!(matches!(ctx, CursorContext::InClassLike { .. }));
}
#[test]
fn finds_standalone_function() {
let php = "<?php\nfunction foo() { return 1; }";
let pos = php.find("function foo").unwrap() as u32;
let ctx = ctx_at(php, pos);
assert!(matches!(ctx, CursorContext::InFunction(_, false)));
}
#[test]
fn detects_cursor_in_function_body() {
let php = "<?php\nfunction foo() { $x = 1; }";
let pos = php.find("$x = 1").unwrap() as u32;
let ctx = ctx_at(php, pos);
assert!(matches!(ctx, CursorContext::InFunction(_, true)));
}
#[test]
fn no_context_outside_class() {
let php = "<?php\n$x = 1;\n";
let ctx = ctx_at(php, 7);
assert!(matches!(ctx, CursorContext::None));
}
#[test]
fn no_context_on_class_keyword() {
let php = "<?php\nclass Foo {\n public function bar() {}\n}";
let pos = php.find("class Foo").unwrap() as u32;
let ctx = ctx_at(php, pos);
assert!(matches!(
ctx,
CursorContext::InClassLike {
member: MemberContext::None,
..
}
));
}
#[test]
fn all_members_returns_members() {
let php = "<?php\nclass Foo {\n public string $a;\n public function bar() {}\n private const C = 1;\n}";
let pos = php.find("public function").unwrap() as u32;
let ctx = ctx_at(php, pos);
match &ctx {
CursorContext::InClassLike { all_members, .. } => {
assert_eq!(all_members.len(), 3);
}
_ => panic!("should have members"),
}
}
#[test]
fn cursor_inside_class_body_finds_class_like() {
let php = "<?php\nclass Foo {\n public function bar() {}\n}";
let pos = php.find("public function").unwrap() as u32;
let ctx = ctx_at(php, pos);
assert!(matches!(ctx, CursorContext::InClassLike { .. }));
}
}