Skip to content

Commit f2f447c

Browse files
committed
Add Arithmetic lint
1 parent f93d418 commit f2f447c

16 files changed

+342
-52
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3436,6 +3436,7 @@ Released 2018-09-13
34363436
[`almost_complete_letter_range`]: https://rust-lang.github.io/rust-clippy/master/index.html#almost_complete_letter_range
34373437
[`almost_swapped`]: https://rust-lang.github.io/rust-clippy/master/index.html#almost_swapped
34383438
[`approx_constant`]: https://rust-lang.github.io/rust-clippy/master/index.html#approx_constant
3439+
[`arithmetic`]: https://rust-lang.github.io/rust-clippy/master/index.html#arithmetic
34393440
[`as_conversions`]: https://rust-lang.github.io/rust-clippy/master/index.html#as_conversions
34403441
[`as_underscore`]: https://rust-lang.github.io/rust-clippy/master/index.html#as_underscore
34413442
[`assertions_on_constants`]: https://rust-lang.github.io/rust-clippy/master/index.html#assertions_on_constants

clippy_lints/src/lib.register_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,7 @@ store.register_lints(&[
417417
only_used_in_recursion::ONLY_USED_IN_RECURSION,
418418
open_options::NONSENSICAL_OPEN_OPTIONS,
419419
operators::ABSURD_EXTREME_COMPARISONS,
420+
operators::ARITHMETIC,
420421
operators::ASSIGN_OP_PATTERN,
421422
operators::BAD_BIT_MASK,
422423
operators::CMP_NAN,

clippy_lints/src/lib.register_restriction.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ store.register_group(true, "clippy::restriction", Some("clippy_restriction"), ve
4747
LintId::of(mixed_read_write_in_expression::MIXED_READ_WRITE_IN_EXPRESSION),
4848
LintId::of(module_style::MOD_MODULE_FILES),
4949
LintId::of(module_style::SELF_NAMED_MODULE_FILES),
50+
LintId::of(operators::ARITHMETIC),
5051
LintId::of(operators::FLOAT_ARITHMETIC),
5152
LintId::of(operators::FLOAT_CMP_CONST),
5253
LintId::of(operators::INTEGER_ARITHMETIC),

clippy_lints/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -547,6 +547,8 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
547547
store.register_late_pass(|| Box::new(utils::internal_lints::MsrvAttrImpl));
548548
}
549549

550+
let arithmetic_allowed = conf.arithmetic_allowed.clone();
551+
store.register_late_pass(move || Box::new(operators::arithmetic::Arithmetic::new(arithmetic_allowed.clone())));
550552
store.register_late_pass(|| Box::new(utils::dump_hir::DumpHir));
551553
store.register_late_pass(|| Box::new(utils::author::Author));
552554
let await_holding_invalid_types = conf.await_holding_invalid_types.clone();
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
#![allow(
2+
// False positive
3+
clippy::match_same_arms
4+
)]
5+
6+
use super::ARITHMETIC;
7+
use clippy_utils::{consts::constant_simple, diagnostics::span_lint};
8+
use rustc_data_structures::fx::FxHashSet;
9+
use rustc_hir as hir;
10+
use rustc_lint::{LateContext, LateLintPass};
11+
use rustc_session::impl_lint_pass;
12+
use rustc_span::source_map::Span;
13+
14+
const HARD_CODED_ALLOWED: &[&str] = &["Saturating", "String", "Wrapping"];
15+
16+
#[derive(Debug)]
17+
pub struct Arithmetic {
18+
allowed: FxHashSet<String>,
19+
// Used to check whether expressions are constants, such as in enum discriminants and consts
20+
const_span: Option<Span>,
21+
expr_span: Option<Span>,
22+
}
23+
24+
impl_lint_pass!(Arithmetic => [ARITHMETIC]);
25+
26+
impl Arithmetic {
27+
#[must_use]
28+
pub fn new(mut allowed: FxHashSet<String>) -> Self {
29+
allowed.extend(HARD_CODED_ALLOWED.iter().copied().map(String::from));
30+
Self {
31+
allowed,
32+
const_span: None,
33+
expr_span: None,
34+
}
35+
}
36+
37+
/// Checks if the given `expr` has any of the inner `allowed` elements.
38+
fn is_allowed_ty(&self, cx: &LateContext<'_>, expr: &hir::Expr<'_>) -> bool {
39+
match expr.kind {
40+
hir::ExprKind::Call(local_expr, _) => self.is_allowed_ty(cx, local_expr),
41+
hir::ExprKind::Path(ref qpath) => self.manage_qpath(cx, qpath),
42+
hir::ExprKind::Struct(qpath, ..) => self.manage_qpath(cx, qpath),
43+
_ => false,
44+
}
45+
}
46+
47+
fn issue_lint(&mut self, cx: &LateContext<'_>, expr: &hir::Expr<'_>) {
48+
span_lint(cx, ARITHMETIC, expr.span, "arithmetic detected");
49+
self.expr_span = Some(expr.span);
50+
}
51+
52+
/// The actual checker used by `is_allowed_ty`.
53+
fn manage_qpath(&self, cx: &LateContext<'_>, qpath: &hir::QPath<'_>) -> bool {
54+
let path = if let hir::QPath::Resolved(_, path) = qpath {
55+
if let hir::def::Res::Local(hid) = path.res
56+
&& let parent_node_id = cx.tcx.hir().get_parent_node(hid)
57+
&& let Some(hir::Node::Local(local)) = cx.tcx.hir().find(parent_node_id)
58+
&& let Some(ty) = local.ty
59+
&& let hir::TyKind::Path(hir::QPath::Resolved(_, path)) = ty.kind
60+
{
61+
path
62+
}
63+
else {
64+
path
65+
}
66+
} else if let hir::QPath::TypeRelative(ty, _) = qpath
67+
&& let hir::TyKind::Path(hir::QPath::Resolved(_, path)) = ty.kind
68+
{
69+
path
70+
}
71+
else {
72+
return false;
73+
};
74+
for segment in path.segments {
75+
if self.allowed.contains(segment.ident.as_str()) {
76+
return true;
77+
}
78+
}
79+
false
80+
}
81+
}
82+
83+
impl<'tcx> LateLintPass<'tcx> for Arithmetic {
84+
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx hir::Expr<'_>) {
85+
if self.expr_span.is_some() {
86+
return;
87+
}
88+
if let Some(span) = self.const_span && span.contains(expr.span) {
89+
return;
90+
}
91+
match &expr.kind {
92+
hir::ExprKind::Binary(op, lhs, rhs) | hir::ExprKind::AssignOp(op, lhs, rhs) => {
93+
let (
94+
hir::BinOpKind::Add
95+
| hir::BinOpKind::Sub
96+
| hir::BinOpKind::Mul
97+
| hir::BinOpKind::Div
98+
| hir::BinOpKind::Rem
99+
| hir::BinOpKind::Shl
100+
| hir::BinOpKind::Shr
101+
) = op.node else {
102+
return;
103+
};
104+
if self.is_allowed_ty(cx, lhs) || self.is_allowed_ty(cx, rhs) {
105+
return;
106+
}
107+
self.issue_lint(cx, expr);
108+
},
109+
hir::ExprKind::Unary(hir::UnOp::Neg, _) => {
110+
// CTFE already takes care of things like `-1` that do not overflow.
111+
if constant_simple(cx, cx.typeck_results(), expr).is_none() {
112+
self.issue_lint(cx, expr);
113+
}
114+
},
115+
_ => {},
116+
}
117+
}
118+
119+
fn check_body(&mut self, cx: &LateContext<'_>, body: &hir::Body<'_>) {
120+
let body_owner = cx.tcx.hir().body_owner_def_id(body.id());
121+
match cx.tcx.hir().body_owner_kind(body_owner) {
122+
hir::BodyOwnerKind::Const | hir::BodyOwnerKind::Static(_) => {
123+
let body_span = cx.tcx.def_span(body_owner);
124+
if let Some(span) = self.const_span && span.contains(body_span) {
125+
return;
126+
}
127+
self.const_span = Some(body_span);
128+
},
129+
hir::BodyOwnerKind::Closure | hir::BodyOwnerKind::Fn => {},
130+
}
131+
}
132+
133+
fn check_body_post(&mut self, cx: &LateContext<'_>, body: &hir::Body<'_>) {
134+
let body_owner = cx.tcx.hir().body_owner(body.id());
135+
let body_span = cx.tcx.hir().span(body_owner);
136+
if let Some(span) = self.const_span && span.contains(body_span) {
137+
return;
138+
}
139+
self.const_span = None;
140+
}
141+
142+
fn check_expr_post(&mut self, _: &LateContext<'tcx>, expr: &'tcx hir::Expr<'_>) {
143+
if Some(expr.span) == self.expr_span {
144+
self.expr_span = None;
145+
}
146+
}
147+
}

clippy_lints/src/operators/mod.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ use rustc_hir::{Body, Expr, ExprKind, UnOp};
22
use rustc_lint::{LateContext, LateLintPass};
33
use rustc_session::{declare_tool_lint, impl_lint_pass};
44

5+
pub(crate) mod arithmetic;
6+
57
mod absurd_extreme_comparisons;
68
mod assign_op_pattern;
79
mod bit_mask;
@@ -57,6 +59,41 @@ declare_clippy_lint! {
5759
"a comparison with a maximum or minimum value that is always true or false"
5860
}
5961

62+
declare_clippy_lint! {
63+
/// ### What it does
64+
/// Checks for any kind of arithmetic operation of any type.
65+
///
66+
/// Operators like `+`, `-`, `*` or `<<` are usually capable of overflowing according to the [Rust
67+
/// Reference](https://doc.rust-lang.org/reference/expressions/operator-expr.html#overflow),
68+
/// or can panic (`/`, `%`). Known safe built-in types like `Wrapping` or `Saturing` are filtered
69+
/// away.
70+
///
71+
/// ### Why is this bad?
72+
/// Integer overflow will trigger a panic in debug builds or will wrap in
73+
/// release mode. Division by zero will cause a panic in either mode. In some applications one
74+
/// wants explicitly checked, wrapping or saturating arithmetic.
75+
///
76+
/// #### Example
77+
/// ```rust
78+
/// # let a = 0;
79+
/// a + 1;
80+
///
81+
/// Third-party types also tend to overflow.
82+
///
83+
/// #### Example
84+
/// ```
85+
/// use rust_decimal::Decimal;
86+
/// let _n = Decimal::MAX + Decimal::MAX;
87+
/// ```
88+
///
89+
/// ### Allowed types
90+
/// Custom allowed types can be specified through the `arithmetic-allowed` filter.
91+
#[clippy::version = "1.64.0"]
92+
pub ARITHMETIC,
93+
restriction,
94+
"any arithmetic expression that could overflow or panic"
95+
}
96+
6097
declare_clippy_lint! {
6198
/// ### What it does
6299
/// Checks for integer arithmetic operations which could overflow or panic.
@@ -747,6 +784,7 @@ pub struct Operators {
747784
}
748785
impl_lint_pass!(Operators => [
749786
ABSURD_EXTREME_COMPARISONS,
787+
ARITHMETIC,
750788
INTEGER_ARITHMETIC,
751789
FLOAT_ARITHMETIC,
752790
ASSIGN_OP_PATTERN,

clippy_lints/src/utils/conf.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,10 @@ macro_rules! define_Conf {
191191
}
192192

193193
define_Conf! {
194+
/// Lint: Arithmetic.
195+
///
196+
/// Suppress checking of the passed type names.
197+
(arithmetic_allowed: rustc_data_structures::fx::FxHashSet<String> = <_>::default()),
194198
/// Lint: ENUM_VARIANT_NAMES, LARGE_TYPES_PASSED_BY_VALUE, TRIVIALLY_COPY_PASS_BY_REF, UNNECESSARY_WRAPS, UPPER_CASE_ACRONYMS, WRONG_SELF_CONVENTION, BOX_COLLECTION, REDUNDANT_ALLOCATION, RC_BUFFER, VEC_BOX, OPTION_OPTION, LINKEDLIST, RC_MUTEX.
195199
///
196200
/// Suppress lints whenever the suggested change would cause breakage for other crates.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#![warn(clippy::arithmetic)]
2+
3+
use core::ops::Add;
4+
5+
#[derive(Clone, Copy)]
6+
struct Point {
7+
x: i32,
8+
y: i32,
9+
}
10+
11+
impl Add for Point {
12+
type Output = Self;
13+
14+
fn add(self, other: Self) -> Self {
15+
todo!()
16+
}
17+
}
18+
19+
fn main() {
20+
let _ = Point { x: 1, y: 0 } + Point { x: 2, y: 3 };
21+
22+
let point: Point = Point { x: 1, y: 0 };
23+
let _ = point + point;
24+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
arithmetic-allowed = ["Point"]

tests/ui-toml/toml_unknown_key/conf_unknown_key.stderr

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ error: error reading Clippy's configuration file `$DIR/clippy.toml`: unknown fie
33
allow-expect-in-tests
44
allow-unwrap-in-tests
55
allowed-scripts
6+
arithmetic-allowed
67
array-size-threshold
78
avoid-breaking-exported-api
89
await-holding-invalid-types

tests/ui/arithmetic.fixed

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// run-rustfix
2+
3+
#![allow(clippy::unnecessary_owned_empty_strings)]
4+
#![feature(saturating_int_impl)]
5+
#![warn(clippy::arithmetic)]
6+
7+
use core::num::{Saturating, Wrapping};
8+
9+
pub fn hard_coded_allowed() {
10+
let _ = Saturating(0u32) + Saturating(0u32);
11+
let _ = String::new() + "";
12+
let _ = Wrapping(0u32) + Wrapping(0u32);
13+
14+
let saturating: Saturating<u32> = Saturating(0u32);
15+
let string: String = String::new();
16+
let wrapping: Wrapping<u32> = Wrapping(0u32);
17+
18+
let _ = saturating + saturating;
19+
let _ = string + "";
20+
let _ = wrapping + wrapping;
21+
}
22+
23+
fn main() {}

tests/ui/arithmetic.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// run-rustfix
2+
3+
#![allow(clippy::unnecessary_owned_empty_strings)]
4+
#![feature(saturating_int_impl)]
5+
#![warn(clippy::arithmetic)]
6+
7+
use core::num::{Saturating, Wrapping};
8+
9+
pub fn hard_coded_allowed() {
10+
let _ = Saturating(0u32) + Saturating(0u32);
11+
let _ = String::new() + "";
12+
let _ = Wrapping(0u32) + Wrapping(0u32);
13+
14+
let saturating: Saturating<u32> = Saturating(0u32);
15+
let string: String = String::new();
16+
let wrapping: Wrapping<u32> = Wrapping(0u32);
17+
18+
let _ = saturating + saturating;
19+
let _ = string + "";
20+
let _ = wrapping + wrapping;
21+
}
22+
23+
fn main() {}

tests/ui/float_arithmetic.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#![warn(clippy::integer_arithmetic, clippy::float_arithmetic)]
1+
#![warn(clippy::arithmetic)]
22
#![allow(
33
unused,
44
clippy::shadow_reuse,

0 commit comments

Comments
 (0)