use crate::rules::{Issue, Location, Rule, RuleContext, Severity};
use syn::visit::Visit;
use syn::{Expr, ExprCall, ExprMethodCall, Item, ItemFn, ReturnType, Attribute, Meta};
pub struct MustUseRule;
impl Rule for MustUseRule {
fn name(&self) -> &'static str {
"must_use_violations"
}
fn check(&self, ctx: &mut RuleContext) {
let syntax_tree = ctx.syntax_tree.clone();
let mut visitor = MustUseVisitor::new(ctx);
visitor.visit_file(&syntax_tree);
}
}
struct MustUseVisitor<'a> {
ctx: &'a mut RuleContext,
must_use_functions: Vec<String>,
}
impl<'a> MustUseVisitor<'a> {
fn new(ctx: &'a mut RuleContext) -> Self {
Self {
ctx,
must_use_functions: vec![
"collect".to_string(),
"map".to_string(),
"filter".to_string(),
"fold".to_string(),
"reduce".to_string(),
"try_fold".to_string(),
"try_reduce".to_string(),
"cloned".to_string(),
"copied".to_string(),
"enumerate".to_string(),
"zip".to_string(),
"chain".to_string(),
"take".to_string(),
"skip".to_string(),
"rev".to_string(),
],
}
}
fn has_must_use_attr(&self, attrs: &[Attribute]) -> bool {
attrs.iter().any(|attr| {
if let Meta::Path(path) = &attr.meta {
path.is_ident("must_use")
} else if let Meta::List(list) = &attr.meta {
list.path.is_ident("must_use")
} else {
false
}
})
}
fn check_unused_result(&mut self, _expr: &Expr, line: usize, col: usize, context: &str) {
self.ctx.report(Issue {
rule: "must_use_violations".to_string(),
severity: Severity::Warning,
message: format!("Unused result from {} - consider using `let _ = ...` if intentional", context),
location: Location {
line,
column: col,
end_line: Some(line),
end_column: Some(col + 10), },
fix: None,
});
}
fn is_result_ignored(&self, parent_expr: Option<&Expr>) -> bool {
match parent_expr {
None => true, Some(Expr::Block(_)) => true, Some(Expr::If(_)) => false, Some(Expr::Match(_)) => false, Some(Expr::Let(_)) => false, Some(Expr::Assign(_)) => false, _ => true, }
}
}
impl<'a> Visit<'a> for MustUseVisitor<'a> {
fn visit_item_fn(&mut self, item_fn: &'a ItemFn) {
if self.has_must_use_attr(&item_fn.attrs) {
let fn_name = item_fn.sig.ident.to_string();
self.must_use_functions.push(fn_name);
}
syn::visit::visit_item_fn(self, item_fn);
}
fn visit_expr_call(&mut self, call: &'a ExprCall) {
if let Expr::Path(path) = call.func.as_ref() {
if let Some(last_segment) = path.path.segments.last() {
let func_name = last_segment.ident.to_string();
if self.must_use_functions.contains(&func_name) {
let (line, col) = self.ctx.line_col(last_segment.ident.span());
self.check_unused_result(
&Expr::Call(call.clone()),
line,
col,
&format!("function call `{}`", func_name)
);
}
match func_name.as_str() {
"write" | "writeln" | "print" | "println" => {
let (line, col) = self.ctx.line_col(last_segment.ident.span());
self.ctx.report(Issue {
rule: "must_use_violations".to_string(),
severity: Severity::Info,
message: format!("Consider checking the result of `{}()` for error handling", func_name),
location: Location {
line,
column: col,
end_line: Some(line),
end_column: Some(col + func_name.len()),
},
fix: None,
});
}
_ => {}
}
}
}
syn::visit::visit_expr_call(self, call);
}
fn visit_expr_method_call(&mut self, method_call: &'a ExprMethodCall) {
let method_name = method_call.method.to_string();
if self.must_use_functions.contains(&method_name) {
let (line, col) = self.ctx.line_col(method_call.method.span());
match method_name.as_str() {
"map" | "filter" | "enumerate" | "zip" | "chain" | "take" | "skip" | "rev" => {
self.ctx.report(Issue {
rule: "must_use_violations".to_string(),
severity: Severity::Warning,
message: format!("Iterator method `{}()` returns a lazy iterator that must be consumed (e.g., with `.collect()`, `.for_each()`, etc.)", method_name),
location: Location {
line,
column: col,
end_line: Some(line),
end_column: Some(col + method_name.len()),
},
fix: None,
});
}
"collect" => {
}
_ => {
self.check_unused_result(
&Expr::MethodCall(method_call.clone()),
line,
col,
&format!("method call `.{}`", method_name)
);
}
}
}
match method_name.as_str() {
"ok" | "err" | "unwrap_or" | "unwrap_or_else" | "unwrap_or_default" => {
let (line, col) = self.ctx.line_col(method_call.method.span());
self.ctx.report(Issue {
rule: "must_use_violations".to_string(),
severity: Severity::Info,
message: format!("Result of `.{}()` should typically be used or explicitly ignored", method_name),
location: Location {
line,
column: col,
end_line: Some(line),
end_column: Some(col + method_name.len()),
},
fix: None,
});
}
_ => {}
}
syn::visit::visit_expr_method_call(self, method_call);
}
}