use crate::ast::{AstNode, AstToken, CallExpr, Expr, Name};
use crate::linter::diagnostic::Diagnostic;
use crate::linter::rules::{Example, Rule, RuleContext};
use crate::semantic::BindingKind;
use crate::syntax::{SyntaxKind, SyntaxNode};
pub struct UnusedArgument;
impl Rule for UnusedArgument {
fn id(&self) -> &'static str {
"unused-argument"
}
fn default_enabled(&self) -> bool {
false
}
fn description(&self) -> &'static str {
"Flag a function parameter that is never read in its body. Every \
signature form is covered — long, short (`f(x) = ...`), anonymous, and \
`do`. All-underscore names (`_`, `__`) follow Julia's throwaway \
convention and are skipped, and stub methods whose body is a single \
placeholder expression — a literal (`f(x) = 0`), `nothing`, or an \
`error(...)`/`throw(...)` call — are exempt. Because methods that \
dispatch on an argument's type without reading its value are common, \
this rule is disabled by default; enable it with \
`--select unused-argument`."
}
fn examples(&self) -> &'static [Example] {
&[Example {
caption: "`factor` is accepted but never used:",
source: "function scale(x, factor)\n 2 * 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::Param | BindingKind::KeywordParam) {
continue;
}
if binding.name.chars().all(|c| c == '_') {
continue;
}
if enclosing_body_is_stub(ctx.root, binding.def_range) {
continue;
}
sink.push(Diagnostic::new(
self.id(),
binding.def_range,
format!("function argument `{}` is never used", binding.name),
));
}
}
}
fn enclosing_body_is_stub(root: &SyntaxNode, def_range: rowan::TextRange) -> bool {
let node = match root.covering_element(def_range) {
rowan::NodeOrToken::Node(node) => node,
rowan::NodeOrToken::Token(token) => match token.parent() {
Some(parent) => parent,
None => return false,
},
};
let Some(func) = node.ancestors().find(|n| {
matches!(
n.kind(),
SyntaxKind::FUNCTION_DEF
| SyntaxKind::ASSIGNMENT_EXPR
| SyntaxKind::ARROW_EXPR
| SyntaxKind::DO_EXPR
)
}) else {
return false;
};
match sole_body_expr(&func) {
Some(expr) => is_stub_expr(&expr),
None => false,
}
}
fn sole_body_expr(func: &SyntaxNode) -> Option<SyntaxNode> {
let mut body = match func.kind() {
SyntaxKind::FUNCTION_DEF | SyntaxKind::DO_EXPR => func
.children()
.find(|c| c.kind() == SyntaxKind::BLOCK)?
.children(),
SyntaxKind::ASSIGNMENT_EXPR | SyntaxKind::ARROW_EXPR => {
let mut children = func.children();
children.next(); children
}
_ => return None,
};
match (body.next(), body.next()) {
(Some(only), None) => Some(only),
_ => None,
}
}
fn is_stub_expr(expr: &SyntaxNode) -> bool {
match expr.kind() {
SyntaxKind::LITERAL | SyntaxKind::STRING_LITERAL | SyntaxKind::CMD_LITERAL => true,
SyntaxKind::NAME => Name::cast(expr.clone()).is_some_and(|n| name_is(&n, &["nothing"])),
SyntaxKind::CALL_EXPR => CallExpr::cast(expr.clone())
.and_then(|call| call.callee())
.is_some_and(
|callee| matches!(callee, Expr::Name(n) if name_is(&n, &["error", "throw"])),
),
_ => false,
}
}
fn name_is(name: &Name, names: &[&str]) -> bool {
name.ident().is_some_and(|id| names.contains(&id.text()))
}