use std::collections::HashMap;
use php_ast::{ClassMemberKind, Expr, ExprKind, NamespaceBody, Span, Stmt, StmtKind, SwitchStmt};
use tower_lsp_server::ls_types::{
CodeAction, CodeActionKind, CodeActionOrCommand, Range, TextEdit, Uri, WorkspaceEdit,
};
use crate::document::ast::{ParsedDoc, SourceView};
pub fn switch_to_match_actions(
source: &str,
doc: &ParsedDoc,
range: Range,
uri: &Uri,
) -> Vec<CodeActionOrCommand> {
let sv = doc.view();
let cursor = sv.byte_of_position(range.start);
let mut out = Vec::new();
collect_in_stmts(&doc.program().stmts, source, cursor, uri, sv, &mut out);
out
}
fn collect_in_stmts(
stmts: &[Stmt<'_, '_>],
source: &str,
cursor: u32,
uri: &Uri,
sv: SourceView<'_>,
out: &mut Vec<CodeActionOrCommand>,
) -> bool {
for stmt in stmts {
if stmt.span.end < cursor || stmt.span.start > cursor {
continue;
}
if collect_in_stmt(stmt, source, cursor, uri, sv, out) {
return true;
}
}
false
}
fn collect_in_stmt(
stmt: &Stmt<'_, '_>,
source: &str,
cursor: u32,
uri: &Uri,
sv: SourceView<'_>,
out: &mut Vec<CodeActionOrCommand>,
) -> bool {
match &stmt.kind {
StmtKind::Switch(sw) => {
for case in sw.body.cases.iter() {
if collect_in_stmts(&case.body, source, cursor, uri, sv, out) {
return true;
}
}
if let Some(action) = build_action(sw, stmt.span, source, uri, sv) {
out.push(action);
}
true
}
StmtKind::Function(f) => collect_in_stmts(&f.body.stmts, source, cursor, uri, sv, out),
StmtKind::Class(c) => {
for member in c.body.members.iter() {
if let ClassMemberKind::Method(m) = &member.kind
&& let Some(body) = &m.body
&& collect_in_stmts(&body.stmts, source, cursor, uri, sv, out)
{
return true;
}
}
false
}
StmtKind::Namespace(ns) => {
if let NamespaceBody::Braced(inner) = &ns.body {
collect_in_stmts(&inner.stmts, source, cursor, uri, sv, out)
} else {
false
}
}
StmtKind::Block(b) => collect_in_stmts(&b.stmts, source, cursor, uri, sv, out),
StmtKind::If(i) => {
if collect_in_stmt(i.then_branch, source, cursor, uri, sv, out) {
return true;
}
for ei in i.elseif_branches.iter() {
if collect_in_stmt(&ei.body, source, cursor, uri, sv, out) {
return true;
}
}
if let Some(e) = &i.else_branch {
collect_in_stmt(e, source, cursor, uri, sv, out)
} else {
false
}
}
StmtKind::While(w) => collect_in_stmt(w.body, source, cursor, uri, sv, out),
StmtKind::DoWhile(d) => collect_in_stmt(d.body, source, cursor, uri, sv, out),
StmtKind::For(f) => collect_in_stmt(f.body, source, cursor, uri, sv, out),
StmtKind::Foreach(f) => collect_in_stmt(f.body, source, cursor, uri, sv, out),
StmtKind::TryCatch(t) => {
if collect_in_stmts(&t.body.stmts, source, cursor, uri, sv, out) {
return true;
}
for catch in t.catches.iter() {
if collect_in_stmts(&catch.body.stmts, source, cursor, uri, sv, out) {
return true;
}
}
if let Some(finally) = &t.finally {
collect_in_stmts(&finally.stmts, source, cursor, uri, sv, out)
} else {
false
}
}
_ => false,
}
}
fn build_action(
sw: &SwitchStmt<'_, '_>,
span: Span,
source: &str,
uri: &Uri,
sv: SourceView<'_>,
) -> Option<CodeActionOrCommand> {
let new_text = build_match_text(sw, span, source)?;
let edit_range = Range {
start: sv.position_of(span.start),
end: sv.position_of(span.end),
};
let mut changes = HashMap::new();
changes.insert(
uri.clone(),
vec![TextEdit {
range: edit_range,
new_text,
}],
);
Some(CodeActionOrCommand::CodeAction(CodeAction {
title: "Convert switch to match".to_string(),
kind: Some(CodeActionKind::REFACTOR),
edit: Some(WorkspaceEdit {
changes: Some(changes),
..Default::default()
}),
..Default::default()
}))
}
struct MatchArm {
keys: Vec<String>,
value: String,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum CaseValueKind {
Int,
Float,
String,
Bool,
Null,
}
fn case_value_kind(expr: &Expr<'_, '_>) -> Option<CaseValueKind> {
match &expr.kind {
ExprKind::Int(_) => Some(CaseValueKind::Int),
ExprKind::Float(_) => Some(CaseValueKind::Float),
ExprKind::String(_) => Some(CaseValueKind::String),
ExprKind::Bool(_) => Some(CaseValueKind::Bool),
ExprKind::Null => Some(CaseValueKind::Null),
_ => None,
}
}
fn build_match_text(sw: &SwitchStmt<'_, '_>, span: Span, source: &str) -> Option<String> {
if sw.uses_alternative {
return None;
}
if !sw.body.cases.iter().any(|c| c.value.is_none()) {
return None;
}
let mut case_kind: Option<CaseValueKind> = None;
for case in sw.body.cases.iter() {
let Some(val_expr) = &case.value else {
continue;
};
if let Some(kind) = case_value_kind(val_expr) {
match case_kind {
None => case_kind = Some(kind),
Some(prev) if prev != kind => return None,
_ => {}
}
}
}
let mut arms: Vec<MatchArm> = Vec::new();
let mut pending_keys: Vec<String> = Vec::new();
for case in sw.body.cases.iter() {
let key_text = match &case.value {
Some(val_expr) => {
source[val_expr.span.start as usize..val_expr.span.end as usize].to_string()
}
None => "default".to_string(),
};
if case
.body
.iter()
.any(|s| matches!(s.kind, StmtKind::Break(Some(_))))
{
return None;
}
let non_break: Vec<_> = case
.body
.iter()
.filter(|s| !matches!(s.kind, StmtKind::Break(None)))
.collect();
if non_break.is_empty() {
if case.body.is_empty() {
pending_keys.push(key_text);
continue;
}
return None;
}
if non_break.len() != 1 {
return None;
}
let StmtKind::Return(Some(ret_expr)) = &non_break[0].kind else {
return None;
};
let value_text =
source[ret_expr.span.start as usize..ret_expr.span.end as usize].to_string();
pending_keys.push(key_text);
arms.push(MatchArm {
keys: std::mem::take(&mut pending_keys),
value: value_text,
});
}
if !pending_keys.is_empty() {
return None;
}
if arms.is_empty() {
return None;
}
let base_indent = line_indent(source, span.start as usize);
let arm_indent = if let Some(first_case) = sw.body.cases.first() {
line_indent(source, first_case.span.start as usize)
} else {
format!("{} ", base_indent)
};
let cond_text = &source[sw.expr.span.start as usize..sw.expr.span.end as usize];
let mut result = format!("return match ({cond_text}) {{\n");
for arm in &arms {
let keys_joined = arm.keys.join(", ");
result.push_str(&format!("{arm_indent}{keys_joined} => {},\n", arm.value));
}
result.push_str(&format!("{base_indent}}};"));
Some(result)
}
fn line_indent(source: &str, pos: usize) -> String {
let line_start = source[..pos].rfind('\n').map_or(0, |i| i + 1);
source[line_start..pos]
.chars()
.take_while(|c| c.is_whitespace())
.collect()
}