Skip to content

Commit 51fdafb

Browse files
committed
improve [cast_sign_loss], to skip warning on mathmatical expression that is always positive
1 parent 6cfbe57 commit 51fdafb

File tree

3 files changed

+255
-26
lines changed

3 files changed

+255
-26
lines changed
+133-25
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
use clippy_utils::consts::{constant, Constant};
22
use clippy_utils::diagnostics::span_lint;
3-
use clippy_utils::{method_chain_args, sext};
4-
use rustc_hir::{Expr, ExprKind};
3+
use clippy_utils::{clip, method_chain_args, sext};
4+
use rustc_hir::{BinOpKind, Expr, ExprKind};
55
use rustc_lint::LateContext;
6-
use rustc_middle::ty::{self, Ty};
6+
use rustc_middle::ty::{self, Ty, UintTy};
77

88
use super::CAST_SIGN_LOSS;
99

10+
const METHODS_RET_POSITIVE: &[&str] = &["abs", "checked_abs", "rem_euclid", "checked_rem_euclid"];
11+
1012
pub(super) fn check(cx: &LateContext<'_>, expr: &Expr<'_>, cast_op: &Expr<'_>, cast_from: Ty<'_>, cast_to: Ty<'_>) {
1113
if should_lint(cx, cast_op, cast_from, cast_to) {
1214
span_lint(
@@ -18,44 +20,150 @@ pub(super) fn check(cx: &LateContext<'_>, expr: &Expr<'_>, cast_op: &Expr<'_>, c
1820
}
1921
}
2022

23+
/// (Remove after final code review)
24+
/// When does a mathmatic expression always resulting a positive value?
25+
/// 1. Even number of same variables: `x*x`
26+
/// 2. Odd number of same variables but, ignore the ones that a guarenteed to be positive, there are
27+
/// even number of the vairables left: `x*x*x.abs()*x.abs()*x*x`
28+
/// 3. Any number of any variables, but all them are guarenteed to be positive:
29+
/// `x.abs()*y.abs()*z.abs()`
30+
/// 4. Any number of any variables, ignoreing the guarenteed ones, there are even number of same
31+
/// variables left: `x.abs()*y.abs()*x*x*y*y`
32+
/// 5. Any number that calls `pow*` with even number: `x.pow(2)`
33+
///
34+
/// TODO:
35+
/// 1. ~A function called `expr_is_always_positive`~
36+
/// 2. ~A function to peel binary op (Skip ops other than Mul, Div)~
37+
/// 3. ~When peeling binary op, collect each expr~
38+
/// 4. ~For each expr, check if they are always positive, then count them if not~
39+
/// 5. ~Detect above cases and call it for the day~
2140
fn should_lint(cx: &LateContext<'_>, cast_op: &Expr<'_>, cast_from: Ty<'_>, cast_to: Ty<'_>) -> bool {
2241
match (cast_from.is_integral(), cast_to.is_integral()) {
2342
(true, true) => {
2443
if !cast_from.is_signed() || cast_to.is_signed() {
2544
return false;
2645
}
2746

28-
// Don't lint for positive constants.
29-
let const_val = constant(cx, cx.typeck_results(), cast_op);
30-
if let Some(Constant::Int(n)) = const_val
31-
&& let ty::Int(ity) = *cast_from.kind()
32-
&& sext(cx.tcx, n, ity) >= 0
33-
{
47+
// Don't lint if `cast_op` is known to be positive.
48+
if let Sign::ZeroOrPositive = expr_sign(cx, cast_op, cast_from) {
3449
return false;
3550
}
3651

37-
// Don't lint for the result of methods that always return non-negative values.
38-
if let ExprKind::MethodCall(path, ..) = cast_op.kind {
39-
let mut method_name = path.ident.name.as_str();
40-
let allowed_methods = ["abs", "checked_abs", "rem_euclid", "checked_rem_euclid"];
41-
42-
if method_name == "unwrap"
43-
&& let Some(arglist) = method_chain_args(cast_op, &["unwrap"])
44-
&& let ExprKind::MethodCall(inner_path, ..) = &arglist[0].0.kind
45-
{
46-
method_name = inner_path.ident.name.as_str();
47-
}
48-
49-
if allowed_methods.iter().any(|&name| method_name == name) {
50-
return false;
51-
}
52+
let (mut uncertain_count, mut negative_count) = (0, 0);
53+
// Peel off possible binary expressions, e.g. x * x * y => [x, x, y]
54+
let Some(exprs) = exprs_with_selected_binop_peeled(cast_op) else {
55+
// Assume cast sign lose if we cannot determine the sign of `cast_op`
56+
return true;
57+
};
58+
for expr in exprs {
59+
let ty = cx.typeck_results().expr_ty(expr);
60+
match expr_sign(cx, expr, ty) {
61+
Sign::Negative => negative_count += 1,
62+
Sign::Uncertain => uncertain_count += 1,
63+
Sign::ZeroOrPositive => (),
64+
};
5265
}
5366

54-
true
67+
// Lint if there are odd number of uncertain or negative results
68+
uncertain_count % 2 == 1 || negative_count % 2 == 1
5569
},
5670

5771
(false, true) => !cast_to.is_signed(),
5872

5973
(_, _) => false,
6074
}
6175
}
76+
77+
fn get_const_int_eval(cx: &LateContext<'_>, expr: &Expr<'_>, ty: Ty<'_>) -> Option<i128> {
78+
if let Constant::Int(n) = constant(cx, cx.typeck_results(), expr)?
79+
&& let ty::Int(ity) = *ty.kind()
80+
{
81+
return Some(sext(cx.tcx, n, ity));
82+
}
83+
None
84+
}
85+
86+
enum Sign {
87+
ZeroOrPositive,
88+
Negative,
89+
Uncertain,
90+
}
91+
92+
fn expr_sign(cx: &LateContext<'_>, expr: &Expr<'_>, ty: Ty<'_>) -> Sign {
93+
// Try evaluate this expr first to see if it's positive
94+
if let Some(val) = get_const_int_eval(cx, expr, ty) {
95+
return if val >= 0 { Sign::ZeroOrPositive } else { Sign::Negative };
96+
}
97+
// Calling on methods that always return non-negative values.
98+
if let ExprKind::MethodCall(path, caller, args, ..) = expr.kind {
99+
let mut method_name = path.ident.name.as_str();
100+
101+
if method_name == "unwrap"
102+
&& let Some(arglist) = method_chain_args(expr, &["unwrap"])
103+
&& let ExprKind::MethodCall(inner_path, ..) = &arglist[0].0.kind
104+
{
105+
method_name = inner_path.ident.name.as_str();
106+
}
107+
108+
if method_name == "pow"
109+
&& let [arg] = args
110+
{
111+
return pow_call_result_sign(cx, caller, arg);
112+
} else if METHODS_RET_POSITIVE.iter().any(|&name| method_name == name) {
113+
return Sign::ZeroOrPositive;
114+
}
115+
}
116+
117+
Sign::Uncertain
118+
}
119+
120+
/// Return the sign of the `pow` call's result.
121+
///
122+
/// If the caller is a positive number, the result is always positive,
123+
/// If the `power_of` is a even number, the result is always positive as well,
124+
/// Otherwise a [`Sign::Uncertain`] will be returned.
125+
fn pow_call_result_sign(cx: &LateContext<'_>, caller: &Expr<'_>, power_of: &Expr<'_>) -> Sign {
126+
let caller_ty = cx.typeck_results().expr_ty(caller);
127+
if let Some(caller_val) = get_const_int_eval(cx, caller, caller_ty)
128+
&& caller_val >= 0
129+
{
130+
return Sign::ZeroOrPositive;
131+
}
132+
133+
if let Some(Constant::Int(n)) = constant(cx, cx.typeck_results(), power_of)
134+
&& clip(cx.tcx, n, UintTy::U32) % 2 == 0
135+
{
136+
return Sign::ZeroOrPositive;
137+
}
138+
139+
Sign::Uncertain
140+
}
141+
142+
/// Peels binary operators such as [`BinOpKind::Mul`], [`BinOpKind::Div`] or [`BinOpKind::Rem`],
143+
/// which the result could always be positive under certain condition.
144+
///
145+
/// Other operators such as `+`/`-` causing the result's sign hard to determine, which we will
146+
/// return `None`
147+
fn exprs_with_selected_binop_peeled<'a>(expr: &'a Expr<'_>) -> Option<Vec<&'a Expr<'a>>> {
148+
#[inline]
149+
fn collect_operands<'a>(expr: &'a Expr<'a>, operands: &mut Vec<&'a Expr<'a>>) -> Option<()> {
150+
match expr.kind {
151+
ExprKind::Binary(op, lhs, rhs) => {
152+
if matches!(op.node, BinOpKind::Mul | BinOpKind::Div | BinOpKind::Rem) {
153+
collect_operands(lhs, operands);
154+
operands.push(rhs);
155+
} else {
156+
// Things are complicated when there are other binary ops exist,
157+
// abort checking by returning `None` for now.
158+
return None;
159+
}
160+
},
161+
_ => operands.push(expr),
162+
}
163+
Some(())
164+
}
165+
166+
let mut res = vec![];
167+
collect_operands(expr, &mut res)?;
168+
Some(res)
169+
}

tests/ui/cast.rs

+49
Original file line numberDiff line numberDiff line change
@@ -365,3 +365,52 @@ fn avoid_subtract_overflow(q: u32) {
365365
fn issue11426() {
366366
(&42u8 >> 0xa9008fb6c9d81e42_0e25730562a601c8_u128) as usize;
367367
}
368+
369+
fn issue11642() {
370+
fn square(x: i16) -> u32 {
371+
let x = x as i32;
372+
(x * x) as u32;
373+
x.pow(2) as u32;
374+
(-2_i32).pow(2) as u32
375+
}
376+
377+
let _a = |x: i32| -> u32 { (x * x * x * x) as u32 };
378+
379+
(-2_i32).pow(3) as u32;
380+
//~^ ERROR: casting `i32` to `u32` may lose the sign of the value
381+
382+
let x: i32 = 10;
383+
(x * x) as u32;
384+
(x * x * x) as u32;
385+
//~^ ERROR: casting `i32` to `u32` may lose the sign of the value
386+
387+
let y: i16 = -2;
388+
(y * y * y * y * -2) as u16;
389+
//~^ ERROR: casting `i16` to `u16` may lose the sign of the value
390+
(y * y * y * y * 2) as u16;
391+
(y * y * y * 2) as u16;
392+
//~^ ERROR: casting `i16` to `u16` may lose the sign of the value
393+
(y * y * y * -2) as u16;
394+
//~^ ERROR: casting `i16` to `u16` may lose the sign of the value
395+
396+
fn foo(a: i32, b: i32, c: i32) -> u32 {
397+
(a * a * b * b * c * c) as u32;
398+
(a * b * c) as u32;
399+
//~^ ERROR: casting `i32` to `u32` may lose the sign of the value
400+
(a * -b * c) as u32;
401+
//~^ ERROR: casting `i32` to `u32` may lose the sign of the value
402+
(a * b * c * c) as u32;
403+
(a * -2) as u32;
404+
//~^ ERROR: casting `i32` to `u32` may lose the sign of the value
405+
(a * b * c * -2) as u32;
406+
//~^ ERROR: casting `i32` to `u32` may lose the sign of the value
407+
(a / b) as u32;
408+
(a / b * c) as u32;
409+
//~^ ERROR: casting `i32` to `u32` may lose the sign of the value
410+
(a / b + b * c) as u32;
411+
//~^ ERROR: casting `i32` to `u32` may lose the sign of the value
412+
a.pow(3) as u32;
413+
//~^ ERROR: casting `i32` to `u32` may lose the sign of the value
414+
(a.abs() * b.pow(2) / c.abs()) as u32
415+
}
416+
}

tests/ui/cast.stderr

+73-1
Original file line numberDiff line numberDiff line change
@@ -444,5 +444,77 @@ help: ... or use `try_from` and handle the error accordingly
444444
LL | let c = u8::try_from(q / 1000);
445445
| ~~~~~~~~~~~~~~~~~~~~~~
446446

447-
error: aborting due to 51 previous errors
447+
error: casting `i32` to `u32` may lose the sign of the value
448+
--> $DIR/cast.rs:379:5
449+
|
450+
LL | (-2_i32).pow(3) as u32;
451+
| ^^^^^^^^^^^^^^^^^^^^^^
452+
453+
error: casting `i32` to `u32` may lose the sign of the value
454+
--> $DIR/cast.rs:384:5
455+
|
456+
LL | (x * x * x) as u32;
457+
| ^^^^^^^^^^^^^^^^^^
458+
459+
error: casting `i16` to `u16` may lose the sign of the value
460+
--> $DIR/cast.rs:388:5
461+
|
462+
LL | (y * y * y * y * -2) as u16;
463+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
464+
465+
error: casting `i16` to `u16` may lose the sign of the value
466+
--> $DIR/cast.rs:391:5
467+
|
468+
LL | (y * y * y * 2) as u16;
469+
| ^^^^^^^^^^^^^^^^^^^^^^
470+
471+
error: casting `i16` to `u16` may lose the sign of the value
472+
--> $DIR/cast.rs:393:5
473+
|
474+
LL | (y * y * y * -2) as u16;
475+
| ^^^^^^^^^^^^^^^^^^^^^^^
476+
477+
error: casting `i32` to `u32` may lose the sign of the value
478+
--> $DIR/cast.rs:398:9
479+
|
480+
LL | (a * b * c) as u32;
481+
| ^^^^^^^^^^^^^^^^^^
482+
483+
error: casting `i32` to `u32` may lose the sign of the value
484+
--> $DIR/cast.rs:400:9
485+
|
486+
LL | (a * -b * c) as u32;
487+
| ^^^^^^^^^^^^^^^^^^^
488+
489+
error: casting `i32` to `u32` may lose the sign of the value
490+
--> $DIR/cast.rs:403:9
491+
|
492+
LL | (a * -2) as u32;
493+
| ^^^^^^^^^^^^^^^
494+
495+
error: casting `i32` to `u32` may lose the sign of the value
496+
--> $DIR/cast.rs:405:9
497+
|
498+
LL | (a * b * c * -2) as u32;
499+
| ^^^^^^^^^^^^^^^^^^^^^^^
500+
501+
error: casting `i32` to `u32` may lose the sign of the value
502+
--> $DIR/cast.rs:408:9
503+
|
504+
LL | (a / b * c) as u32;
505+
| ^^^^^^^^^^^^^^^^^^
506+
507+
error: casting `i32` to `u32` may lose the sign of the value
508+
--> $DIR/cast.rs:410:9
509+
|
510+
LL | (a / b + b * c) as u32;
511+
| ^^^^^^^^^^^^^^^^^^^^^^
512+
513+
error: casting `i32` to `u32` may lose the sign of the value
514+
--> $DIR/cast.rs:412:9
515+
|
516+
LL | a.pow(3) as u32;
517+
| ^^^^^^^^^^^^^^^
518+
519+
error: aborting due to 63 previous errors
448520

0 commit comments

Comments
 (0)