Skip to content

Commit 47444c4

Browse files
committed
Added lint for TryFrom for checked integer conversion #3947.
1 parent 60a609a commit 47444c4

File tree

6 files changed

+489
-0
lines changed

6 files changed

+489
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -846,6 +846,7 @@ All notable changes to this project will be documented in this file.
846846
[`char_lit_as_u8`]: https://rust-lang.github.io/rust-clippy/master/index.html#char_lit_as_u8
847847
[`chars_last_cmp`]: https://rust-lang.github.io/rust-clippy/master/index.html#chars_last_cmp
848848
[`chars_next_cmp`]: https://rust-lang.github.io/rust-clippy/master/index.html#chars_next_cmp
849+
[`checked_conversions`]: https://rust-lang.github.io/rust-clippy/master/index.html#checked_conversions
849850
[`clone_double_ref`]: https://rust-lang.github.io/rust-clippy/master/index.html#clone_double_ref
850851
[`clone_on_copy`]: https://rust-lang.github.io/rust-clippy/master/index.html#clone_on_copy
851852
[`clone_on_ref_ptr`]: https://rust-lang.github.io/rust-clippy/master/index.html#clone_on_ref_ptr
Lines changed: 332 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,332 @@
1+
//! lint on manually implemented checked conversions that could be transformed into try_from
2+
3+
use if_chain::if_chain;
4+
use rustc::hir::*;
5+
use rustc::lint::{in_external_macro, LateContext, LateLintPass, LintArray, LintContext, LintPass};
6+
use rustc::{declare_lint_pass, declare_tool_lint};
7+
use syntax::ast::LitKind;
8+
9+
use crate::utils::{span_lint, SpanlessEq};
10+
11+
declare_clippy_lint! {
12+
/// **What it does:** Checks for explicit bounds checking when casting.
13+
///
14+
/// **Why is this bad?** Reduces the readability of statements & is error prone.
15+
///
16+
/// **Known problems:** None.
17+
///
18+
/// **Example:**
19+
/// ```rust
20+
/// # let foo: u32 = 5;
21+
/// # let _ =
22+
/// foo <= i32::max_value() as u32
23+
/// # ;
24+
/// ```
25+
///
26+
/// Could be written:
27+
///
28+
/// ```rust
29+
/// # let _ =
30+
/// i32::try_from(foo).is_ok()
31+
/// # ;
32+
/// ```
33+
pub CHECKED_CONVERSIONS,
34+
pedantic,
35+
"`try_from` could replace manual bounds checking when casting"
36+
}
37+
38+
declare_lint_pass!(CheckedConversions => [CHECKED_CONVERSIONS]);
39+
40+
impl<'a, 'tcx> LateLintPass<'a, 'tcx> for CheckedConversions {
41+
fn check_expr(&mut self, cx: &LateContext<'_, '_>, item: &Expr) {
42+
let result = if_chain! {
43+
if !in_external_macro(cx.sess(), item.span);
44+
if let ExprKind::Binary(op, ref left, ref right) = &item.node;
45+
46+
then {
47+
match op.node {
48+
BinOpKind::Ge | BinOpKind::Le => single_check(item),
49+
BinOpKind::And => double_check(cx, left, right),
50+
_ => None,
51+
}
52+
} else {
53+
None
54+
}
55+
};
56+
57+
if let Some(cv) = result {
58+
span_lint(
59+
cx,
60+
CHECKED_CONVERSIONS,
61+
item.span,
62+
&format!(
63+
"Checked cast can be simplified: `{}::try_from`",
64+
cv.to_type.unwrap_or("IntegerType".to_string()),
65+
),
66+
);
67+
}
68+
}
69+
}
70+
71+
/// Searches for a single check from unsigned to _ is done
72+
/// todo: check for case signed -> larger unsigned == only x >= 0
73+
fn single_check(expr: &Expr) -> Option<Conversion<'_>> {
74+
check_upper_bound(expr).filter(|cv| cv.cvt == ConversionType::FromUnsigned)
75+
}
76+
77+
/// Searches for a combination of upper & lower bound checks
78+
fn double_check<'a>(cx: &LateContext<'_, '_>, left: &'a Expr, right: &'a Expr) -> Option<Conversion<'a>> {
79+
let upper_lower = |l, r| {
80+
let upper = check_upper_bound(l);
81+
let lower = check_lower_bound(r);
82+
83+
transpose(upper, lower).and_then(|(l, r)| l.combine(r, cx))
84+
};
85+
86+
upper_lower(left, right).or_else(|| upper_lower(right, left))
87+
}
88+
89+
/// Contains the result of a tried conversion check
90+
#[derive(Clone, Debug)]
91+
struct Conversion<'a> {
92+
cvt: ConversionType,
93+
expr_to_cast: &'a Expr,
94+
to_type: Option<String>,
95+
}
96+
97+
/// The kind of conversion that is checked
98+
#[derive(Copy, Clone, Debug, PartialEq)]
99+
enum ConversionType {
100+
SignedToUnsigned,
101+
SignedToSigned,
102+
FromUnsigned,
103+
}
104+
105+
impl<'a> Conversion<'a> {
106+
/// Combine multiple conversions if the are compatible
107+
pub fn combine(self, other: Self, cx: &LateContext<'_, '_>) -> Option<Conversion<'a>> {
108+
if self.is_compatible(&other, cx) {
109+
// Prefer a Conversion that contains a type-constraint
110+
Some(if self.to_type.is_some() { self } else { other })
111+
} else {
112+
None
113+
}
114+
}
115+
116+
/// Checks if two conversions are compatible
117+
/// same type of conversion, same 'castee' and same 'to type'
118+
pub fn is_compatible(&self, other: &Self, cx: &LateContext<'_, '_>) -> bool {
119+
(self.cvt == other.cvt)
120+
&& (SpanlessEq::new(cx).eq_expr(self.expr_to_cast, other.expr_to_cast))
121+
&& (self.has_compatible_to_type(other))
122+
}
123+
124+
/// Checks if the to-type is the same (if there is a type constraint)
125+
fn has_compatible_to_type(&self, other: &Self) -> bool {
126+
transpose(self.to_type.as_ref(), other.to_type.as_ref())
127+
.map(|(l, r)| l == r)
128+
.unwrap_or(true)
129+
}
130+
131+
/// Try to construct a new conversion if the conversion type is valid
132+
fn try_new<'b>(expr_to_cast: &'a Expr, from_type: &'b str, to_type: String) -> Option<Conversion<'a>> {
133+
ConversionType::try_new(from_type, &to_type).map(|cvt| Conversion {
134+
cvt,
135+
expr_to_cast,
136+
to_type: Some(to_type),
137+
})
138+
}
139+
140+
/// Construct a new conversion without type constraint
141+
fn new_any(expr_to_cast: &'a Expr) -> Conversion<'a> {
142+
Conversion {
143+
cvt: ConversionType::SignedToUnsigned,
144+
expr_to_cast,
145+
to_type: None,
146+
}
147+
}
148+
}
149+
150+
impl ConversionType {
151+
/// Creates a conversion type if the type is allowed & conversion is valid
152+
fn try_new(from: &str, to: &str) -> Option<ConversionType> {
153+
if UNSIGNED_TYPES.contains(&from) {
154+
Some(ConversionType::FromUnsigned)
155+
} else if SIGNED_TYPES.contains(&from) {
156+
if UNSIGNED_TYPES.contains(&to) {
157+
Some(ConversionType::SignedToUnsigned)
158+
} else if SIGNED_TYPES.contains(&to) {
159+
Some(ConversionType::SignedToSigned)
160+
} else {
161+
None
162+
}
163+
} else {
164+
None
165+
}
166+
}
167+
}
168+
169+
/// Check for `expr <= (to_type::max_value() as from_type)`
170+
fn check_upper_bound(expr: &Expr) -> Option<Conversion<'_>> {
171+
if_chain! {
172+
if let ExprKind::Binary(ref op, ref left, ref right) = &expr.node;
173+
if let Some((candidate, check)) = normalize_le_ge(op, left, right);
174+
if let Some((from, to)) = get_types_from_cast(check, "max_value", INT_TYPES);
175+
176+
then {
177+
Conversion::try_new(candidate, &from, to)
178+
} else {
179+
None
180+
}
181+
}
182+
}
183+
184+
/// Check for `expr >= 0|(to_type::min_value() as from_type)`
185+
fn check_lower_bound(expr: &Expr) -> Option<Conversion<'_>> {
186+
fn check_function<'a>(candidate: &'a Expr, check: &'a Expr) -> Option<Conversion<'a>> {
187+
(check_lower_bound_zero(candidate, check)).or_else(|| (check_lower_bound_min(candidate, check)))
188+
}
189+
190+
// First of we need a binary containing the expression & the cast
191+
if let ExprKind::Binary(ref op, ref left, ref right) = &expr.node {
192+
normalize_le_ge(op, right, left).and_then(|(l, r)| check_function(l, r))
193+
} else {
194+
None
195+
}
196+
}
197+
198+
/// Check for `expr >= 0`
199+
fn check_lower_bound_zero<'a>(candidate: &'a Expr, check: &'a Expr) -> Option<Conversion<'a>> {
200+
if_chain! {
201+
if let ExprKind::Lit(ref lit) = &check.node;
202+
if let LitKind::Int(0, _) = &lit.node;
203+
204+
then {
205+
Some(Conversion::new_any(candidate))
206+
} else {
207+
None
208+
}
209+
}
210+
}
211+
212+
/// Check for `expr >= (to_type::min_value() as from_type)`
213+
fn check_lower_bound_min<'a>(candidate: &'a Expr, check: &'a Expr) -> Option<Conversion<'a>> {
214+
if let Some((from, to)) = get_types_from_cast(check, "min_value", SIGNED_TYPES) {
215+
Conversion::try_new(candidate, &from, to)
216+
} else {
217+
None
218+
}
219+
}
220+
221+
/// Tries to extract the from- and to-type from a cast expression
222+
fn get_types_from_cast(expr: &Expr, func: &str, types: &[&str]) -> Option<(String, String)> {
223+
// `to_type::maxmin_value() as from_type`
224+
let call_from_cast: Option<(&Expr, String)> = if_chain! {
225+
// to_type::maxmin_value(), from_type
226+
if let ExprKind::Cast(ref limit, ref from_type) = &expr.node;
227+
if let TyKind::Path(ref from_type_path) = &from_type.node;
228+
if let Some(from_type_str) = int_ty_to_str(from_type_path);
229+
230+
then {
231+
Some((limit, from_type_str.to_string()))
232+
} else {
233+
None
234+
}
235+
};
236+
237+
// `from_type::from(to_type::maxmin_value())`
238+
let limit_from: Option<(&Expr, String)> = call_from_cast.or_else(|| {
239+
if_chain! {
240+
// `from_type::from, to_type::maxmin_value()`
241+
if let ExprKind::Call(ref from_func, ref args) = &expr.node;
242+
// `to_type::maxmin_value()`
243+
if args.len() == 1;
244+
if let limit = &args[0];
245+
// `from_type::from`
246+
if let ExprKind::Path(ref path) = &from_func.node;
247+
if let Some(from_type) = get_implementing_type(path, INT_TYPES, "from");
248+
249+
then {
250+
Some((limit, from_type))
251+
} else {
252+
None
253+
}
254+
}
255+
});
256+
257+
if let Some((limit, from_type)) = limit_from {
258+
if_chain! {
259+
if let ExprKind::Call(ref fun_name, _) = &limit.node;
260+
// `to_type, maxmin_value`
261+
if let ExprKind::Path(ref path) = &fun_name.node;
262+
// `to_type`
263+
if let Some(to_type) = get_implementing_type(path, types, func);
264+
265+
then {
266+
Some((from_type, to_type))
267+
} else {
268+
None
269+
}
270+
}
271+
} else {
272+
None
273+
}
274+
}
275+
276+
/// Gets the type which implements the called function
277+
fn get_implementing_type(path: &QPath, candidates: &[&str], function: &str) -> Option<String> {
278+
if_chain! {
279+
if let QPath::TypeRelative(ref ty, ref path) = &path;
280+
if path.ident.name == function;
281+
if let TyKind::Path(QPath::Resolved(None, ref tp)) = &ty.node;
282+
if let [int] = &*tp.segments;
283+
let name = int.ident.as_str().get();
284+
if candidates.contains(&name);
285+
286+
then {
287+
Some(name.to_string())
288+
} else {
289+
None
290+
}
291+
}
292+
}
293+
294+
/// Gets the type as a string, if it is a supported integer
295+
fn int_ty_to_str(path: &QPath) -> Option<&str> {
296+
if_chain! {
297+
if let QPath::Resolved(_, ref path) = *path;
298+
if let [ty] = &*path.segments;
299+
300+
then {
301+
INT_TYPES
302+
.into_iter()
303+
.find(|c| (&ty.ident.name) == *c)
304+
.cloned()
305+
} else {
306+
None
307+
}
308+
}
309+
}
310+
311+
/// (Option<T>, Option<U>) -> Option<(T, U)>
312+
fn transpose<T, U>(lhs: Option<T>, rhs: Option<U>) -> Option<(T, U)> {
313+
match (lhs, rhs) {
314+
(Some(l), Some(r)) => Some((l, r)),
315+
_ => None,
316+
}
317+
}
318+
319+
/// Will return the expressions as if they were expr1 <= expr2
320+
fn normalize_le_ge<'a>(op: &'a BinOp, left: &'a Expr, right: &'a Expr) -> Option<(&'a Expr, &'a Expr)> {
321+
match op.node {
322+
BinOpKind::Le => Some((left, right)),
323+
BinOpKind::Ge => Some((right, left)),
324+
_ => None,
325+
}
326+
}
327+
328+
const UNSIGNED_TYPES: &[&str] = &["u8", "u16", "u32", "u64", "u128", "usize"];
329+
const SIGNED_TYPES: &[&str] = &["i8", "i16", "i32", "i64", "i128", "isize"];
330+
const INT_TYPES: &[&str] = &[
331+
"u8", "u16", "u32", "u64", "u128", "usize", "i8", "i16", "i32", "i64", "i128", "isize",
332+
];

clippy_lints/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ pub mod block_in_if_condition;
154154
pub mod booleans;
155155
pub mod bytecount;
156156
pub mod cargo_common_metadata;
157+
pub mod checked_conversions;
157158
pub mod cognitive_complexity;
158159
pub mod collapsible_if;
159160
pub mod const_static_lifetime;
@@ -605,6 +606,7 @@ pub fn register_plugins(reg: &mut rustc_plugin::Registry<'_>, conf: &Conf) {
605606

606607
reg.register_lint_group("clippy::pedantic", Some("clippy_pedantic"), vec![
607608
attrs::INLINE_ALWAYS,
609+
checked_conversions::CHECKED_CONVERSIONS,
608610
copies::MATCH_SAME_ARMS,
609611
copy_iterator::COPY_ITERATOR,
610612
default_trait_access::DEFAULT_TRAIT_ACCESS,

0 commit comments

Comments
 (0)