use rowan::TextRange;
use crate::linter::diagnostic::{Diagnostic, Fix, Severity, 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",
}];
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.\n\nThe rule fires only when the \
terminator is a direct statement of the block (a `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)| terminator_name(s, ctx, block).map(|n| (i, n)))
else {
return;
};
if idx + 1 >= stmts.len() {
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: Severity::Warning,
path: Default::default(),
range: region,
message: ViolationData::new(
"unreachable-code",
format!("code after `{term}()` can never be reached"),
)
.with_suggestion("Remove the unreachable code, or fix the control flow."),
fix,
});
}
}
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)
}