Skip to content

Commit 2cdb90d

Browse files
authored
New lint: manual_midpoint (rust-lang#13851)
changelog: [`manual_midpoint`]: new lint Closes rust-lang#13849
2 parents e1c1ac1 + baadee8 commit 2cdb90d

16 files changed

+355
-28
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -5807,6 +5807,7 @@ Released 2018-09-13
58075807
[`manual_main_separator_str`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_main_separator_str
58085808
[`manual_map`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_map
58095809
[`manual_memcpy`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_memcpy
5810+
[`manual_midpoint`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_midpoint
58105811
[`manual_next_back`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_next_back
58115812
[`manual_non_exhaustive`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_non_exhaustive
58125813
[`manual_ok_err`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_ok_err

book/src/lint_configuration.md

+1
Original file line numberDiff line numberDiff line change
@@ -781,6 +781,7 @@ The minimum rust version that the project supports. Defaults to the `rust-versio
781781
* [`manual_hash_one`](https://rust-lang.github.io/rust-clippy/master/index.html#manual_hash_one)
782782
* [`manual_is_ascii_check`](https://rust-lang.github.io/rust-clippy/master/index.html#manual_is_ascii_check)
783783
* [`manual_let_else`](https://rust-lang.github.io/rust-clippy/master/index.html#manual_let_else)
784+
* [`manual_midpoint`](https://rust-lang.github.io/rust-clippy/master/index.html#manual_midpoint)
784785
* [`manual_non_exhaustive`](https://rust-lang.github.io/rust-clippy/master/index.html#manual_non_exhaustive)
785786
* [`manual_option_as_slice`](https://rust-lang.github.io/rust-clippy/master/index.html#manual_option_as_slice)
786787
* [`manual_pattern_char_comparison`](https://rust-lang.github.io/rust-clippy/master/index.html#manual_pattern_char_comparison)

clippy_config/src/conf.rs

+1
Original file line numberDiff line numberDiff line change
@@ -629,6 +629,7 @@ define_Conf! {
629629
manual_hash_one,
630630
manual_is_ascii_check,
631631
manual_let_else,
632+
manual_midpoint,
632633
manual_non_exhaustive,
633634
manual_option_as_slice,
634635
manual_pattern_char_comparison,

clippy_lints/src/declared_lints.rs

+1
Original file line numberDiff line numberDiff line change
@@ -608,6 +608,7 @@ pub static LINTS: &[&crate::LintInfo] = &[
608608
crate::operators::IMPOSSIBLE_COMPARISONS_INFO,
609609
crate::operators::INEFFECTIVE_BIT_MASK_INFO,
610610
crate::operators::INTEGER_DIVISION_INFO,
611+
crate::operators::MANUAL_MIDPOINT_INFO,
611612
crate::operators::MISREFACTORED_ASSIGN_OP_INFO,
612613
crate::operators::MODULO_ARITHMETIC_INFO,
613614
crate::operators::MODULO_ONE_INFO,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
use clippy_utils::diagnostics::span_lint_and_sugg;
2+
use clippy_utils::msrvs::{self, Msrv};
3+
use clippy_utils::sugg::Sugg;
4+
use clippy_utils::{is_float_literal, is_integer_literal};
5+
use rustc_ast::BinOpKind;
6+
use rustc_errors::Applicability;
7+
use rustc_hir::{Expr, ExprKind};
8+
use rustc_lint::LateContext;
9+
use rustc_middle::ty::{self, Ty};
10+
11+
use super::MANUAL_MIDPOINT;
12+
13+
pub(super) fn check<'tcx>(
14+
cx: &LateContext<'tcx>,
15+
expr: &'tcx Expr<'_>,
16+
op: BinOpKind,
17+
left: &'tcx Expr<'_>,
18+
right: &'tcx Expr<'_>,
19+
msrv: &Msrv,
20+
) {
21+
if !left.span.from_expansion()
22+
&& !right.span.from_expansion()
23+
&& op == BinOpKind::Div
24+
&& (is_integer_literal(right, 2) || is_float_literal(right, 2.0))
25+
&& let Some((ll_expr, lr_expr)) = add_operands(left)
26+
&& add_operands(ll_expr).is_none() && add_operands(lr_expr).is_none()
27+
&& let left_ty = cx.typeck_results().expr_ty_adjusted(ll_expr)
28+
&& let right_ty = cx.typeck_results().expr_ty_adjusted(lr_expr)
29+
&& left_ty == right_ty
30+
// Do not lint on `(_+1)/2` and `(1+_)/2`, it is likely a `div_ceil()` operation
31+
&& !is_integer_literal(ll_expr, 1) && !is_integer_literal(lr_expr, 1)
32+
&& is_midpoint_implemented(left_ty, msrv)
33+
{
34+
let mut app = Applicability::MachineApplicable;
35+
let left_sugg = Sugg::hir_with_context(cx, ll_expr, expr.span.ctxt(), "..", &mut app);
36+
let right_sugg = Sugg::hir_with_context(cx, lr_expr, expr.span.ctxt(), "..", &mut app);
37+
let sugg = format!("{left_ty}::midpoint({left_sugg}, {right_sugg})");
38+
span_lint_and_sugg(
39+
cx,
40+
MANUAL_MIDPOINT,
41+
expr.span,
42+
"manual implementation of `midpoint` which can overflow",
43+
format!("use `{left_ty}::midpoint` instead"),
44+
sugg,
45+
app,
46+
);
47+
}
48+
}
49+
50+
/// Return the left and right operands if `expr` represents an addition
51+
fn add_operands<'e, 'tcx>(expr: &'e Expr<'tcx>) -> Option<(&'e Expr<'tcx>, &'e Expr<'tcx>)> {
52+
match expr.kind {
53+
ExprKind::Binary(op, left, right) if op.node == BinOpKind::Add => Some((left, right)),
54+
_ => None,
55+
}
56+
}
57+
58+
fn is_midpoint_implemented(ty: Ty<'_>, msrv: &Msrv) -> bool {
59+
match ty.kind() {
60+
ty::Uint(_) | ty::Float(_) => msrv.meets(msrvs::UINT_FLOAT_MIDPOINT),
61+
ty::Int(_) => msrv.meets(msrvs::INT_MIDPOINT),
62+
_ => false,
63+
}
64+
}

clippy_lints/src/operators/mod.rs

+32
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ mod float_cmp;
1111
mod float_equality_without_abs;
1212
mod identity_op;
1313
mod integer_division;
14+
mod manual_midpoint;
1415
mod misrefactored_assign_op;
1516
mod modulo_arithmetic;
1617
mod modulo_one;
@@ -24,6 +25,7 @@ mod verbose_bit_mask;
2425
pub(crate) mod arithmetic_side_effects;
2526

2627
use clippy_config::Conf;
28+
use clippy_utils::msrvs::Msrv;
2729
use rustc_hir::{Body, Expr, ExprKind, UnOp};
2830
use rustc_lint::{LateContext, LateLintPass};
2931
use rustc_session::impl_lint_pass;
@@ -834,17 +836,43 @@ declare_clippy_lint! {
834836
"explicit self-assignment"
835837
}
836838

839+
declare_clippy_lint! {
840+
/// ### What it does
841+
/// Checks for manual implementation of `midpoint`.
842+
///
843+
/// ### Why is this bad?
844+
/// Using `(x + y) / 2` might cause an overflow on the intermediate
845+
/// addition result.
846+
///
847+
/// ### Example
848+
/// ```no_run
849+
/// # let a: u32 = 0;
850+
/// let c = (a + 10) / 2;
851+
/// ```
852+
/// Use instead:
853+
/// ```no_run
854+
/// # let a: u32 = 0;
855+
/// let c = u32::midpoint(a, 10);
856+
/// ```
857+
#[clippy::version = "1.87.0"]
858+
pub MANUAL_MIDPOINT,
859+
pedantic,
860+
"manual implementation of `midpoint` which can overflow"
861+
}
862+
837863
pub struct Operators {
838864
arithmetic_context: numeric_arithmetic::Context,
839865
verbose_bit_mask_threshold: u64,
840866
modulo_arithmetic_allow_comparison_to_zero: bool,
867+
msrv: Msrv,
841868
}
842869
impl Operators {
843870
pub fn new(conf: &'static Conf) -> Self {
844871
Self {
845872
arithmetic_context: numeric_arithmetic::Context::default(),
846873
verbose_bit_mask_threshold: conf.verbose_bit_mask_threshold,
847874
modulo_arithmetic_allow_comparison_to_zero: conf.allow_comparison_to_zero,
875+
msrv: conf.msrv.clone(),
848876
}
849877
}
850878
}
@@ -876,6 +904,7 @@ impl_lint_pass!(Operators => [
876904
NEEDLESS_BITWISE_BOOL,
877905
PTR_EQ,
878906
SELF_ASSIGNMENT,
907+
MANUAL_MIDPOINT,
879908
]);
880909

881910
impl<'tcx> LateLintPass<'tcx> for Operators {
@@ -893,6 +922,7 @@ impl<'tcx> LateLintPass<'tcx> for Operators {
893922
identity_op::check(cx, e, op.node, lhs, rhs);
894923
needless_bitwise_bool::check(cx, e, op.node, lhs, rhs);
895924
ptr_eq::check(cx, e, op.node, lhs, rhs);
925+
manual_midpoint::check(cx, e, op.node, lhs, rhs, &self.msrv);
896926
}
897927
self.arithmetic_context.check_binary(cx, e, op.node, lhs, rhs);
898928
bit_mask::check(cx, e, op.node, lhs, rhs);
@@ -943,6 +973,8 @@ impl<'tcx> LateLintPass<'tcx> for Operators {
943973
fn check_body_post(&mut self, cx: &LateContext<'tcx>, b: &Body<'_>) {
944974
self.arithmetic_context.body_post(cx, b);
945975
}
976+
977+
extract_msrv_attr!(LateContext);
946978
}
947979

948980
fn macro_with_not_op(e: &Expr<'_>) -> bool {

clippy_utils/src/lib.rs

+11
Original file line numberDiff line numberDiff line change
@@ -1752,6 +1752,17 @@ pub fn is_integer_literal(expr: &Expr<'_>, value: u128) -> bool {
17521752
false
17531753
}
17541754

1755+
/// Checks whether the given expression is a constant literal of the given value.
1756+
pub fn is_float_literal(expr: &Expr<'_>, value: f64) -> bool {
1757+
if let ExprKind::Lit(spanned) = expr.kind
1758+
&& let LitKind::Float(v, _) = spanned.node
1759+
{
1760+
v.as_str().parse() == Ok(value)
1761+
} else {
1762+
false
1763+
}
1764+
}
1765+
17551766
/// Returns `true` if the given `Expr` has been coerced before.
17561767
///
17571768
/// Examples of coercions can be found in the Nomicon at

clippy_utils/src/msrvs.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ macro_rules! msrv_aliases {
1919

2020
// names may refer to stabilized feature flags or library items
2121
msrv_aliases! {
22-
1,87,0 { OS_STR_DISPLAY }
22+
1,87,0 { OS_STR_DISPLAY, INT_MIDPOINT }
23+
1,85,0 { UINT_FLOAT_MIDPOINT }
2324
1,84,0 { CONST_OPTION_AS_SLICE }
2425
1,83,0 { CONST_EXTERN_FN, CONST_FLOAT_BITS_CONV, CONST_FLOAT_CLASSIFY, CONST_MUT_REFS, CONST_UNWRAP }
2526
1,82,0 { IS_NONE_OR, REPEAT_N, RAW_REF_OP }

tests/ui/manual_midpoint.fixed

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
#![warn(clippy::manual_midpoint)]
2+
3+
macro_rules! mac {
4+
($a: expr, $b: expr) => {
5+
($a + $b) / 2
6+
};
7+
}
8+
9+
macro_rules! add {
10+
($a: expr, $b: expr) => {
11+
($a + $b)
12+
};
13+
}
14+
15+
macro_rules! two {
16+
() => {
17+
2
18+
};
19+
}
20+
21+
#[clippy::msrv = "1.84"]
22+
fn older_msrv() {
23+
let a: u32 = 10;
24+
let _ = (a + 5) / 2;
25+
}
26+
27+
#[clippy::msrv = "1.85"]
28+
fn main() {
29+
let a: u32 = 10;
30+
let _ = u32::midpoint(a, 5); //~ ERROR: manual implementation of `midpoint`
31+
32+
let f: f32 = 10.0;
33+
let _ = f32::midpoint(f, 5.0); //~ ERROR: manual implementation of `midpoint`
34+
35+
let _: u32 = 5 + u32::midpoint(8, 8) + 2; //~ ERROR: manual implementation of `midpoint`
36+
let _: u32 = const { u32::midpoint(8, 8) }; //~ ERROR: manual implementation of `midpoint`
37+
let _: f64 = const { f64::midpoint(8.0f64, 8.) }; //~ ERROR: manual implementation of `midpoint`
38+
let _: u32 = u32::midpoint(u32::default(), u32::default()); //~ ERROR: manual implementation of `midpoint`
39+
let _: u32 = u32::midpoint(two!(), two!()); //~ ERROR: manual implementation of `midpoint`
40+
41+
// Do not lint in presence of an addition with more than 2 operands
42+
let _: u32 = (10 + 20 + 30) / 2;
43+
44+
// Do not lint if whole or part is coming from a macro
45+
let _ = mac!(10, 20);
46+
let _: u32 = add!(10u32, 20u32) / 2;
47+
let _: u32 = (10 + 20) / two!();
48+
49+
// Do not lint if a literal is not present
50+
let _ = (f + 5.0) / (1.0 + 1.0);
51+
52+
// Do not lint on signed integer types
53+
let i: i32 = 10;
54+
let _ = (i + 5) / 2;
55+
56+
// Do not lint on (x+1)/2 or (1+x)/2 as this looks more like a `div_ceil()` operation
57+
let _ = (i + 1) / 2;
58+
let _ = (1 + i) / 2;
59+
60+
// But if we see (x+1.0)/2.0 or (x+1.0)/2.0, it is probably a midpoint operation
61+
let _ = f32::midpoint(f, 1.0); //~ ERROR: manual implementation of `midpoint`
62+
let _ = f32::midpoint(1.0, f); //~ ERROR: manual implementation of `midpoint`
63+
}
64+
65+
#[clippy::msrv = "1.86"]
66+
fn older_signed_midpoint(i: i32) {
67+
// Do not lint
68+
let _ = (i + 10) / 2;
69+
}
70+
71+
#[clippy::msrv = "1.87"]
72+
fn signed_midpoint(i: i32) {
73+
let _ = i32::midpoint(i, 10); //~ ERROR: manual implementation of `midpoint`
74+
}

tests/ui/manual_midpoint.rs

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
#![warn(clippy::manual_midpoint)]
2+
3+
macro_rules! mac {
4+
($a: expr, $b: expr) => {
5+
($a + $b) / 2
6+
};
7+
}
8+
9+
macro_rules! add {
10+
($a: expr, $b: expr) => {
11+
($a + $b)
12+
};
13+
}
14+
15+
macro_rules! two {
16+
() => {
17+
2
18+
};
19+
}
20+
21+
#[clippy::msrv = "1.84"]
22+
fn older_msrv() {
23+
let a: u32 = 10;
24+
let _ = (a + 5) / 2;
25+
}
26+
27+
#[clippy::msrv = "1.85"]
28+
fn main() {
29+
let a: u32 = 10;
30+
let _ = (a + 5) / 2; //~ ERROR: manual implementation of `midpoint`
31+
32+
let f: f32 = 10.0;
33+
let _ = (f + 5.0) / 2.0; //~ ERROR: manual implementation of `midpoint`
34+
35+
let _: u32 = 5 + (8 + 8) / 2 + 2; //~ ERROR: manual implementation of `midpoint`
36+
let _: u32 = const { (8 + 8) / 2 }; //~ ERROR: manual implementation of `midpoint`
37+
let _: f64 = const { (8.0f64 + 8.) / 2. }; //~ ERROR: manual implementation of `midpoint`
38+
let _: u32 = (u32::default() + u32::default()) / 2; //~ ERROR: manual implementation of `midpoint`
39+
let _: u32 = (two!() + two!()) / 2; //~ ERROR: manual implementation of `midpoint`
40+
41+
// Do not lint in presence of an addition with more than 2 operands
42+
let _: u32 = (10 + 20 + 30) / 2;
43+
44+
// Do not lint if whole or part is coming from a macro
45+
let _ = mac!(10, 20);
46+
let _: u32 = add!(10u32, 20u32) / 2;
47+
let _: u32 = (10 + 20) / two!();
48+
49+
// Do not lint if a literal is not present
50+
let _ = (f + 5.0) / (1.0 + 1.0);
51+
52+
// Do not lint on signed integer types
53+
let i: i32 = 10;
54+
let _ = (i + 5) / 2;
55+
56+
// Do not lint on (x+1)/2 or (1+x)/2 as this looks more like a `div_ceil()` operation
57+
let _ = (i + 1) / 2;
58+
let _ = (1 + i) / 2;
59+
60+
// But if we see (x+1.0)/2.0 or (x+1.0)/2.0, it is probably a midpoint operation
61+
let _ = (f + 1.0) / 2.0; //~ ERROR: manual implementation of `midpoint`
62+
let _ = (1.0 + f) / 2.0; //~ ERROR: manual implementation of `midpoint`
63+
}
64+
65+
#[clippy::msrv = "1.86"]
66+
fn older_signed_midpoint(i: i32) {
67+
// Do not lint
68+
let _ = (i + 10) / 2;
69+
}
70+
71+
#[clippy::msrv = "1.87"]
72+
fn signed_midpoint(i: i32) {
73+
let _ = (i + 10) / 2; //~ ERROR: manual implementation of `midpoint`
74+
}

0 commit comments

Comments
 (0)