]>
Commit | Line | Data |
---|---|---|
f2b60f7d | 1 | use super::ARITHMETIC_SIDE_EFFECTS; |
487cf647 | 2 | use clippy_utils::{ |
9ffffee4 | 3 | consts::{constant, constant_simple, Constant}, |
487cf647 | 4 | diagnostics::span_lint, |
9ffffee4 | 5 | is_lint_allowed, peel_hir_expr_refs, peel_hir_expr_unary, |
487cf647 | 6 | }; |
f2b60f7d | 7 | use rustc_ast as ast; |
9c376795 | 8 | use rustc_data_structures::fx::{FxHashMap, FxHashSet}; |
f2b60f7d FG |
9 | use rustc_hir as hir; |
10 | use rustc_lint::{LateContext, LateLintPass}; | |
11 | use rustc_middle::ty::Ty; | |
12 | use rustc_session::impl_lint_pass; | |
13 | use rustc_span::source_map::{Span, Spanned}; | |
14 | ||
9c376795 FG |
15 | const HARD_CODED_ALLOWED_BINARY: &[[&str; 2]] = &[ |
16 | ["f32", "f32"], | |
17 | ["f64", "f64"], | |
18 | ["std::num::Saturating", "std::num::Saturating"], | |
19 | ["std::num::Wrapping", "std::num::Wrapping"], | |
20 | ["std::string::String", "&str"], | |
f2b60f7d | 21 | ]; |
9c376795 | 22 | const HARD_CODED_ALLOWED_UNARY: &[&str] = &["f32", "f64", "std::num::Saturating", "std::num::Wrapping"]; |
f2b60f7d FG |
23 | |
24 | #[derive(Debug)] | |
25 | pub struct ArithmeticSideEffects { | |
9c376795 FG |
26 | allowed_binary: FxHashMap<String, FxHashSet<String>>, |
27 | allowed_unary: FxHashSet<String>, | |
f2b60f7d FG |
28 | // Used to check whether expressions are constants, such as in enum discriminants and consts |
29 | const_span: Option<Span>, | |
30 | expr_span: Option<Span>, | |
31 | } | |
32 | ||
33 | impl_lint_pass!(ArithmeticSideEffects => [ARITHMETIC_SIDE_EFFECTS]); | |
34 | ||
35 | impl ArithmeticSideEffects { | |
36 | #[must_use] | |
9c376795 FG |
37 | pub fn new(user_allowed_binary: Vec<[String; 2]>, user_allowed_unary: Vec<String>) -> Self { |
38 | let mut allowed_binary: FxHashMap<String, FxHashSet<String>> = <_>::default(); | |
39 | for [lhs, rhs] in user_allowed_binary.into_iter().chain( | |
40 | HARD_CODED_ALLOWED_BINARY | |
41 | .iter() | |
42 | .copied() | |
43 | .map(|[lhs, rhs]| [lhs.to_string(), rhs.to_string()]), | |
44 | ) { | |
45 | allowed_binary.entry(lhs).or_default().insert(rhs); | |
46 | } | |
47 | let allowed_unary = user_allowed_unary | |
48 | .into_iter() | |
49 | .chain(HARD_CODED_ALLOWED_UNARY.iter().copied().map(String::from)) | |
50 | .collect(); | |
f2b60f7d | 51 | Self { |
9c376795 FG |
52 | allowed_binary, |
53 | allowed_unary, | |
f2b60f7d FG |
54 | const_span: None, |
55 | expr_span: None, | |
56 | } | |
57 | } | |
58 | ||
9c376795 FG |
59 | /// Checks if the lhs and the rhs types of a binary operation like "addition" or |
60 | /// "multiplication" are present in the inner set of allowed types. | |
61 | fn has_allowed_binary(&self, lhs_ty: Ty<'_>, rhs_ty: Ty<'_>) -> bool { | |
62 | let lhs_ty_string = lhs_ty.to_string(); | |
63 | let lhs_ty_string_elem = lhs_ty_string.split('<').next().unwrap_or_default(); | |
64 | let rhs_ty_string = rhs_ty.to_string(); | |
65 | let rhs_ty_string_elem = rhs_ty_string.split('<').next().unwrap_or_default(); | |
66 | if let Some(rhs_from_specific) = self.allowed_binary.get(lhs_ty_string_elem) | |
67 | && { | |
68 | let rhs_has_allowed_ty = rhs_from_specific.contains(rhs_ty_string_elem); | |
69 | rhs_has_allowed_ty || rhs_from_specific.contains("*") | |
70 | } | |
71 | { | |
72 | true | |
73 | } else if let Some(rhs_from_glob) = self.allowed_binary.get("*") { | |
74 | rhs_from_glob.contains(rhs_ty_string_elem) | |
75 | } else { | |
76 | false | |
77 | } | |
78 | } | |
79 | ||
80 | /// Checks if the type of an unary operation like "negation" is present in the inner set of | |
81 | /// allowed types. | |
82 | fn has_allowed_unary(&self, ty: Ty<'_>) -> bool { | |
83 | let ty_string = ty.to_string(); | |
84 | let ty_string_elem = ty_string.split('<').next().unwrap_or_default(); | |
85 | self.allowed_unary.contains(ty_string_elem) | |
f2b60f7d FG |
86 | } |
87 | ||
2b03887a FG |
88 | // For example, 8i32 or &i64::MAX. |
89 | fn is_integral(ty: Ty<'_>) -> bool { | |
90 | ty.peel_refs().is_integral() | |
f2b60f7d FG |
91 | } |
92 | ||
2b03887a | 93 | // Common entry-point to avoid code duplication. |
f2b60f7d | 94 | fn issue_lint(&mut self, cx: &LateContext<'_>, expr: &hir::Expr<'_>) { |
2b03887a FG |
95 | let msg = "arithmetic operation that can potentially result in unexpected side-effects"; |
96 | span_lint(cx, ARITHMETIC_SIDE_EFFECTS, expr.span, msg); | |
f2b60f7d FG |
97 | self.expr_span = Some(expr.span); |
98 | } | |
99 | ||
9ffffee4 | 100 | /// Returns the numeric value of a literal integer originated from `expr`, if any. |
9c376795 | 101 | /// |
9ffffee4 FG |
102 | /// Literal integers can be originated from adhoc declarations like `1`, associated constants |
103 | /// like `i32::MAX` or constant references like `N` from `const N: i32 = 1;`, | |
104 | fn literal_integer(cx: &LateContext<'_>, expr: &hir::Expr<'_>) -> Option<u128> { | |
9c376795 FG |
105 | let actual = peel_hir_expr_unary(expr).0; |
106 | if let hir::ExprKind::Lit(ref lit) = actual.kind && let ast::LitKind::Int(n, _) = lit.node { | |
9ffffee4 | 107 | return Some(n) |
2b03887a | 108 | } |
9ffffee4 FG |
109 | if let Some((Constant::Int(n), _)) = constant(cx, cx.typeck_results(), expr) { |
110 | return Some(n); | |
2b03887a | 111 | } |
9ffffee4 | 112 | None |
2b03887a FG |
113 | } |
114 | ||
f2b60f7d FG |
115 | /// Manages when the lint should be triggered. Operations in constant environments, hard coded |
116 | /// types, custom allowed types and non-constant operations that won't overflow are ignored. | |
2b03887a | 117 | fn manage_bin_ops<'tcx>( |
f2b60f7d | 118 | &mut self, |
2b03887a FG |
119 | cx: &LateContext<'tcx>, |
120 | expr: &hir::Expr<'tcx>, | |
f2b60f7d | 121 | op: &Spanned<hir::BinOpKind>, |
2b03887a FG |
122 | lhs: &hir::Expr<'tcx>, |
123 | rhs: &hir::Expr<'tcx>, | |
f2b60f7d FG |
124 | ) { |
125 | if constant_simple(cx, cx.typeck_results(), expr).is_some() { | |
126 | return; | |
127 | } | |
128 | if !matches!( | |
129 | op.node, | |
130 | hir::BinOpKind::Add | |
f2b60f7d | 131 | | hir::BinOpKind::Div |
9c376795 | 132 | | hir::BinOpKind::Mul |
f2b60f7d FG |
133 | | hir::BinOpKind::Rem |
134 | | hir::BinOpKind::Shl | |
135 | | hir::BinOpKind::Shr | |
9c376795 | 136 | | hir::BinOpKind::Sub |
f2b60f7d FG |
137 | ) { |
138 | return; | |
139 | }; | |
2b03887a FG |
140 | let lhs_ty = cx.typeck_results().expr_ty(lhs); |
141 | let rhs_ty = cx.typeck_results().expr_ty(rhs); | |
9c376795 | 142 | if self.has_allowed_binary(lhs_ty, rhs_ty) { |
f2b60f7d FG |
143 | return; |
144 | } | |
2b03887a | 145 | let has_valid_op = if Self::is_integral(lhs_ty) && Self::is_integral(rhs_ty) { |
353b0b11 FG |
146 | if let hir::BinOpKind::Shl | hir::BinOpKind::Shr = op.node { |
147 | // At least for integers, shifts are already handled by the CTFE | |
148 | return; | |
149 | } | |
487cf647 FG |
150 | let (actual_lhs, lhs_ref_counter) = peel_hir_expr_refs(lhs); |
151 | let (actual_rhs, rhs_ref_counter) = peel_hir_expr_refs(rhs); | |
9ffffee4 FG |
152 | match ( |
153 | Self::literal_integer(cx, actual_lhs), | |
154 | Self::literal_integer(cx, actual_rhs), | |
155 | ) { | |
487cf647 | 156 | (None, None) => false, |
353b0b11 FG |
157 | (None, Some(n)) => match (&op.node, n) { |
158 | // Division and module are always valid if applied to non-zero integers | |
159 | (hir::BinOpKind::Div | hir::BinOpKind::Rem, local_n) if local_n != 0 => true, | |
160 | // Adding or subtracting zeros is always a no-op | |
161 | (hir::BinOpKind::Add | hir::BinOpKind::Sub, 0) | |
162 | // Multiplication by 1 or 0 will never overflow | |
163 | | (hir::BinOpKind::Mul, 0 | 1) | |
164 | => true, | |
165 | _ => false, | |
166 | }, | |
167 | (Some(n), None) => match (&op.node, n) { | |
168 | // Adding or subtracting zeros is always a no-op | |
487cf647 | 169 | (hir::BinOpKind::Add | hir::BinOpKind::Sub, 0) |
353b0b11 FG |
170 | // Multiplication by 1 or 0 will never overflow |
171 | | (hir::BinOpKind::Mul, 0 | 1) | |
172 | => true, | |
487cf647 FG |
173 | _ => false, |
174 | }, | |
175 | (Some(_), Some(_)) => { | |
176 | matches!((lhs_ref_counter, rhs_ref_counter), (0, 0)) | |
177 | }, | |
2b03887a FG |
178 | } |
179 | } else { | |
180 | false | |
181 | }; | |
182 | if !has_valid_op { | |
183 | self.issue_lint(cx, expr); | |
f2b60f7d | 184 | } |
f2b60f7d | 185 | } |
487cf647 FG |
186 | |
187 | fn manage_unary_ops<'tcx>( | |
188 | &mut self, | |
189 | cx: &LateContext<'tcx>, | |
190 | expr: &hir::Expr<'tcx>, | |
191 | un_expr: &hir::Expr<'tcx>, | |
192 | un_op: hir::UnOp, | |
193 | ) { | |
194 | let hir::UnOp::Neg = un_op else { return; }; | |
195 | if constant(cx, cx.typeck_results(), un_expr).is_some() { | |
196 | return; | |
197 | } | |
198 | let ty = cx.typeck_results().expr_ty(expr).peel_refs(); | |
9c376795 | 199 | if self.has_allowed_unary(ty) { |
487cf647 FG |
200 | return; |
201 | } | |
202 | let actual_un_expr = peel_hir_expr_refs(un_expr).0; | |
9ffffee4 | 203 | if Self::literal_integer(cx, actual_un_expr).is_some() { |
487cf647 FG |
204 | return; |
205 | } | |
206 | self.issue_lint(cx, expr); | |
207 | } | |
208 | ||
9ffffee4 FG |
209 | fn should_skip_expr(&mut self, cx: &LateContext<'_>, expr: &hir::Expr<'_>) -> bool { |
210 | is_lint_allowed(cx, ARITHMETIC_SIDE_EFFECTS, expr.hir_id) | |
211 | || self.expr_span.is_some() | |
212 | || self.const_span.map_or(false, |sp| sp.contains(expr.span)) | |
487cf647 | 213 | } |
f2b60f7d FG |
214 | } |
215 | ||
216 | impl<'tcx> LateLintPass<'tcx> for ArithmeticSideEffects { | |
2b03887a | 217 | fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &hir::Expr<'tcx>) { |
9ffffee4 | 218 | if self.should_skip_expr(cx, expr) { |
f2b60f7d FG |
219 | return; |
220 | } | |
221 | match &expr.kind { | |
487cf647 | 222 | hir::ExprKind::AssignOp(op, lhs, rhs) | hir::ExprKind::Binary(op, lhs, rhs) => { |
f2b60f7d FG |
223 | self.manage_bin_ops(cx, expr, op, lhs, rhs); |
224 | }, | |
487cf647 FG |
225 | hir::ExprKind::Unary(un_op, un_expr) => { |
226 | self.manage_unary_ops(cx, expr, un_expr, *un_op); | |
f2b60f7d FG |
227 | }, |
228 | _ => {}, | |
229 | } | |
230 | } | |
231 | ||
232 | fn check_body(&mut self, cx: &LateContext<'_>, body: &hir::Body<'_>) { | |
233 | let body_owner = cx.tcx.hir().body_owner(body.id()); | |
9ffffee4 FG |
234 | let body_owner_def_id = cx.tcx.hir().body_owner_def_id(body.id()); |
235 | ||
f2b60f7d FG |
236 | let body_owner_kind = cx.tcx.hir().body_owner_kind(body_owner_def_id); |
237 | if let hir::BodyOwnerKind::Const | hir::BodyOwnerKind::Static(_) = body_owner_kind { | |
238 | let body_span = cx.tcx.hir().span_with_body(body_owner); | |
239 | if let Some(span) = self.const_span && span.contains(body_span) { | |
240 | return; | |
241 | } | |
242 | self.const_span = Some(body_span); | |
243 | } | |
244 | } | |
245 | ||
246 | fn check_body_post(&mut self, cx: &LateContext<'_>, body: &hir::Body<'_>) { | |
247 | let body_owner = cx.tcx.hir().body_owner(body.id()); | |
248 | let body_span = cx.tcx.hir().span(body_owner); | |
249 | if let Some(span) = self.const_span && span.contains(body_span) { | |
250 | return; | |
251 | } | |
252 | self.const_span = None; | |
253 | } | |
254 | ||
255 | fn check_expr_post(&mut self, _: &LateContext<'tcx>, expr: &'tcx hir::Expr<'_>) { | |
256 | if Some(expr.span) == self.expr_span { | |
257 | self.expr_span = None; | |
258 | } | |
259 | } | |
260 | } |