use rowan::TextRange;
use crate::linter::diagnostic::{Diagnostic, Fix, Severity, ViolationData};
use crate::linter::rules::{Rule, RuleContext};
use crate::semantic::ScopeKind;
use crate::syntax::{SyntaxKind, SyntaxNode};
pub struct UnusedBinding;
impl Rule for UnusedBinding {
fn id(&self) -> &'static str {
"unused-binding"
}
fn default_severity(&self) -> Severity {
Severity::Warning
}
fn run(&self, ctx: &RuleContext<'_>) -> Vec<Diagnostic> {
let src = ctx.root.text().to_string();
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,
}
})
.collect()
}
}
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) = 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()
}
fn deletion_span(src: &str, range: TextRange) -> (usize, usize) {
let bytes = src.as_bytes();
let mut start = usize::from(range.start());
while start > 0 && matches!(bytes[start - 1], b' ' | b'\t') {
start -= 1;
}
let mut end = usize::from(range.end());
while end < bytes.len() && matches!(bytes[end], b' ' | b'\t') {
end += 1;
}
end = consume_newline(bytes, end);
loop {
let mut probe = end;
while probe < bytes.len() && matches!(bytes[probe], b' ' | b'\t') {
probe += 1;
}
let after_nl = consume_newline(bytes, probe);
if after_nl == probe {
break; }
end = after_nl;
}
if end == bytes.len() {
let mut prev = start;
while prev > 0 && matches!(bytes[prev - 1], b' ' | b'\t' | b'\n' | b'\r') {
prev -= 1;
}
start = if prev > 0 {
consume_newline(bytes, prev)
} else {
0
};
}
(start, end)
}
fn consume_newline(bytes: &[u8], i: usize) -> usize {
match bytes.get(i) {
Some(b'\n') => i + 1,
Some(b'\r') if bytes.get(i + 1) == Some(&b'\n') => i + 2,
_ => i,
}
}