use rowan::TextRange;
use rowan::ast::AstNode as _;
use crate::ast::{AstToken as _, CallExpr};
use crate::linter::diagnostic::Diagnostic;
use crate::linter::rules::{Example, Rule, RuleContext};
use crate::semantic::ControlFlowGraph;
use crate::syntax::{SyntaxKind, SyntaxNode};
pub struct UnreachableCode;
const TERMINATOR_NAMES: [&str; 3] = ["throw", "error", "rethrow"];
fn region_body(owner: &SyntaxNode) -> Option<SyntaxNode> {
if owner.kind() == SyntaxKind::ROOT {
return Some(owner.clone());
}
owner.children().find(|c| c.kind() == SyntaxKind::BLOCK)
}
fn terminators_confirmed(ctx: &RuleContext<'_>, body: &SyntaxNode) -> bool {
let mut stack = vec![body.clone()];
while let Some(node) = stack.pop() {
for child in node.children() {
if is_region_owner(child.kind()) {
continue;
}
if child.kind() == SyntaxKind::CALL_EXPR
&& let Some(call) = CallExpr::cast(child.clone())
&& call
.callee_ident()
.is_some_and(|name| TERMINATOR_NAMES.contains(&name.text()))
&& !ctx.resolves_to_base(&call)
{
return false;
}
stack.push(child);
}
}
true
}
fn is_region_owner(kind: SyntaxKind) -> bool {
matches!(
kind,
SyntaxKind::FUNCTION_DEF
| SyntaxKind::MACRO_DEF
| SyntaxKind::DO_EXPR
| SyntaxKind::MODULE_DEF
)
}
fn dead_heads(graph: &ControlFlowGraph) -> impl Iterator<Item = TextRange> {
graph
.iter()
.filter(|(id, _)| !graph.is_reachable(*id))
.filter_map(|(_, block)| block.stmts.first().copied())
}
impl Rule for UnreachableCode {
fn id(&self) -> &'static str {
"unreachable-code"
}
fn description(&self) -> &'static str {
"Flag a statement no path of execution can reach: the tail after an \
unconditional `return`, `throw`, `error`, or `rethrow`, after an \
`if`/`else` that diverges in every arm, or after a `while true` with \
no `break`. The code runs, but the flagged statement never does, so \
it is either dead weight or a sign that the divergence above it is \
misplaced. Reachability comes from the file's control-flow graph, so \
a `for` that may run zero times, an `if` with no `else`, a `catch` \
clause, and a conditional `a && return` all keep their tails live. \
No fix is offered: deleting the statement is a judgment call, and \
keeping it may be the point when the divergence is the bug."
}
fn examples(&self) -> &'static [Example] {
&[
Example {
caption: "Nothing after an unconditional `return` can run:",
source: "function f(x)\n return x + 1\n println(\"never\")\nend\n",
},
Example {
caption: "Both arms diverge, so the tail is dead too:",
source: "function classify(x)\n if x > 0\n return :pos\n else\n throw(DomainError(x))\n end\n return :unknown\nend\n",
},
]
}
fn check_file(&self, ctx: &RuleContext<'_>, sink: &mut Vec<Diagnostic>) {
let cfg = ctx.control_flow();
let regions = std::iter::once((ctx.root.clone(), cfg.toplevel())).chain(
cfg.regions()
.iter()
.map(|(ptr, graph)| (ptr.to_node(ctx.root), graph)),
);
for (owner, graph) in regions {
let Some(body) = region_body(&owner) else {
continue;
};
if !terminators_confirmed(ctx, &body) {
continue;
}
for range in dead_heads(graph) {
sink.push(Diagnostic::new(
self.id(),
range,
"unreachable code: no path of execution reaches this statement".to_string(),
));
}
}
}
}