use rowan::TextRange;
use crate::linter::diagnostic::{Diagnostic, Fix, Severity, ViolationData};
use crate::linter::rules::{Example, Rule, RuleContext, matchers};
use crate::semantic::ScopeKind;
use crate::syntax::{SyntaxKind, SyntaxNode};
pub struct UnusedBinding;
impl Rule for UnusedBinding {
fn id(&self) -> &'static str {
"unused-binding"
}
fn description(&self) -> &'static str {
"Flag a local binding that is never read in the same file. Function \
parameters, `for`-loop variables, and names beginning with `.` are \
exempt, since those are meaningful even when unused."
}
fn examples(&self) -> &'static [Example] {
&[Example {
caption: "`x` is assigned but never used:",
source: "x <- 1\ny <- 2\nprint(y)\n",
}]
}
fn default_severity(&self) -> Severity {
Severity::Warning
}
fn check_file(&self, ctx: &RuleContext<'_>, sink: &mut Vec<Diagnostic>) {
let src = ctx.root.text().to_string();
sink.extend(
ctx.model
.unused_local_bindings()
.filter(|id| {
let b = ctx.model.binding(*id);
let top_level = ctx.model.scope(b.scope).kind == ScopeKind::File;
!(top_level && ctx.project.is_some_and(|p| p.used_elsewhere(&b.name)))
})
.map(|id| {
let b = ctx.model.binding(id);
let fix = deletion_fix(ctx.root, &src, &b.name, b.def_range);
Diagnostic {
rule: "unused-binding",
severity: Severity::Warning,
path: Default::default(),
range: b.def_range,
message: ViolationData::new(
"unused-binding",
format!("local binding `{}` is assigned but never read", b.name),
)
.with_suggestion("Remove the assignment, or prefix the name with `.` to mark it intentional."),
fix,
}
}),
);
}
}
fn deletion_fix(root: &SyntaxNode, src: &str, name: &str, def_range: TextRange) -> Option<Fix> {
let token = root.covering_element(def_range).into_token()?;
let assign = token.parent()?;
if assign.kind() != SyntaxKind::ASSIGNMENT_EXPR {
return None;
}
let parent = assign.parent()?;
if !matches!(parent.kind(), SyntaxKind::ROOT | SyntaxKind::BLOCK_EXPR) {
return None;
}
let lhs = assign
.children_with_tokens()
.find_map(|e| e.into_token().filter(|t| t.kind() == SyntaxKind::IDENT))?;
if lhs.text_range() != def_range {
return None;
}
if parent.kind() == SyntaxKind::BLOCK_EXPR {
let remaining = block_statement_count(&parent).saturating_sub(1);
let is_function_body = parent.parent().map(|g| g.kind()) == Some(SyntaxKind::FUNCTION_EXPR);
if remaining == 0 || (remaining == 1 && is_function_body) {
return None;
}
}
let (start, end) = matchers::deletion_span(src, assign.text_range());
Some(Fix::unsafe_(
start,
end,
"",
format!("Remove unused binding `{name}`"),
))
}
fn block_statement_count(block: &SyntaxNode) -> usize {
block
.children_with_tokens()
.filter(|el| {
!matches!(
el.kind(),
SyntaxKind::LBRACE
| SyntaxKind::RBRACE
| SyntaxKind::WHITESPACE
| SyntaxKind::NEWLINE
| SyntaxKind::COMMENT
)
})
.count()
}