use rowan::TextRange;
use rowan::ast::AstNode as _;
use crate::ast::CallExpr;
use crate::linter::diagnostic::{Diagnostic, Fix, ViolationData};
use crate::linter::rules::{Example, Rule, RuleContext, matchers};
use crate::syntax::{SyntaxElement, SyntaxKind, SyntaxNode};
pub struct UnreachableCode;
const EXAMPLES: &[Example] = &[
Example {
caption: "A statement after `return()` can never run:",
source: "f <- function() {\n return(1)\n 2\n}\n",
},
Example {
caption: "An `if`/`else` that exits in both branches leaves its tail dead:",
source: "f <- function() {\n if (x) return(1) else return(2)\n 3\n}\n",
},
];
impl Rule for UnreachableCode {
fn id(&self) -> &'static str {
"unreachable-code"
}
fn description(&self) -> &'static str {
"Flag statements that follow an unconditional `return()` or `stop()` in a \
block—once either runs, nothing after it in the same block can be \
reached, so the trailing code is dead. A direct-statement `if`/`else` \
that exits in both branches likewise leaves its tail unreachable (a \
control-flow-graph verdict).\n\nThe rule fires only when the terminator \
is a direct statement of the block (a lone `return()`/`stop()` guarded \
by an `if` leaves the tail reachable) and only when the callee resolves \
to base R; a local redefinition is left alone. `return` is additionally \
required to sit inside a function. The deletion fix is unsafe, and \
withheld when it would drop a comment."
}
fn examples(&self) -> &'static [Example] {
EXAMPLES
}
fn interests(&self) -> &'static [SyntaxKind] {
&[SyntaxKind::BLOCK_EXPR]
}
fn check(&self, el: &SyntaxElement, ctx: &RuleContext<'_>, sink: &mut Vec<Diagnostic>) {
let Some(block) = el.as_node() else {
return;
};
let stmts: Vec<SyntaxElement> = block
.children_with_tokens()
.filter(|e| {
!matches!(
e.kind(),
SyntaxKind::LBRACE
| SyntaxKind::RBRACE
| SyntaxKind::SEMICOLON
| SyntaxKind::WHITESPACE
| SyntaxKind::NEWLINE
| SyntaxKind::COMMENT
)
})
.collect();
let Some((idx, term)) = stmts.iter().enumerate().find_map(|(i, s)| {
if i + 1 >= stmts.len() {
return None;
}
if let Some(name) = terminator_name(s, ctx, block) {
return Some((i, Terminator::Call(name)));
}
both_branches_diverge(s, &stmts[i + 1], ctx, block).then_some((i, Terminator::BothArms))
}) else {
return;
};
let first = &stmts[idx + 1];
let last = stmts.last().expect("at least one statement follows");
let region = TextRange::new(first.text_range().start(), last.text_range().end());
let drops_comment = block
.descendants_with_tokens()
.any(|e| e.kind() == SyntaxKind::COMMENT && region.contains_range(e.text_range()));
let fix = (!drops_comment).then(|| {
let src = ctx.root.text().to_string();
let (start, end) = matchers::deletion_span(&src, region);
Fix::unsafe_(start, end, "", "Remove the unreachable code")
});
sink.push(Diagnostic {
rule: "unreachable-code",
severity: Default::default(),
path: Default::default(),
range: region,
message: ViolationData::new("unreachable-code", term.message())
.with_suggestion("Remove the unreachable code, or fix the control flow."),
fix,
});
}
}
enum Terminator {
Call(&'static str),
BothArms,
}
impl Terminator {
fn message(&self) -> String {
match self {
Terminator::Call(name) => format!("code after `{name}()` can never be reached"),
Terminator::BothArms => {
"code after this `if` can never be reached (both branches exit)".to_string()
}
}
}
}
fn both_branches_diverge(
stmt: &SyntaxElement,
next: &SyntaxElement,
ctx: &RuleContext<'_>,
block: &SyntaxNode,
) -> bool {
let Some(node) = stmt.as_node() else {
return false;
};
if node.kind() != SyntaxKind::IF_EXPR {
return false;
}
if !ctx.cfg.is_unreachable(next.text_range()) {
return false;
}
let mut saw_return = false;
for call in node.descendants().filter_map(CallExpr::cast) {
if let Some(name @ ("return" | "stop")) = call.callee_name().as_deref() {
if !ctx.resolves_to_base(&call) {
return false;
}
saw_return |= name == "return";
}
}
!saw_return || in_function(block)
}
fn terminator_name(
stmt: &SyntaxElement,
ctx: &RuleContext<'_>,
block: &SyntaxNode,
) -> Option<&'static str> {
let node = stmt.as_node()?;
for name in ["return", "stop"] {
if let Some(call) = matchers::call_named(node, name) {
if !ctx.resolves_to_base(&call) {
return None;
}
if name == "return" && !in_function(block) {
return None;
}
return Some(name);
}
}
None
}
fn in_function(block: &SyntaxNode) -> bool {
block
.ancestors()
.any(|n| n.kind() == SyntaxKind::FUNCTION_EXPR)
}