Skip to main content

cargo_fl/rules/
must_use.rs

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                // Common std functions that return must_use types
30                "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), // Approximate
71            },
72            fix: None,
73        });
74    }
75
76    fn is_result_ignored(&self, parent_expr: Option<&Expr>) -> bool {
77        // Check if this is a standalone statement (not assigned or used)
78        match parent_expr {
79            None => true, // Top-level expression
80            Some(Expr::Block(_)) => true, // Statement in block
81            Some(Expr::If(_)) => false, // Used in condition
82            Some(Expr::Match(_)) => false, // Used in match
83            Some(Expr::Let(_)) => false, // Assigned to variable
84            Some(Expr::Assign(_)) => false, // Part of assignment
85            _ => true, // Default to checking
86        }
87    }
88}
89
90impl<'a> Visit<'a> for MustUseVisitor<'a> {
91    fn visit_item_fn(&mut self, item_fn: &'a ItemFn) {
92        // Check if function has #[must_use] and track it
93        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        // Continue visiting the function body
99        syn::visit::visit_item_fn(self, item_fn);
100    }
101
102    fn visit_expr_call(&mut self, call: &'a ExprCall) {
103        // Check for calls to functions that return must_use types
104        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                // Check for specific patterns
119                match func_name.as_str() {
120                    "write" | "writeln" | "print" | "println" => {
121                        // These are commonly ignored, but should be checked
122                        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        // Continue visiting
142        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        // Check for iterator methods that should be consumed
149        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                    // This is good - consuming the iterator
169                }
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        // Check for Result/Option methods
182        match method_name.as_str() {
183            "ok" | "err" | "unwrap_or" | "unwrap_or_else" | "unwrap_or_default" => {
184                // These convert Result/Option and should often be used
185                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        // Continue visiting
203        syn::visit::visit_expr_method_call(self, method_call);
204    }
205}