use bumpalo::Bump;
use mago_span::HasSpan;
use mago_syntax::ast::*;
use tower_lsp::lsp_types::{Position, Range, SelectionRange};
use crate::Backend;
use crate::util::{offset_to_position, position_to_offset};
impl Backend {
pub fn handle_selection_range(
&self,
content: &str,
positions: &[Position],
) -> Option<Vec<SelectionRange>> {
let arena = Bump::new();
let file_id = mago_database::file::FileId::new("input.php");
let program = mago_syntax::parser::parse_file_content(&arena, file_id, content);
let mut results = Vec::with_capacity(positions.len());
for pos in positions {
let offset = position_to_offset(content, *pos);
let mut spans: Vec<(u32, u32)> = Vec::new();
let file_span = (0u32, content.len() as u32);
spans.push(file_span);
for stmt in program.statements.iter() {
collect_spans_from_statement(stmt, offset, &mut spans);
}
spans.sort_unstable();
spans.dedup();
spans.sort_by(|a, b| {
let len_a = a.1.saturating_sub(a.0);
let len_b = b.1.saturating_sub(b.0);
len_b.cmp(&len_a).then(a.0.cmp(&b.0))
});
let selection_range = build_selection_range(content, &spans);
results.push(selection_range);
}
Some(results)
}
}
fn build_selection_range(content: &str, spans: &[(u32, u32)]) -> SelectionRange {
if spans.is_empty() {
let range = Range::new(Position::new(0, 0), Position::new(0, 0));
return SelectionRange {
range,
parent: None,
};
}
let mut current = to_selection_range(content, spans[0], None);
for &span in &spans[1..] {
current = to_selection_range(content, span, Some(current));
}
current
}
fn to_selection_range(
content: &str,
span: (u32, u32),
parent: Option<SelectionRange>,
) -> SelectionRange {
let start = offset_to_position(content, span.0 as usize);
let end = offset_to_position(content, span.1 as usize);
SelectionRange {
range: Range::new(start, end),
parent: parent.map(Box::new),
}
}
fn push_if_contains(span: mago_span::Span, offset: u32, spans: &mut Vec<(u32, u32)>) -> bool {
let start = span.start.offset;
let end = span.end.offset;
if start <= offset && offset <= end {
spans.push((start, end));
true
} else {
false
}
}
fn push_brace_pair(
left: mago_span::Span,
right: mago_span::Span,
offset: u32,
spans: &mut Vec<(u32, u32)>,
) {
let start = left.start.offset;
let end = right.end.offset;
if start <= offset && offset <= end {
spans.push((start, end));
}
}
fn push_block(block: &Block<'_>, offset: u32, spans: &mut Vec<(u32, u32)>) {
push_brace_pair(block.left_brace, block.right_brace, offset, spans);
for stmt in block.statements.iter() {
collect_spans_from_statement(stmt, offset, spans);
}
}
fn collect_spans_from_statement(stmt: &Statement<'_>, offset: u32, spans: &mut Vec<(u32, u32)>) {
let stmt_span = stmt.span();
if !push_if_contains(stmt_span, offset, spans) {
return;
}
match stmt {
Statement::Namespace(ns) => match &ns.body {
NamespaceBody::BraceDelimited(block) => {
push_block(block, offset, spans);
}
NamespaceBody::Implicit(body) => {
for inner in body.statements.iter() {
collect_spans_from_statement(inner, offset, spans);
}
}
},
Statement::Class(class) => {
push_brace_pair(class.left_brace, class.right_brace, offset, spans);
for member in class.members.iter() {
collect_spans_from_class_member(member, offset, spans);
}
}
Statement::Interface(iface) => {
push_brace_pair(iface.left_brace, iface.right_brace, offset, spans);
for member in iface.members.iter() {
collect_spans_from_class_member(member, offset, spans);
}
}
Statement::Trait(trait_def) => {
push_brace_pair(trait_def.left_brace, trait_def.right_brace, offset, spans);
for member in trait_def.members.iter() {
collect_spans_from_class_member(member, offset, spans);
}
}
Statement::Enum(enum_def) => {
push_brace_pair(enum_def.left_brace, enum_def.right_brace, offset, spans);
for member in enum_def.members.iter() {
collect_spans_from_class_member(member, offset, spans);
}
}
Statement::Function(func) => {
push_block(&func.body, offset, spans);
push_paren_pair(
func.parameter_list.left_parenthesis,
func.parameter_list.right_parenthesis,
offset,
spans,
);
for param in func.parameter_list.parameters.iter() {
collect_spans_from_parameter(param, offset, spans);
}
for inner in func.body.statements.iter() {
collect_spans_from_statement(inner, offset, spans);
}
}
Statement::If(if_stmt) => {
collect_spans_from_if(if_stmt, offset, spans);
}
Statement::Switch(switch_stmt) => {
collect_spans_from_expression(switch_stmt.expression, offset, spans);
match &switch_stmt.body {
SwitchBody::BraceDelimited(body) => {
push_brace_pair(body.left_brace, body.right_brace, offset, spans);
for case in body.cases.iter() {
let case_span = case.span();
if push_if_contains(case_span, offset, spans) {
for inner in case.statements().iter() {
collect_spans_from_statement(inner, offset, spans);
}
}
}
}
SwitchBody::ColonDelimited(body) => {
for case in body.cases.iter() {
let case_span = case.span();
if push_if_contains(case_span, offset, spans) {
for inner in case.statements().iter() {
collect_spans_from_statement(inner, offset, spans);
}
}
}
}
}
}
Statement::Foreach(foreach) => {
collect_spans_from_expression(foreach.expression, offset, spans);
let target_span = foreach.target.span();
let _ = push_if_contains(target_span, offset, spans);
match &foreach.target {
r#loop::foreach::ForeachTarget::Value(val) => {
collect_spans_from_expression(val.value, offset, spans);
}
r#loop::foreach::ForeachTarget::KeyValue(kv) => {
collect_spans_from_expression(kv.key, offset, spans);
collect_spans_from_expression(kv.value, offset, spans);
}
}
match &foreach.body {
ForeachBody::Statement(body) => {
collect_spans_from_statement(body, offset, spans);
}
ForeachBody::ColonDelimited(body) => {
for inner in body.statements.iter() {
collect_spans_from_statement(inner, offset, spans);
}
}
}
}
Statement::For(for_stmt) => {
for expr in for_stmt.initializations.iter() {
collect_spans_from_expression(expr, offset, spans);
}
for expr in for_stmt.conditions.iter() {
collect_spans_from_expression(expr, offset, spans);
}
for expr in for_stmt.increments.iter() {
collect_spans_from_expression(expr, offset, spans);
}
match &for_stmt.body {
ForBody::Statement(body) => {
collect_spans_from_statement(body, offset, spans);
}
ForBody::ColonDelimited(body) => {
for inner in body.statements.iter() {
collect_spans_from_statement(inner, offset, spans);
}
}
}
}
Statement::While(while_stmt) => {
collect_spans_from_expression(while_stmt.condition, offset, spans);
match &while_stmt.body {
WhileBody::Statement(body) => {
collect_spans_from_statement(body, offset, spans);
}
WhileBody::ColonDelimited(body) => {
for inner in body.statements.iter() {
collect_spans_from_statement(inner, offset, spans);
}
}
}
}
Statement::DoWhile(do_while) => {
collect_spans_from_expression(do_while.condition, offset, spans);
collect_spans_from_statement(do_while.statement, offset, spans);
}
Statement::Try(try_stmt) => {
push_block(&try_stmt.block, offset, spans);
for inner in try_stmt.block.statements.iter() {
collect_spans_from_statement(inner, offset, spans);
}
for catch in try_stmt.catch_clauses.iter() {
let catch_span = catch.span();
if push_if_contains(catch_span, offset, spans) {
push_block(&catch.block, offset, spans);
for inner in catch.block.statements.iter() {
collect_spans_from_statement(inner, offset, spans);
}
}
}
if let Some(ref finally) = try_stmt.finally_clause {
let finally_span = finally.span();
if push_if_contains(finally_span, offset, spans) {
push_block(&finally.block, offset, spans);
for inner in finally.block.statements.iter() {
collect_spans_from_statement(inner, offset, spans);
}
}
}
}
Statement::Return(ret) => {
if let Some(value) = ret.value {
collect_spans_from_expression(value, offset, spans);
}
}
Statement::Expression(expr_stmt) => {
collect_spans_from_expression(expr_stmt.expression, offset, spans);
}
Statement::Echo(echo) => {
for expr in echo.values.iter() {
collect_spans_from_expression(expr, offset, spans);
}
}
Statement::Unset(unset) => {
for expr in unset.values.iter() {
collect_spans_from_expression(expr, offset, spans);
}
}
Statement::Block(block) => {
push_block(block, offset, spans);
}
Statement::Declare(declare) => match &declare.body {
DeclareBody::Statement(body) => {
collect_spans_from_statement(body, offset, spans);
}
DeclareBody::ColonDelimited(body) => {
for inner in body.statements.iter() {
collect_spans_from_statement(inner, offset, spans);
}
}
},
Statement::Global(_)
| Statement::Static(_)
| Statement::Use(_)
| Statement::Constant(_)
| Statement::Goto(_)
| Statement::Label(_)
| Statement::Continue(_)
| Statement::Break(_)
| Statement::OpeningTag(_)
| Statement::ClosingTag(_)
| Statement::Inline(_)
| Statement::EchoTag(_)
| Statement::HaltCompiler(_)
| Statement::Noop(_) => {}
_ => {}
}
}
fn collect_spans_from_class_member(
member: &class_like::member::ClassLikeMember<'_>,
offset: u32,
spans: &mut Vec<(u32, u32)>,
) {
use class_like::member::ClassLikeMember;
let member_span = member.span();
if !push_if_contains(member_span, offset, spans) {
return;
}
match member {
ClassLikeMember::Method(method) => {
push_paren_pair(
method.parameter_list.left_parenthesis,
method.parameter_list.right_parenthesis,
offset,
spans,
);
for param in method.parameter_list.parameters.iter() {
collect_spans_from_parameter(param, offset, spans);
}
use class_like::method::MethodBody;
match &method.body {
MethodBody::Concrete(block) => {
push_block(block, offset, spans);
for inner in block.statements.iter() {
collect_spans_from_statement(inner, offset, spans);
}
}
MethodBody::Abstract(_) => {}
}
}
ClassLikeMember::Property(prop) => {
use class_like::property::{Property, PropertyItem};
match prop {
Property::Plain(plain) => {
for item in plain.items.iter() {
match item {
PropertyItem::Abstract(abs) => {
let _ = push_if_contains(abs.variable.span(), offset, spans);
}
PropertyItem::Concrete(concrete) => {
let item_span = concrete.span();
if push_if_contains(item_span, offset, spans) {
let _ =
push_if_contains(concrete.variable.span(), offset, spans);
collect_spans_from_expression(concrete.value, offset, spans);
}
}
}
}
}
Property::Hooked(hooked) => match &hooked.item {
PropertyItem::Abstract(abs) => {
let _ = push_if_contains(abs.variable.span(), offset, spans);
}
PropertyItem::Concrete(concrete) => {
let item_span = concrete.span();
if push_if_contains(item_span, offset, spans) {
let _ = push_if_contains(concrete.variable.span(), offset, spans);
collect_spans_from_expression(concrete.value, offset, spans);
}
}
},
}
}
ClassLikeMember::Constant(constant) => {
for item in constant.items.iter() {
let item_span = item.span();
if push_if_contains(item_span, offset, spans) {
collect_spans_from_expression(item.value, offset, spans);
}
}
}
ClassLikeMember::EnumCase(enum_case) => {
use class_like::enum_case::EnumCaseItem;
match &enum_case.item {
EnumCaseItem::Unit(unit) => {
let _ = push_if_contains(unit.span(), offset, spans);
}
EnumCaseItem::Backed(backed) => {
let item_span = backed.span();
if push_if_contains(item_span, offset, spans) {
collect_spans_from_expression(backed.value, offset, spans);
}
}
}
}
ClassLikeMember::TraitUse(_) => {
}
}
}
fn collect_spans_from_parameter(
param: &function_like::parameter::FunctionLikeParameter<'_>,
offset: u32,
spans: &mut Vec<(u32, u32)>,
) {
let param_span = param.span();
if push_if_contains(param_span, offset, spans) {
let _ = push_if_contains(param.variable.span(), offset, spans);
if let Some(ref default) = param.default_value {
collect_spans_from_expression(default.value, offset, spans);
}
}
}
fn collect_spans_from_if(
if_stmt: &control_flow::r#if::If<'_>,
offset: u32,
spans: &mut Vec<(u32, u32)>,
) {
collect_spans_from_expression(if_stmt.condition, offset, spans);
match &if_stmt.body {
IfBody::Statement(body) => {
collect_spans_from_statement(body.statement, offset, spans);
for elseif in body.else_if_clauses.iter() {
let elseif_span: mago_span::Span = elseif.span();
if push_if_contains(elseif_span, offset, spans) {
collect_spans_from_expression(elseif.condition, offset, spans);
collect_spans_from_statement(elseif.statement, offset, spans);
}
}
if let Some(ref else_clause) = body.else_clause {
let else_span: mago_span::Span = else_clause.span();
if push_if_contains(else_span, offset, spans) {
collect_spans_from_statement(else_clause.statement, offset, spans);
}
}
}
IfBody::ColonDelimited(body) => {
for inner in body.statements.iter() {
collect_spans_from_statement(inner, offset, spans);
}
for elseif in body.else_if_clauses.iter() {
let elseif_span: mago_span::Span = elseif.span();
if push_if_contains(elseif_span, offset, spans) {
collect_spans_from_expression(elseif.condition, offset, spans);
for inner in elseif.statements.iter() {
collect_spans_from_statement(inner, offset, spans);
}
}
}
if let Some(ref else_clause) = body.else_clause {
let else_span: mago_span::Span = else_clause.span();
if push_if_contains(else_span, offset, spans) {
for inner in else_clause.statements.iter() {
collect_spans_from_statement(inner, offset, spans);
}
}
}
}
}
}
fn collect_spans_from_expression(expr: &Expression<'_>, offset: u32, spans: &mut Vec<(u32, u32)>) {
let expr_span = expr.span();
if !push_if_contains(expr_span, offset, spans) {
return;
}
match expr {
Expression::Binary(bin) => {
collect_spans_from_expression(bin.lhs, offset, spans);
collect_spans_from_expression(bin.rhs, offset, spans);
}
Expression::UnaryPrefix(unary) => {
collect_spans_from_expression(unary.operand, offset, spans);
}
Expression::UnaryPostfix(unary) => {
collect_spans_from_expression(unary.operand, offset, spans);
}
Expression::Parenthesized(paren) => {
collect_spans_from_expression(paren.expression, offset, spans);
}
Expression::Assignment(assign) => {
collect_spans_from_expression(assign.lhs, offset, spans);
collect_spans_from_expression(assign.rhs, offset, spans);
}
Expression::Conditional(cond) => {
collect_spans_from_expression(cond.condition, offset, spans);
if let Some(then_expr) = cond.then {
collect_spans_from_expression(then_expr, offset, spans);
}
collect_spans_from_expression(cond.r#else, offset, spans);
}
Expression::Call(call) => {
match call {
Call::Function(func_call) => {
collect_spans_from_expression(func_call.function, offset, spans);
collect_spans_from_argument_list(&func_call.argument_list, offset, spans);
}
Call::Method(method_call) => {
collect_spans_from_expression(method_call.object, offset, spans);
let sel_span = method_call.method.span();
let _ = push_if_contains(sel_span, offset, spans);
collect_spans_from_argument_list(&method_call.argument_list, offset, spans);
}
Call::NullSafeMethod(method_call) => {
collect_spans_from_expression(method_call.object, offset, spans);
let sel_span = method_call.method.span();
let _ = push_if_contains(sel_span, offset, spans);
collect_spans_from_argument_list(&method_call.argument_list, offset, spans);
}
Call::StaticMethod(static_call) => {
collect_spans_from_expression(static_call.class, offset, spans);
let sel_span = static_call.method.span();
let _ = push_if_contains(sel_span, offset, spans);
collect_spans_from_argument_list(&static_call.argument_list, offset, spans);
}
}
}
Expression::Access(access) => match access {
Access::Property(prop) => {
collect_spans_from_expression(prop.object, offset, spans);
let sel_span = prop.property.span();
let _ = push_if_contains(sel_span, offset, spans);
}
Access::NullSafeProperty(prop) => {
collect_spans_from_expression(prop.object, offset, spans);
let sel_span = prop.property.span();
let _ = push_if_contains(sel_span, offset, spans);
}
Access::StaticProperty(prop) => {
collect_spans_from_expression(prop.class, offset, spans);
let var_span = prop.property.span();
let _ = push_if_contains(var_span, offset, spans);
}
Access::ClassConstant(cc) => {
collect_spans_from_expression(cc.class, offset, spans);
let sel_span = cc.constant.span();
let _ = push_if_contains(sel_span, offset, spans);
}
},
Expression::Instantiation(inst) => {
collect_spans_from_expression(inst.class, offset, spans);
if let Some(ref args) = inst.argument_list {
collect_spans_from_argument_list(args, offset, spans);
}
}
Expression::Array(array) => {
for element in array.elements.iter() {
let el_span = element.span();
if push_if_contains(el_span, offset, spans) {
match element {
ArrayElement::KeyValue(kv) => {
collect_spans_from_expression(kv.key, offset, spans);
collect_spans_from_expression(kv.value, offset, spans);
}
ArrayElement::Value(val) => {
collect_spans_from_expression(val.value, offset, spans);
}
ArrayElement::Variadic(var) => {
collect_spans_from_expression(var.value, offset, spans);
}
ArrayElement::Missing(_) => {}
}
}
}
}
Expression::LegacyArray(array) => {
for element in array.elements.iter() {
let el_span = element.span();
if push_if_contains(el_span, offset, spans) {
match element {
ArrayElement::KeyValue(kv) => {
collect_spans_from_expression(kv.key, offset, spans);
collect_spans_from_expression(kv.value, offset, spans);
}
ArrayElement::Value(val) => {
collect_spans_from_expression(val.value, offset, spans);
}
ArrayElement::Variadic(var) => {
collect_spans_from_expression(var.value, offset, spans);
}
ArrayElement::Missing(_) => {}
}
}
}
}
Expression::List(list) => {
for element in list.elements.iter() {
let el_span = element.span();
let _ = push_if_contains(el_span, offset, spans);
}
}
Expression::ArrayAccess(access) => {
collect_spans_from_expression(access.array, offset, spans);
collect_spans_from_expression(access.index, offset, spans);
}
Expression::ArrayAppend(append) => {
collect_spans_from_expression(append.array, offset, spans);
}
Expression::Closure(closure) => {
push_paren_pair(
closure.parameter_list.left_parenthesis,
closure.parameter_list.right_parenthesis,
offset,
spans,
);
for param in closure.parameter_list.parameters.iter() {
collect_spans_from_parameter(param, offset, spans);
}
push_block(&closure.body, offset, spans);
for inner in closure.body.statements.iter() {
collect_spans_from_statement(inner, offset, spans);
}
}
Expression::ArrowFunction(arrow) => {
push_paren_pair(
arrow.parameter_list.left_parenthesis,
arrow.parameter_list.right_parenthesis,
offset,
spans,
);
for param in arrow.parameter_list.parameters.iter() {
collect_spans_from_parameter(param, offset, spans);
}
collect_spans_from_expression(arrow.expression, offset, spans);
}
Expression::AnonymousClass(anon) => {
push_brace_pair(anon.left_brace, anon.right_brace, offset, spans);
for member in anon.members.iter() {
collect_spans_from_class_member(member, offset, spans);
}
}
Expression::Match(match_expr) => {
collect_spans_from_expression(match_expr.expression, offset, spans);
push_brace_pair(match_expr.left_brace, match_expr.right_brace, offset, spans);
for arm in match_expr.arms.iter() {
let arm_span = arm.span();
if push_if_contains(arm_span, offset, spans) {
collect_spans_from_expression(arm.expression(), offset, spans);
}
}
}
Expression::Yield(yield_expr) => match yield_expr {
Yield::Value(yv) => {
if let Some(value) = yv.value {
collect_spans_from_expression(value, offset, spans);
}
}
Yield::Pair(yp) => {
collect_spans_from_expression(yp.key, offset, spans);
collect_spans_from_expression(yp.value, offset, spans);
}
Yield::From(yf) => {
collect_spans_from_expression(yf.iterator, offset, spans);
}
},
Expression::Throw(throw) => {
collect_spans_from_expression(throw.exception, offset, spans);
}
Expression::Clone(clone) => {
collect_spans_from_expression(clone.object, offset, spans);
}
Expression::Construct(construct) => match construct {
Construct::Isset(isset) => {
for expr in isset.values.iter() {
collect_spans_from_expression(expr, offset, spans);
}
}
Construct::Empty(empty) => {
collect_spans_from_expression(empty.value, offset, spans);
}
Construct::Eval(eval) => {
collect_spans_from_expression(eval.value, offset, spans);
}
Construct::Include(inc) => {
collect_spans_from_expression(inc.value, offset, spans);
}
Construct::IncludeOnce(inc) => {
collect_spans_from_expression(inc.value, offset, spans);
}
Construct::Require(req) => {
collect_spans_from_expression(req.value, offset, spans);
}
Construct::RequireOnce(req) => {
collect_spans_from_expression(req.value, offset, spans);
}
Construct::Print(print) => {
collect_spans_from_expression(print.value, offset, spans);
}
Construct::Exit(exit) => {
if let Some(ref args) = exit.arguments {
collect_spans_from_argument_list(args, offset, spans);
}
}
Construct::Die(die) => {
if let Some(ref args) = die.arguments {
collect_spans_from_argument_list(args, offset, spans);
}
}
},
Expression::CompositeString(composite) => {
for part in composite.parts().iter() {
match part {
string::StringPart::Expression(expr_ref) => {
collect_spans_from_expression(expr_ref, offset, spans);
}
string::StringPart::BracedExpression(braced) => {
collect_spans_from_expression(braced.expression, offset, spans);
}
_ => {}
}
}
}
Expression::Pipe(pipe) => {
collect_spans_from_expression(pipe.input, offset, spans);
collect_spans_from_expression(pipe.callable, offset, spans);
}
Expression::Literal(_)
| Expression::Variable(_)
| Expression::ConstantAccess(_)
| Expression::Identifier(_)
| Expression::Parent(_)
| Expression::Static(_)
| Expression::Self_(_)
| Expression::MagicConstant(_)
| Expression::PartialApplication(_)
| Expression::Error(_) => {}
_ => {}
}
}
fn collect_spans_from_argument_list(
args: &argument::ArgumentList<'_>,
offset: u32,
spans: &mut Vec<(u32, u32)>,
) {
push_paren_pair(args.left_parenthesis, args.right_parenthesis, offset, spans);
for arg in args.arguments.iter() {
let arg_span = arg.span();
if push_if_contains(arg_span, offset, spans) {
match arg {
argument::Argument::Positional(pos) => {
collect_spans_from_expression(pos.value, offset, spans);
}
argument::Argument::Named(named) => {
collect_spans_from_expression(named.value, offset, spans);
}
}
}
}
}
fn push_paren_pair(
left: mago_span::Span,
right: mago_span::Span,
offset: u32,
spans: &mut Vec<(u32, u32)>,
) {
let start = left.start.offset;
let end = right.end.offset;
if start <= offset && offset <= end {
spans.push((start, end));
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_fixtures::make_backend;
fn selection_ranges(content: &str, positions: &[Position]) -> Vec<SelectionRange> {
let backend = make_backend();
backend
.handle_selection_range(content, positions)
.unwrap_or_default()
}
fn flatten(sel: &SelectionRange) -> Vec<Range> {
let mut result = vec![sel.range];
let mut current = &sel.parent;
while let Some(parent) = current {
result.push(parent.range);
current = &parent.parent;
}
result
}
#[test]
fn single_variable_in_function() {
let content = r#"<?php
function hello() {
$name = "world";
echo $name;
}
"#;
let results = selection_ranges(content, &[Position::new(3, 9)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 3,
"Expected at least 3 selection range levels, got {}",
ranges.len()
);
let innermost = &ranges[0];
let outermost = ranges.last().unwrap();
assert!(
innermost.start.line >= outermost.start.line
|| innermost.start.character >= outermost.start.character,
"Innermost range should be within outermost"
);
}
#[test]
fn class_method_body() {
let content = r#"<?php
class Greeter {
public function greet(string $name): string {
return "Hello, " . $name;
}
}
"#;
let results = selection_ranges(content, &[Position::new(3, 29)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 4,
"Expected at least 4 selection range levels, got {}",
ranges.len()
);
}
#[test]
fn multiple_positions() {
let content = r#"<?php
$a = 1;
$b = 2;
"#;
let results = selection_ranges(content, &[Position::new(1, 1), Position::new(2, 1)]);
assert_eq!(results.len(), 2);
for result in &results {
let ranges = flatten(result);
assert!(!ranges.is_empty());
}
}
#[test]
fn nested_if_statement() {
let content = r#"<?php
if (true) {
if (false) {
echo "inner";
}
}
"#;
let results = selection_ranges(content, &[Position::new(3, 14)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 4,
"Expected at least 4 levels, got {}",
ranges.len()
);
}
#[test]
fn empty_file() {
let content = "<?php\n";
let results = selection_ranges(content, &[Position::new(0, 3)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(!ranges.is_empty());
}
#[test]
fn instanceof_in_method_has_fine_grained_levels() {
let content = r#"<?php
class Demo {
public function test(): void {
$x = new User();
if ($x instanceof User) {
$x->getEmail();
}
}
}
"#;
let results = selection_ranges(content, &[Position::new(5, 17)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 7,
"Expected at least 7 fine-grained levels for method call inside if, got {}: {:?}",
ranges.len(),
ranges,
);
}
#[test]
fn ranges_are_nested() {
let content = r#"<?php
function test() {
$x = [1, 2, 3];
}
"#;
let results = selection_ranges(content, &[Position::new(2, 13)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
for window in ranges.windows(2) {
let inner = &window[0];
let outer = &window[1];
assert!(
(inner.start.line > outer.start.line
|| (inner.start.line == outer.start.line
&& inner.start.character >= outer.start.character))
&& (inner.end.line < outer.end.line
|| (inner.end.line == outer.end.line
&& inner.end.character <= outer.end.character)),
"Inner range {:?} should be contained within outer range {:?}",
inner,
outer,
);
}
}
fn assert_nested(ranges: &[Range]) {
for window in ranges.windows(2) {
let inner = &window[0];
let outer = &window[1];
assert!(
(inner.start.line > outer.start.line
|| (inner.start.line == outer.start.line
&& inner.start.character >= outer.start.character))
&& (inner.end.line < outer.end.line
|| (inner.end.line == outer.end.line
&& inner.end.character <= outer.end.character)),
"Inner range {:?} should be contained within outer range {:?}",
inner,
outer,
);
}
}
#[test]
fn switch_statement_case_body() {
let content = r#"<?php
switch ($x) {
case 1:
echo "one";
break;
case 2:
echo "two";
break;
default:
echo "other";
}
"#;
let results = selection_ranges(content, &[Position::new(3, 14)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 4,
"Expected at least 4 levels for switch case body, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn foreach_value_variable() {
let content = r#"<?php
$items = [1, 2, 3];
foreach ($items as $item) {
echo $item;
}
"#;
let results = selection_ranges(content, &[Position::new(3, 10)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 3,
"Expected at least 3 levels for foreach value, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn foreach_key_value() {
let content = r#"<?php
$map = ['a' => 1];
foreach ($map as $key => $val) {
echo $key;
}
"#;
let results = selection_ranges(content, &[Position::new(3, 10)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 3,
"Expected at least 3 levels for foreach key-value body, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn foreach_cursor_on_key_target() {
let content = r#"<?php
$map = ['a' => 1];
foreach ($map as $key => $val) {
echo $val;
}
"#;
let results = selection_ranges(content, &[Position::new(2, 18)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 3,
"Expected at least 3 levels for foreach key target, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn for_loop_body() {
let content = r#"<?php
for ($i = 0; $i < 10; $i++) {
echo $i;
}
"#;
let results = selection_ranges(content, &[Position::new(2, 10)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 3,
"Expected at least 3 levels for for loop body, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn while_loop_body() {
let content = r#"<?php
while (true) {
echo "loop";
}
"#;
let results = selection_ranges(content, &[Position::new(2, 10)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 3,
"Expected at least 3 levels for while body, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn do_while_body() {
let content = r#"<?php
do {
echo "loop";
} while (true);
"#;
let results = selection_ranges(content, &[Position::new(2, 10)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 3,
"Expected at least 3 levels for do-while body, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn try_body() {
let content = r#"<?php
try {
echo "try";
} catch (\Exception $e) {
echo "catch";
} finally {
echo "finally";
}
"#;
let results = selection_ranges(content, &[Position::new(2, 10)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 4,
"Expected at least 4 levels for try body, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn catch_body() {
let content = r#"<?php
try {
echo "try";
} catch (\Exception $e) {
echo "catch";
} finally {
echo "finally";
}
"#;
let results = selection_ranges(content, &[Position::new(4, 10)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 5,
"Expected at least 5 levels for catch body, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn finally_body() {
let content = r#"<?php
try {
echo "try";
} catch (\Exception $e) {
echo "catch";
} finally {
echo "finally";
}
"#;
let results = selection_ranges(content, &[Position::new(6, 10)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 5,
"Expected at least 5 levels for finally body, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn return_statement_value() {
let content = r#"<?php
function foo() {
return 42;
}
"#;
let results = selection_ranges(content, &[Position::new(2, 11)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 4,
"Expected at least 4 levels for return value, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn echo_statement_value() {
let content = r#"<?php
echo "hello", "world";
"#;
let results = selection_ranges(content, &[Position::new(1, 15)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 3,
"Expected at least 3 levels for echo value, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn closure_body() {
let content = r#"<?php
$fn = function ($x) {
return $x + 1;
};
"#;
let results = selection_ranges(content, &[Position::new(2, 11)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 5,
"Expected at least 5 levels for closure body, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn arrow_function_expression_body() {
let content = r#"<?php
$fn = fn($x) => $x + 1;
"#;
let results = selection_ranges(content, &[Position::new(1, 17)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 4,
"Expected at least 4 levels for arrow function body, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn match_arm_expression() {
let content = r#"<?php
$result = match($x) {
1 => "one",
2 => "two",
default => "other",
};
"#;
let results = selection_ranges(content, &[Position::new(3, 10)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 5,
"Expected at least 5 levels for match arm, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn anonymous_class_method() {
let content = r#"<?php
$obj = new class {
public function hello() {
echo "hi";
}
};
"#;
let results = selection_ranges(content, &[Position::new(3, 14)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 6,
"Expected at least 6 levels for anonymous class method, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn array_key_value_element() {
let content = r#"<?php
$x = ['key' => 'value', 'b' => 'c'];
"#;
let results = selection_ranges(content, &[Position::new(1, 17)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 4,
"Expected at least 4 levels for array key-value element, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn legacy_array_elements() {
let content = r#"<?php
$x = array('a' => 1, 'b' => 2);
"#;
let results = selection_ranges(content, &[Position::new(1, 19)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 4,
"Expected at least 4 levels for legacy array element, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn list_expression() {
let content = r#"<?php
list($a, $b) = [1, 2];
"#;
let results = selection_ranges(content, &[Position::new(1, 5)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 3,
"Expected at least 3 levels for list expression, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn binary_expression_rhs() {
let content = r#"<?php
$c = $a + $b;
"#;
let results = selection_ranges(content, &[Position::new(1, 11)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 4,
"Expected at least 4 levels for binary rhs, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn ternary_else_branch() {
let content = r#"<?php
$x = true ? "yes" : "no";
"#;
let results = selection_ranges(content, &[Position::new(1, 22)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 4,
"Expected at least 4 levels for ternary else, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn property_access() {
let content = r#"<?php
$x = $obj->prop;
"#;
let results = selection_ranges(content, &[Position::new(1, 12)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 4,
"Expected at least 4 levels for property access, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn static_method_call() {
let content = r#"<?php
$x = Foo::bar();
"#;
let results = selection_ranges(content, &[Position::new(1, 11)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 4,
"Expected at least 4 levels for static method call, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn instantiation_expression() {
let content = r#"<?php
$x = new Foo();
"#;
let results = selection_ranges(content, &[Position::new(1, 10)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 4,
"Expected at least 4 levels for instantiation, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn yield_value() {
let content = r#"<?php
function gen() {
yield 42;
}
"#;
let results = selection_ranges(content, &[Position::new(2, 10)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 5,
"Expected at least 5 levels for yield value, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn yield_pair() {
let content = r#"<?php
function gen() {
yield 'key' => 'value';
}
"#;
let results = selection_ranges(content, &[Position::new(2, 20)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 5,
"Expected at least 5 levels for yield pair, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn yield_from() {
let content = r#"<?php
function gen() {
yield from other();
}
"#;
let results = selection_ranges(content, &[Position::new(2, 16)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 5,
"Expected at least 5 levels for yield from, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn throw_expression() {
let content = r#"<?php
throw new \Exception("error");
"#;
let results = selection_ranges(content, &[Position::new(1, 12)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 3,
"Expected at least 3 levels for throw expression, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn clone_expression() {
let content = r#"<?php
$y = clone $x;
"#;
let results = selection_ranges(content, &[Position::new(1, 12)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 4,
"Expected at least 4 levels for clone expression, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn construct_isset() {
let content = r#"<?php
$x = isset($a, $b);
"#;
let results = selection_ranges(content, &[Position::new(1, 12)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 4,
"Expected at least 4 levels for isset, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn construct_empty() {
let content = r#"<?php
$x = empty($a);
"#;
let results = selection_ranges(content, &[Position::new(1, 12)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 4,
"Expected at least 4 levels for empty, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn construct_eval() {
let content = r#"<?php
eval('echo 1;');
"#;
let results = selection_ranges(content, &[Position::new(1, 6)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 3,
"Expected at least 3 levels for eval, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn construct_include() {
let content = r#"<?php
include 'file.php';
"#;
let results = selection_ranges(content, &[Position::new(1, 10)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 3,
"Expected at least 3 levels for include, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn construct_include_once() {
let content = r#"<?php
include_once 'file.php';
"#;
let results = selection_ranges(content, &[Position::new(1, 15)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 3,
"Expected at least 3 levels for include_once, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn construct_require() {
let content = r#"<?php
require 'file.php';
"#;
let results = selection_ranges(content, &[Position::new(1, 10)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 3,
"Expected at least 3 levels for require, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn construct_require_once() {
let content = r#"<?php
require_once 'file.php';
"#;
let results = selection_ranges(content, &[Position::new(1, 15)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 3,
"Expected at least 3 levels for require_once, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn namespace_brace_delimited() {
let content = r#"<?php
namespace App {
function foo() {
echo "hello";
}
}
"#;
let results = selection_ranges(content, &[Position::new(3, 14)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 5,
"Expected at least 5 levels for braced namespace, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn namespace_implicit() {
let content = r#"<?php
namespace App;
function foo() {
echo "hello";
}
"#;
let results = selection_ranges(content, &[Position::new(3, 10)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 5,
"Expected at least 5 levels for implicit namespace, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn interface_method() {
let content = r#"<?php
interface Greetable {
public function greet(string $name): string;
}
"#;
let results = selection_ranges(content, &[Position::new(2, 35)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 4,
"Expected at least 4 levels for interface method, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn trait_method_body() {
let content = r#"<?php
trait Greeter {
public function greet(): string {
return "hello";
}
}
"#;
let results = selection_ranges(content, &[Position::new(3, 16)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 5,
"Expected at least 5 levels for trait method body, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn backed_enum_case() {
let content = r#"<?php
enum Color: string {
case Red = 'red';
case Blue = 'blue';
}
"#;
let results = selection_ranges(content, &[Position::new(2, 16)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 4,
"Expected at least 4 levels for backed enum case, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn method_parameter_variable() {
let content = r#"<?php
class Foo {
public function bar(int $x, string $y): void {}
}
"#;
let results = selection_ranges(content, &[Position::new(2, 40)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 4,
"Expected at least 4 levels for method parameter, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn function_parameter_default() {
let content = r#"<?php
function foo(int $x = 42) {
echo $x;
}
"#;
let results = selection_ranges(content, &[Position::new(1, 22)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 4,
"Expected at least 4 levels for param default, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn named_argument_value() {
let content = r#"<?php
foo(name: "John");
"#;
let results = selection_ranges(content, &[Position::new(1, 11)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 4,
"Expected at least 4 levels for named argument, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn unset_statement() {
let content = r#"<?php
unset($a, $b);
"#;
let results = selection_ranges(content, &[Position::new(1, 6)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 3,
"Expected at least 3 levels for unset, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn declare_statement_body() {
let content = r#"<?php
declare(strict_types=1) {
echo "strict";
}
"#;
let results = selection_ranges(content, &[Position::new(2, 10)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 3,
"Expected at least 3 levels for declare body, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn composite_string_expression() {
let content = r#"<?php
$name = "world";
echo "hello {$name}!";
"#;
let results = selection_ranges(content, &[Position::new(2, 14)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 3,
"Expected at least 3 levels for composite string, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn pipe_expression() {
let content = r#"<?php
$x = $a |> 'strtoupper';
"#;
let results = selection_ranges(content, &[Position::new(1, 6)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 2,
"Expected at least 2 levels for pipe expression, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn elseif_body() {
let content = r#"<?php
if (true) {
echo "a";
} elseif (false) {
echo "b";
} else {
echo "c";
}
"#;
let results = selection_ranges(content, &[Position::new(4, 10)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 4,
"Expected at least 4 levels for elseif body, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn else_body() {
let content = r#"<?php
if (true) {
echo "a";
} else {
echo "c";
}
"#;
let results = selection_ranges(content, &[Position::new(4, 10)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 4,
"Expected at least 4 levels for else body, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn colon_delimited_if_body() {
let content = r#"<?php
if (true):
echo "a";
elseif (false):
echo "b";
else:
echo "c";
endif;
"#;
let results = selection_ranges(content, &[Position::new(2, 10)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 3,
"Expected at least 3 levels for colon-delimited if body, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn colon_delimited_elseif_body() {
let content = r#"<?php
if (true):
echo "a";
elseif (false):
echo "b";
else:
echo "c";
endif;
"#;
let results = selection_ranges(content, &[Position::new(4, 10)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 4,
"Expected at least 4 levels for colon-delimited elseif body, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn colon_delimited_else_body() {
let content = r#"<?php
if (true):
echo "a";
else:
echo "c";
endif;
"#;
let results = selection_ranges(content, &[Position::new(4, 10)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 4,
"Expected at least 4 levels for colon-delimited else body, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn class_constant_value() {
let content = r#"<?php
class Foo {
const BAR = 42;
}
"#;
let results = selection_ranges(content, &[Position::new(2, 16)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 4,
"Expected at least 4 levels for class constant, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn enum_backed_case_value() {
let content = r#"<?php
enum Status: int {
case Active = 1;
case Inactive = 0;
}
"#;
let results = selection_ranges(content, &[Position::new(3, 20)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 4,
"Expected at least 4 levels for enum backed case value, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn property_with_default() {
let content = r#"<?php
class Foo {
public int $x = 42;
}
"#;
let results = selection_ranges(content, &[Position::new(2, 21)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 4,
"Expected at least 4 levels for property default, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn array_access_expression() {
let content = r#"<?php
$x = $arr[0];
"#;
let results = selection_ranges(content, &[Position::new(1, 10)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 4,
"Expected at least 4 levels for array access, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn array_append_expression() {
let content = r#"<?php
$arr[] = 42;
"#;
let results = selection_ranges(content, &[Position::new(1, 1)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 3,
"Expected at least 3 levels for array append, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn unary_prefix_expression() {
let content = r#"<?php
$x = !$flag;
"#;
let results = selection_ranges(content, &[Position::new(1, 7)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 4,
"Expected at least 4 levels for unary prefix, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn unary_postfix_expression() {
let content = r#"<?php
$x++;
"#;
let results = selection_ranges(content, &[Position::new(1, 1)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 3,
"Expected at least 3 levels for unary postfix, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn parenthesized_expression() {
let content = r#"<?php
$x = (1 + 2);
"#;
let results = selection_ranges(content, &[Position::new(1, 6)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 4,
"Expected at least 4 levels for parenthesized, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn assignment_expression() {
let content = r#"<?php
$x = $y = 5;
"#;
let results = selection_ranges(content, &[Position::new(1, 10)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 4,
"Expected at least 4 levels for chained assignment, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn method_call_expression() {
let content = r#"<?php
$obj->method(42);
"#;
let results = selection_ranges(content, &[Position::new(1, 13)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 4,
"Expected at least 4 levels for method call arg, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn null_safe_property_access() {
let content = r#"<?php
$x = $obj?->prop;
"#;
let results = selection_ranges(content, &[Position::new(1, 13)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 4,
"Expected at least 4 levels for null-safe property access, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn static_property_access() {
let content = r#"<?php
$x = Foo::$bar;
"#;
let results = selection_ranges(content, &[Position::new(1, 11)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 4,
"Expected at least 4 levels for static property access, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn class_constant_access() {
let content = r#"<?php
$x = Foo::BAR;
"#;
let results = selection_ranges(content, &[Position::new(1, 11)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 4,
"Expected at least 4 levels for class constant access, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn null_safe_method_call() {
let content = r#"<?php
$x = $obj?->method(1);
"#;
let results = selection_ranges(content, &[Position::new(1, 14)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 4,
"Expected at least 4 levels for null-safe method call, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn function_call_expression() {
let content = r#"<?php
strlen("hello");
"#;
let results = selection_ranges(content, &[Position::new(1, 8)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 4,
"Expected at least 4 levels for function call, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn array_variadic_element() {
let content = r#"<?php
$x = [...$arr];
"#;
let results = selection_ranges(content, &[Position::new(1, 10)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 4,
"Expected at least 4 levels for array variadic, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn closure_parameter() {
let content = r#"<?php
$fn = function (int $x) {
return $x;
};
"#;
let results = selection_ranges(content, &[Position::new(1, 20)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 5,
"Expected at least 5 levels for closure parameter, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn arrow_function_parameter() {
let content = r#"<?php
$fn = fn(int $x) => $x + 1;
"#;
let results = selection_ranges(content, &[Position::new(1, 14)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 5,
"Expected at least 5 levels for arrow fn parameter, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn construct_print() {
let content = r#"<?php
print "hello";
"#;
let results = selection_ranges(content, &[Position::new(1, 7)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 3,
"Expected at least 3 levels for print, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn for_loop_colon_delimited() {
let content = r#"<?php
for ($i = 0; $i < 10; $i++):
echo $i;
endfor;
"#;
let results = selection_ranges(content, &[Position::new(2, 10)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 3,
"Expected at least 3 levels for colon-delimited for, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn while_loop_colon_delimited() {
let content = r#"<?php
while (true):
echo "loop";
endwhile;
"#;
let results = selection_ranges(content, &[Position::new(2, 10)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 3,
"Expected at least 3 levels for colon-delimited while, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn foreach_colon_delimited() {
let content = r#"<?php
foreach ([1, 2] as $v):
echo $v;
endforeach;
"#;
let results = selection_ranges(content, &[Position::new(2, 10)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 3,
"Expected at least 3 levels for colon-delimited foreach, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn switch_colon_delimited() {
let content = r#"<?php
switch ($x):
case 1:
echo "one";
break;
endswitch;
"#;
let results = selection_ranges(content, &[Position::new(3, 14)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 4,
"Expected at least 4 levels for colon-delimited switch, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn declare_colon_delimited() {
let content = r#"<?php
declare(strict_types=1):
echo "hello";
enddeclare;
"#;
let results = selection_ranges(content, &[Position::new(2, 10)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 3,
"Expected at least 3 levels for colon-delimited declare, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn block_statement() {
let content = r#"<?php
{
echo "inside";
}
"#;
let results = selection_ranges(content, &[Position::new(2, 10)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 3,
"Expected at least 3 levels for block statement, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn enum_unit_case() {
let content = r#"<?php
enum Suit {
case Hearts;
case Diamonds;
}
"#;
let results = selection_ranges(content, &[Position::new(2, 10)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 3,
"Expected at least 3 levels for unit enum case, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn abstract_property() {
let content = r#"<?php
class Foo {
public int $x;
}
"#;
let results = selection_ranges(content, &[Position::new(2, 16)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 3,
"Expected at least 3 levels for abstract property, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn ternary_short_form() {
let content = r#"<?php
$x = $a ?: $b;
"#;
let results = selection_ranges(content, &[Position::new(1, 12)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 4,
"Expected at least 4 levels for short ternary, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn instantiation_without_args() {
let content = r#"<?php
$x = new Foo;
"#;
let results = selection_ranges(content, &[Position::new(1, 10)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 4,
"Expected at least 4 levels for new without args, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn legacy_array_value_element() {
let content = r#"<?php
$x = array(1, 2, 3);
"#;
let results = selection_ranges(content, &[Position::new(1, 15)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 4,
"Expected at least 4 levels for legacy array value element, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn enum_with_method() {
let content = r#"<?php
enum Color: string {
case Red = 'red';
public function label(): string {
return "Color";
}
}
"#;
let results = selection_ranges(content, &[Position::new(5, 16)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 5,
"Expected at least 5 levels for enum method body, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn for_loop_initializer() {
let content = r#"<?php
for ($i = 0; $i < 10; $i++) {
echo $i;
}
"#;
let results = selection_ranges(content, &[Position::new(1, 6)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 3,
"Expected at least 3 levels for for initializer, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn for_loop_condition() {
let content = r#"<?php
for ($i = 0; $i < 10; $i++) {
echo $i;
}
"#;
let results = selection_ranges(content, &[Position::new(1, 19)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 3,
"Expected at least 3 levels for for condition, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn for_loop_increment() {
let content = r#"<?php
for ($i = 0; $i < 10; $i++) {
echo $i;
}
"#;
let results = selection_ranges(content, &[Position::new(1, 23)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 3,
"Expected at least 3 levels for for increment, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn while_condition() {
let content = r#"<?php
while ($x > 0) {
$x--;
}
"#;
let results = selection_ranges(content, &[Position::new(1, 8)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 3,
"Expected at least 3 levels for while condition, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn do_while_condition() {
let content = r#"<?php
do {
$x--;
} while ($x > 0);
"#;
let results = selection_ranges(content, &[Position::new(3, 10)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 3,
"Expected at least 3 levels for do-while condition, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn foreach_source_expression() {
let content = r#"<?php
foreach ($items as $item) {
echo $item;
}
"#;
let results = selection_ranges(content, &[Position::new(1, 10)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 3,
"Expected at least 3 levels for foreach source expression, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn switch_expression() {
let content = r#"<?php
switch ($x) {
case 1:
break;
}
"#;
let results = selection_ranges(content, &[Position::new(1, 9)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 3,
"Expected at least 3 levels for switch expression, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn match_subject_expression() {
let content = r#"<?php
$r = match($x) {
default => 1,
};
"#;
let results = selection_ranges(content, &[Position::new(1, 12)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 4,
"Expected at least 4 levels for match subject, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn if_condition() {
let content = r#"<?php
if ($x > 0) {
echo "positive";
}
"#;
let results = selection_ranges(content, &[Position::new(1, 5)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 3,
"Expected at least 3 levels for if condition, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn construct_exit_with_args() {
let content = r#"<?php
exit(1);
"#;
let results = selection_ranges(content, &[Position::new(1, 5)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 3,
"Expected at least 3 levels for exit with args, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn construct_die_with_args() {
let content = r#"<?php
die("error");
"#;
let results = selection_ranges(content, &[Position::new(1, 5)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 3,
"Expected at least 3 levels for die with args, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn trait_use_member() {
let content = r#"<?php
trait Greeter {
public function greet(): string {
return "hello";
}
}
class Foo {
use Greeter;
public function bar(): void {}
}
"#;
let results = selection_ranges(content, &[Position::new(8, 8)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 3,
"Expected at least 3 levels for trait use member, got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn hooked_property_abstract() {
let content = r#"<?php
class Foo {
public string $name {
get => $this->name;
}
}
"#;
let results = selection_ranges(content, &[Position::new(2, 19)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 3,
"Expected at least 3 levels for hooked property (abstract), got {}",
ranges.len()
);
assert_nested(&ranges);
}
#[test]
fn hooked_property_concrete() {
let content = r#"<?php
class Foo {
public string $name = "default" {
get => $this->name;
}
}
"#;
let results = selection_ranges(content, &[Position::new(2, 27)]);
assert_eq!(results.len(), 1);
let ranges = flatten(&results[0]);
assert!(
ranges.len() >= 4,
"Expected at least 4 levels for hooked property (concrete), got {}",
ranges.len()
);
assert_nested(&ranges);
}
}