use perl_diagnostics_codes::DiagnosticCode;
use perl_lsp_diagnostic_types::{Diagnostic, DiagnosticSeverity, DiagnosticTag};
use perl_parser_core::ast::{Node, NodeKind};
pub fn check_unreachable_code(root: &Node, diagnostics: &mut Vec<Diagnostic>) {
visit_node(root, diagnostics);
}
fn visit_node(node: &Node, diagnostics: &mut Vec<Diagnostic>) {
match &node.kind {
NodeKind::Program { statements } => {
check_statement_list(statements, diagnostics);
}
NodeKind::Subroutine { body, .. } | NodeKind::Method { body, .. } => {
visit_node(body, diagnostics);
}
NodeKind::Block { statements } => {
check_statement_list(statements, diagnostics);
}
NodeKind::If { then_branch, elsif_branches, else_branch, .. } => {
visit_node(then_branch, diagnostics);
for (_, branch_body) in elsif_branches {
visit_node(branch_body, diagnostics);
}
if let Some(else_body) = else_branch {
visit_node(else_body, diagnostics);
}
}
NodeKind::While { body, .. }
| NodeKind::For { body, .. }
| NodeKind::Foreach { body, .. } => {
visit_node(body, diagnostics);
}
NodeKind::Given { body, .. } | NodeKind::When { body, .. } | NodeKind::Default { body } => {
visit_node(body, diagnostics);
}
NodeKind::PhaseBlock { block, .. } => {
visit_node(block, diagnostics);
}
NodeKind::Class { body, .. } => {
visit_node(body, diagnostics);
}
NodeKind::Do { block } => {
visit_node(block, diagnostics);
}
NodeKind::Try { body, catch_blocks, finally_block } => {
visit_node(body, diagnostics);
for (_, catch_body) in catch_blocks {
visit_node(catch_body, diagnostics);
}
if let Some(finally) = finally_block {
visit_node(finally, diagnostics);
}
}
NodeKind::ExpressionStatement { expression } => {
visit_expr(expression, diagnostics);
}
NodeKind::VariableDeclaration { initializer: Some(init), .. }
| NodeKind::VariableListDeclaration { initializer: Some(init), .. } => {
visit_expr(init, diagnostics);
}
NodeKind::Eval { .. } => {}
NodeKind::LabeledStatement { statement, .. } => {
visit_node(statement, diagnostics);
}
_ => {}
}
}
fn visit_expr(expr: &Node, diagnostics: &mut Vec<Diagnostic>) {
match &expr.kind {
NodeKind::Subroutine { body, .. } => {
visit_node(body, diagnostics);
}
NodeKind::Assignment { lhs, rhs, .. } => {
visit_expr(lhs, diagnostics);
visit_expr(rhs, diagnostics);
}
NodeKind::Binary { left, right, .. } => {
visit_expr(left, diagnostics);
visit_expr(right, diagnostics);
}
NodeKind::Unary { operand, .. } => {
visit_expr(operand, diagnostics);
}
NodeKind::Ternary { condition, then_expr, else_expr } => {
visit_expr(condition, diagnostics);
visit_expr(then_expr, diagnostics);
visit_expr(else_expr, diagnostics);
}
NodeKind::FunctionCall { args, .. } | NodeKind::MethodCall { args, .. } => {
for arg in args {
visit_expr(arg, diagnostics);
}
}
NodeKind::ArrayLiteral { elements } => {
for elem in elements {
visit_expr(elem, diagnostics);
}
}
NodeKind::HashLiteral { pairs } => {
for (key, val) in pairs {
visit_expr(key, diagnostics);
visit_expr(val, diagnostics);
}
}
_ => {}
}
}
fn check_statement_list(stmts: &[Node], diagnostics: &mut Vec<Diagnostic>) {
let mut found_exit = false;
for stmt in stmts {
if found_exit {
diagnostics.push(Diagnostic {
range: (stmt.location.start, stmt.location.end),
severity: DiagnosticSeverity::Hint,
code: Some(DiagnosticCode::UnreachableCode.as_str().to_string()),
message: "Unreachable code: this statement cannot be executed".to_string(),
related_information: vec![],
tags: vec![DiagnosticTag::Unnecessary],
suggestion: Some("Remove unreachable code".to_string()),
});
visit_node(stmt, diagnostics);
} else {
visit_node(stmt, diagnostics);
if is_unconditional_exit(stmt) {
found_exit = true;
}
}
}
}
fn is_unconditional_exit(node: &Node) -> bool {
match &node.kind {
NodeKind::Return { .. } => true,
NodeKind::FunctionCall { name, .. } => is_exit_function(name),
NodeKind::ExpressionStatement { expression } => is_unconditional_exit(expression),
NodeKind::LoopControl { op, .. } => matches!(op.as_str(), "last" | "next" | "redo"),
NodeKind::StatementModifier { .. } => false,
_ => false,
}
}
fn is_exit_function(name: &str) -> bool {
matches!(name, "die" | "exit" | "croak" | "Carp::croak" | "confess" | "Carp::confess")
}