1use crate::rules::{Issue, Location, Rule, RuleContext, Severity};
2use syn::visit::Visit;
3use syn::{Expr, ExprCall, ExprMethodCall, Item, ItemFn, ReturnType, Attribute, Meta};
4
5pub struct MustUseRule;
6
7impl Rule for MustUseRule {
8 fn name(&self) -> &'static str {
9 "must_use_violations"
10 }
11
12 fn check(&self, ctx: &mut RuleContext) {
13 let syntax_tree = ctx.syntax_tree.clone();
14 let mut visitor = MustUseVisitor::new(ctx);
15 visitor.visit_file(&syntax_tree);
16 }
17}
18
19struct MustUseVisitor<'a> {
20 ctx: &'a mut RuleContext,
21 must_use_functions: Vec<String>,
22}
23
24impl<'a> MustUseVisitor<'a> {
25 fn new(ctx: &'a mut RuleContext) -> Self {
26 Self {
27 ctx,
28 must_use_functions: vec![
29 "collect".to_string(),
31 "map".to_string(),
32 "filter".to_string(),
33 "fold".to_string(),
34 "reduce".to_string(),
35 "try_fold".to_string(),
36 "try_reduce".to_string(),
37 "cloned".to_string(),
38 "copied".to_string(),
39 "enumerate".to_string(),
40 "zip".to_string(),
41 "chain".to_string(),
42 "take".to_string(),
43 "skip".to_string(),
44 "rev".to_string(),
45 ],
46 }
47 }
48
49 fn has_must_use_attr(&self, attrs: &[Attribute]) -> bool {
50 attrs.iter().any(|attr| {
51 if let Meta::Path(path) = &attr.meta {
52 path.is_ident("must_use")
53 } else if let Meta::List(list) = &attr.meta {
54 list.path.is_ident("must_use")
55 } else {
56 false
57 }
58 })
59 }
60
61 fn check_unused_result(&mut self, _expr: &Expr, line: usize, col: usize, context: &str) {
62 self.ctx.report(Issue {
63 rule: "must_use_violations".to_string(),
64 severity: Severity::Warning,
65 message: format!("Unused result from {} - consider using `let _ = ...` if intentional", context),
66 location: Location {
67 line,
68 column: col,
69 end_line: Some(line),
70 end_column: Some(col + 10), },
72 fix: None,
73 });
74 }
75
76 fn is_result_ignored(&self, parent_expr: Option<&Expr>) -> bool {
77 match parent_expr {
79 None => true, Some(Expr::Block(_)) => true, Some(Expr::If(_)) => false, Some(Expr::Match(_)) => false, Some(Expr::Let(_)) => false, Some(Expr::Assign(_)) => false, _ => true, }
87 }
88}
89
90impl<'a> Visit<'a> for MustUseVisitor<'a> {
91 fn visit_item_fn(&mut self, item_fn: &'a ItemFn) {
92 if self.has_must_use_attr(&item_fn.attrs) {
94 let fn_name = item_fn.sig.ident.to_string();
95 self.must_use_functions.push(fn_name);
96 }
97
98 syn::visit::visit_item_fn(self, item_fn);
100 }
101
102 fn visit_expr_call(&mut self, call: &'a ExprCall) {
103 if let Expr::Path(path) = call.func.as_ref() {
105 if let Some(last_segment) = path.path.segments.last() {
106 let func_name = last_segment.ident.to_string();
107
108 if self.must_use_functions.contains(&func_name) {
109 let (line, col) = self.ctx.line_col(last_segment.ident.span());
110 self.check_unused_result(
111 &Expr::Call(call.clone()),
112 line,
113 col,
114 &format!("function call `{}`", func_name)
115 );
116 }
117
118 match func_name.as_str() {
120 "write" | "writeln" | "print" | "println" => {
121 let (line, col) = self.ctx.line_col(last_segment.ident.span());
123 self.ctx.report(Issue {
124 rule: "must_use_violations".to_string(),
125 severity: Severity::Info,
126 message: format!("Consider checking the result of `{}()` for error handling", func_name),
127 location: Location {
128 line,
129 column: col,
130 end_line: Some(line),
131 end_column: Some(col + func_name.len()),
132 },
133 fix: None,
134 });
135 }
136 _ => {}
137 }
138 }
139 }
140
141 syn::visit::visit_expr_call(self, call);
143 }
144
145 fn visit_expr_method_call(&mut self, method_call: &'a ExprMethodCall) {
146 let method_name = method_call.method.to_string();
147
148 if self.must_use_functions.contains(&method_name) {
150 let (line, col) = self.ctx.line_col(method_call.method.span());
151
152 match method_name.as_str() {
153 "map" | "filter" | "enumerate" | "zip" | "chain" | "take" | "skip" | "rev" => {
154 self.ctx.report(Issue {
155 rule: "must_use_violations".to_string(),
156 severity: Severity::Warning,
157 message: format!("Iterator method `{}()` returns a lazy iterator that must be consumed (e.g., with `.collect()`, `.for_each()`, etc.)", method_name),
158 location: Location {
159 line,
160 column: col,
161 end_line: Some(line),
162 end_column: Some(col + method_name.len()),
163 },
164 fix: None,
165 });
166 }
167 "collect" => {
168 }
170 _ => {
171 self.check_unused_result(
172 &Expr::MethodCall(method_call.clone()),
173 line,
174 col,
175 &format!("method call `.{}`", method_name)
176 );
177 }
178 }
179 }
180
181 match method_name.as_str() {
183 "ok" | "err" | "unwrap_or" | "unwrap_or_else" | "unwrap_or_default" => {
184 let (line, col) = self.ctx.line_col(method_call.method.span());
186 self.ctx.report(Issue {
187 rule: "must_use_violations".to_string(),
188 severity: Severity::Info,
189 message: format!("Result of `.{}()` should typically be used or explicitly ignored", method_name),
190 location: Location {
191 line,
192 column: col,
193 end_line: Some(line),
194 end_column: Some(col + method_name.len()),
195 },
196 fix: None,
197 });
198 }
199 _ => {}
200 }
201
202 syn::visit::visit_expr_method_call(self, method_call);
204 }
205}