use rowan::TextRange;
use crate::linter::diagnostic::Diagnostic;
use crate::linter::rules::{Example, Rule, RuleContext};
use crate::semantic::BindingKind;
use crate::syntax::{SyntaxKind, SyntaxNode};
pub struct UnusedBinding;
const ATTRIBUTE_DSL_MACROS: &[&str] = &["gen_defaults!", "DocumentedAttributes", "recipe"];
fn in_attribute_dsl_macro(root: &SyntaxNode, range: TextRange) -> bool {
let mut node = match root.covering_element(range) {
rowan::NodeOrToken::Token(t) => t.parent(),
rowan::NodeOrToken::Node(n) => Some(n),
};
while let Some(current) = node {
if current.kind() == SyntaxKind::MACRO_CALL
&& macro_call_name(¤t)
.is_some_and(|name| ATTRIBUTE_DSL_MACROS.contains(&name.as_str()))
{
return true;
}
node = current.parent();
}
false
}
fn is_direct_macro_argument(root: &SyntaxNode, range: TextRange) -> bool {
let mut node = match root.covering_element(range) {
rowan::NodeOrToken::Token(t) => t.parent(),
rowan::NodeOrToken::Node(n) => Some(n),
};
while let Some(current) = node {
match current.kind() {
SyntaxKind::BLOCK => return false,
SyntaxKind::MACRO_CALL => return true,
_ => node = current.parent(),
}
}
false
}
fn macro_call_name(call: &SyntaxNode) -> Option<String> {
let name = call
.children()
.find(|c| c.kind() == SyntaxKind::MACRO_NAME)?;
name.descendants_with_tokens()
.filter_map(|e| e.into_token())
.filter(|t| t.kind() == SyntaxKind::IDENT)
.last()
.map(|t| t.text().to_string())
}
impl Rule for UnusedBinding {
fn id(&self) -> &'static str {
"unused-binding"
}
fn description(&self) -> &'static str {
"Flag a local variable that is assigned but never read in the same \
scope. Parameters, loop and `catch` variables, struct fields, type \
parameters, and top-level definitions are exempt, since those are \
meaningful even when unread. Names beginning with `_` are skipped, \
following Julia's throwaway convention."
}
fn examples(&self) -> &'static [Example] {
&[Example {
caption: "`tmp` is assigned inside `f` but never used:",
source: "function f(x)\n tmp = x + 1\n return x\nend\n",
}]
}
fn check_file(&self, ctx: &RuleContext<'_>, sink: &mut Vec<Diagnostic>) {
for binding in ctx.model.bindings() {
if binding.read {
continue;
}
if !matches!(binding.kind, BindingKind::Local | BindingKind::LetVar) {
continue;
}
if binding.name.starts_with('_') {
continue;
}
if in_attribute_dsl_macro(ctx.root, binding.def_range) {
continue;
}
if is_direct_macro_argument(ctx.root, binding.def_range) {
continue;
}
sink.push(Diagnostic::new(
self.id(),
binding.def_range,
format!(
"local variable `{}` is assigned but never used",
binding.name
),
));
}
}
}