diff options
| author | Yoshitomo Nakanishi <yurayura.rounin.3@gmail.com> | 2021-02-02 12:26:20 +0900 |
|---|---|---|
| committer | Yoshitomo Nakanishi <yurayura.rounin.3@gmail.com> | 2021-02-04 17:26:43 +0900 |
| commit | e32e4dedf1781a4696c34f31d69e68c7c0eaf6a9 (patch) | |
| tree | 136ddfa6acbeee4e59544d18c6c5a748f7a7c937 | |
| parent | 876ffa4674c5d304983ff4d9bfeaffadee2cbb37 (diff) | |
| download | rust-e32e4dedf1781a4696c34f31d69e68c7c0eaf6a9.tar.gz rust-e32e4dedf1781a4696c34f31d69e68c7c0eaf6a9.zip | |
New lint: default_numeric_fallback
| -rw-r--r-- | CHANGELOG.md | 1 | ||||
| -rw-r--r-- | clippy_lints/src/default_numeric_fallback.rs | 388 | ||||
| -rw-r--r-- | clippy_lints/src/lib.rs | 4 | ||||
| -rw-r--r-- | tests/ui/default_numeric_fallback.rs | 99 | ||||
| -rw-r--r-- | tests/ui/default_numeric_fallback.stderr | 187 |
5 files changed, 679 insertions, 0 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index c1032204a22..5eed9664088 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1909,6 +1909,7 @@ Released 2018-09-13 [`debug_assert_with_mut_call`]: https://rust-lang.github.io/rust-clippy/master/index.html#debug_assert_with_mut_call [`decimal_literal_representation`]: https://rust-lang.github.io/rust-clippy/master/index.html#decimal_literal_representation [`declare_interior_mutable_const`]: https://rust-lang.github.io/rust-clippy/master/index.html#declare_interior_mutable_const +[`default_numeric_fallback`]: https://rust-lang.github.io/rust-clippy/master/index.html#default_numeric_fallback [`default_trait_access`]: https://rust-lang.github.io/rust-clippy/master/index.html#default_trait_access [`deprecated_cfg_attr`]: https://rust-lang.github.io/rust-clippy/master/index.html#deprecated_cfg_attr [`deprecated_semver`]: https://rust-lang.github.io/rust-clippy/master/index.html#deprecated_semver diff --git a/clippy_lints/src/default_numeric_fallback.rs b/clippy_lints/src/default_numeric_fallback.rs new file mode 100644 index 00000000000..f049e64d0fb --- /dev/null +++ b/clippy_lints/src/default_numeric_fallback.rs @@ -0,0 +1,388 @@ +use rustc_ast::ast::{Label, LitFloatType, LitIntType, LitKind}; +use rustc_hir::{ + self as hir, + intravisit::{walk_expr, walk_stmt, walk_ty, FnKind, NestedVisitorMap, Visitor}, + Body, Expr, ExprKind, FnDecl, FnRetTy, Guard, HirId, Lit, Stmt, StmtKind, +}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::{ + hir::map::Map, + ty::{self, subst::GenericArgKind, FloatTy, IntTy, Ty, TyCtxt}, +}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::Span; +use rustc_typeck::hir_ty_to_ty; + +use if_chain::if_chain; + +use crate::utils::span_lint_and_help; + +declare_clippy_lint! { + /// **What it does:** Checks for usage of unconstrained numeric literals which may cause default numeric fallback in type + /// inference. + /// + /// Default numeric fallback means that if numeric types have not yet been bound to concrete + /// types at the end of type inference, then integer type is bound to `i32`, and similarly + /// floating type is bound to `f64`. + /// + /// See [RFC0212](https://github.com/rust-lang/rfcs/blob/master/text/0212-restore-int-fallback.md) for more information about the fallback. + /// + /// **Why is this bad?** For those who are very careful about types, default numeric fallback + /// can be a pitfall that cause unexpected runtime behavior. + /// + /// **Known problems:** None. + /// + /// **Example:** + /// ```rust + /// let i = 10; + /// let f = 1.23; + /// ``` + /// + /// Use instead: + /// ```rust + /// let i = 10i32; + /// let f = 1.23f64; + /// ``` + pub DEFAULT_NUMERIC_FALLBACK, + restriction, + "usage of unconstrained numeric literals which may cause default numeric fallback." +} + +declare_lint_pass!(DefaultNumericFallback => [DEFAULT_NUMERIC_FALLBACK]); + +fn enclosing_body_owner_opt(tcx: TyCtxt<'_>, hir_id: HirId) -> Option<HirId> { + let hir_map = tcx.hir(); + for (parent, _) in hir_map.parent_iter(hir_id) { + if let Some(body) = hir_map.maybe_body_owned_by(parent) { + return Some(hir_map.body_owner(body)); + } + } + None +} + +impl LateLintPass<'_> for DefaultNumericFallback { + fn check_fn( + &mut self, + cx: &LateContext<'tcx>, + _: FnKind<'tcx>, + fn_decl: &'tcx FnDecl<'_>, + body: &'tcx Body<'_>, + _: Span, + hir_id: HirId, + ) { + let ret_ty_bound = match fn_decl.output { + FnRetTy::DefaultReturn(_) => None, + FnRetTy::Return(ty) => Some(ty), + } + .and_then(|ty| { + let mut infer_ty_finder = InferTyFinder::new(); + infer_ty_finder.visit_ty(ty); + if infer_ty_finder.found { + None + } else if enclosing_body_owner_opt(cx.tcx, hir_id).is_some() { + cx.typeck_results().node_type_opt(ty.hir_id) + } else { + Some(hir_ty_to_ty(cx.tcx, ty)) + } + }); + + let mut visitor = NumericFallbackVisitor::new(ret_ty_bound, cx); + visitor.visit_body(body); + } +} + +struct NumericFallbackVisitor<'a, 'tcx> { + /// Stack manages type bound of exprs. The top element holds current expr type. + ty_bounds: Vec<Option<Ty<'tcx>>>, + + /// Ret type bound. + ret_ty_bound: Option<Ty<'tcx>>, + + /// Break type bounds. + break_ty_bounds: Vec<(Option<Label>, Option<Ty<'tcx>>)>, + + cx: &'a LateContext<'tcx>, +} + +impl<'a, 'tcx> NumericFallbackVisitor<'a, 'tcx> { + fn new(ret_ty_bound: Option<Ty<'tcx>>, cx: &'a LateContext<'tcx>) -> Self { + Self { + ty_bounds: vec![ret_ty_bound], + ret_ty_bound, + break_ty_bounds: vec![], + cx, + } + } + + /// Check whether lit cause fallback or not. + fn check_lit(&self, lit: &Lit, lit_ty: Ty<'tcx>) { + let ty_bound = self.ty_bounds.last().unwrap(); + + let should_lint = match (&lit.node, lit_ty.kind()) { + (LitKind::Int(_, LitIntType::Unsuffixed), ty::Int(ty::IntTy::I32)) => { + // In case integer literal is explicitly bound to i32, then suppress lint. + ty_bound.map_or(true, |ty_bound| !matches!(ty_bound.kind(), ty::Int(IntTy::I32))) + }, + + (LitKind::Float(_, LitFloatType::Unsuffixed), ty::Float(ty::FloatTy::F64)) => { + // In case float literal is explicitly bound to f64, then suppress lint. + ty_bound.map_or(true, |ty_bound| !matches!(ty_bound.kind(), ty::Float(FloatTy::F64))) + }, + + _ => false, + }; + + if should_lint { + span_lint_and_help( + self.cx, + DEFAULT_NUMERIC_FALLBACK, + lit.span, + "default numeric fallback might occur", + None, + "consider adding suffix to avoid default numeric fallback", + ); + } + } +} + +impl<'a, 'tcx> Visitor<'tcx> for NumericFallbackVisitor<'a, 'tcx> { + type Map = Map<'tcx>; + + #[allow(clippy::too_many_lines)] + fn visit_expr(&mut self, expr: &'tcx Expr<'_>) { + match (&expr.kind, *self.ty_bounds.last().unwrap()) { + (ExprKind::Array(_), Some(last_bound)) => { + if let ty::Array(ty, _) = last_bound.kind() { + self.ty_bounds.push(Some(ty)) + } else { + self.ty_bounds.push(None) + } + }, + + (ExprKind::Call(func, args), _) => { + if_chain! { + if let ExprKind::Path(ref func_path) = func.kind; + if let Some(def_id) = self.cx.qpath_res(func_path, func.hir_id).opt_def_id(); + then { + let fn_sig = self.cx.tcx.fn_sig(def_id).skip_binder(); + for (expr, bound) in args.iter().zip(fn_sig.inputs().iter()) { + // Push found arg type, then visit arg. + self.ty_bounds.push(Some(bound)); + self.visit_expr(expr); + self.ty_bounds.pop(); + } + return; + } else { + self.ty_bounds.push(None) + } + } + }, + + (ExprKind::MethodCall(_, _, args, _), _) => { + if let Some(def_id) = self.cx.typeck_results().type_dependent_def_id(expr.hir_id) { + let fn_sig = self.cx.tcx.fn_sig(def_id).skip_binder(); + for (expr, bound) in args.iter().zip(fn_sig.inputs().iter()) { + self.ty_bounds.push(Some(bound)); + self.visit_expr(expr); + self.ty_bounds.pop(); + } + return; + } + + self.ty_bounds.push(None) + }, + + (ExprKind::Tup(exprs), Some(last_bound)) => { + if let ty::Tuple(tys) = last_bound.kind() { + for (expr, bound) in exprs.iter().zip(tys.iter()) { + if let GenericArgKind::Type(ty) = bound.unpack() { + self.ty_bounds.push(Some(ty)); + } else { + self.ty_bounds.push(None); + } + + self.visit_expr(expr); + self.ty_bounds.pop(); + } + return; + } + + self.ty_bounds.push(None) + }, + + (ExprKind::Lit(lit), _) => { + let ty = self.cx.typeck_results().expr_ty(expr); + self.check_lit(lit, ty); + return; + }, + + (ExprKind::If(cond, then, else_), last_bound) => { + // Cond has no type bound in any situation. + self.ty_bounds.push(None); + self.visit_expr(cond); + self.ty_bounds.pop(); + + // Propagate current bound to childs. + self.ty_bounds.push(last_bound); + self.visit_expr(then); + if let Some(else_) = else_ { + self.visit_expr(else_); + } + self.ty_bounds.pop(); + return; + }, + + (ExprKind::Loop(_, label, ..), last_bound) => { + self.break_ty_bounds.push((*label, last_bound)); + walk_expr(self, expr); + self.break_ty_bounds.pop(); + return; + }, + + (ExprKind::Match(arg, arms, _), last_bound) => { + // Match argument has no type bound. + self.ty_bounds.push(None); + self.visit_expr(arg); + for arm in arms.iter() { + self.visit_pat(arm.pat); + if let Some(Guard::If(guard)) = arm.guard { + self.visit_expr(guard); + } + } + self.ty_bounds.pop(); + + // Propagate current bound. + self.ty_bounds.push(last_bound); + for arm in arms.iter() { + self.visit_expr(arm.body); + } + self.ty_bounds.pop(); + return; + }, + + (ExprKind::Block(..), last_bound) => self.ty_bounds.push(last_bound), + + (ExprKind::Break(destination, _), _) => { + let ty = destination.label.map_or_else( + || self.break_ty_bounds.last().unwrap().1, + |dest_label| { + self.break_ty_bounds + .iter() + .rev() + .find_map(|(loop_label, ty)| { + loop_label.map_or(None, |loop_label| { + if loop_label.ident == dest_label.ident { + Some(*ty) + } else { + None + } + }) + }) + .unwrap() + }, + ); + self.ty_bounds.push(ty); + }, + + (ExprKind::Ret(_), _) => self.ty_bounds.push(self.ret_ty_bound), + + (ExprKind::Struct(qpath, fields, base), _) => { + if_chain! { + if let Some(def_id) = self.cx.qpath_res(qpath, expr.hir_id).opt_def_id(); + let ty = self.cx.tcx.type_of(def_id); + if let Some(adt_def) = ty.ty_adt_def(); + if adt_def.is_struct(); + if let Some(variant) = adt_def.variants.iter().next(); + then { + let fields_def = &variant.fields; + + // Push field type then visit each field expr. + for field in fields.iter() { + let field_ty = + fields_def + .iter() + .find_map(|f_def| { + if f_def.ident == field.ident + { Some(self.cx.tcx.type_of(f_def.did)) } + else { None } + }); + self.ty_bounds.push(field_ty); + self.visit_expr(field.expr); + self.ty_bounds.pop(); + } + + // Visit base with no bound. + if let Some(base) = base { + self.ty_bounds.push(None); + self.visit_expr(base); + self.ty_bounds.pop(); + } + return; + } + } + self.ty_bounds.push(None); + }, + + _ => self.ty_bounds.push(None), + } + + walk_expr(self, expr); + self.ty_bounds.pop(); + } + + fn visit_stmt(&mut self, stmt: &'tcx Stmt<'_>) { + match stmt.kind { + StmtKind::Local(local) => { + let ty = local.ty.and_then(|hir_ty| { + let mut infer_ty_finder = InferTyFinder::new(); + infer_ty_finder.visit_ty(hir_ty); + if infer_ty_finder.found { + None + } else { + self.cx.typeck_results().node_type_opt(hir_ty.hir_id) + } + }); + self.ty_bounds.push(ty); + }, + + _ => self.ty_bounds.push(None), + } + + walk_stmt(self, stmt); + self.ty_bounds.pop(); + } + + fn nested_visit_map(&mut self) -> NestedVisitorMap<Self::Map> { + NestedVisitorMap::None + } +} + +/// Find `hir::TyKind::Infer` is included in passed typed. +struct InferTyFinder { + found: bool, +} + +impl InferTyFinder { + fn new() -> Self { + Self { found: false } + } +} + +impl<'tcx> Visitor<'tcx> for InferTyFinder { + type Map = Map<'tcx>; + + fn visit_ty(&mut self, ty: &'tcx hir::Ty<'_>) { + match ty.kind { + hir::TyKind::Infer => { + self.found = true; + }, + _ => { + walk_ty(self, ty); + }, + } + } + + fn nested_visit_map(&mut self) -> NestedVisitorMap<Self::Map> { + NestedVisitorMap::None + } +} diff --git a/clippy_lints/src/lib.rs b/clippy_lints/src/lib.rs index 5a40c00bd67..642c1e68dac 100644 --- a/clippy_lints/src/lib.rs +++ b/clippy_lints/src/lib.rs @@ -181,6 +181,7 @@ mod copy_iterator; mod create_dir; mod dbg_macro; mod default; +mod default_numeric_fallback; mod dereference; mod derive; mod disallowed_method; @@ -584,6 +585,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf: &dbg_macro::DBG_MACRO, &default::DEFAULT_TRAIT_ACCESS, &default::FIELD_REASSIGN_WITH_DEFAULT, + &default_numeric_fallback::DEFAULT_NUMERIC_FALLBACK, &dereference::EXPLICIT_DEREF_METHODS, &derive::DERIVE_HASH_XOR_EQ, &derive::DERIVE_ORD_XOR_PARTIAL_ORD, @@ -1026,6 +1028,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf: store.register_late_pass(|| box strings::StringAdd); store.register_late_pass(|| box implicit_return::ImplicitReturn); store.register_late_pass(|| box implicit_saturating_sub::ImplicitSaturatingSub); + store.register_late_pass(|| box default_numeric_fallback::DefaultNumericFallback); let msrv = conf.msrv.as_ref().and_then(|s| { parse_msrv(s, None, None).or_else(|| { @@ -1258,6 +1261,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf: LintId::of(&asm_syntax::INLINE_ASM_X86_INTEL_SYNTAX), LintId::of(&create_dir::CREATE_DIR), LintId::of(&dbg_macro::DBG_MACRO), + LintId::of(&default_numeric_fallback::DEFAULT_NUMERIC_FALLBACK), LintId::of(&else_if_without_else::ELSE_IF_WITHOUT_ELSE), LintId::of(&exhaustive_items::EXHAUSTIVE_ENUMS), LintId::of(&exhaustive_items::EXHAUSTIVE_STRUCTS), diff --git a/tests/ui/default_numeric_fallback.rs b/tests/ui/default_numeric_fallback.rs new file mode 100644 index 00000000000..90ee8ea0399 --- /dev/null +++ b/tests/ui/default_numeric_fallback.rs @@ -0,0 +1,99 @@ +#![warn(clippy::default_numeric_fallback)] +#![allow(unused)] +#![allow(clippy::never_loop)] +#![allow(clippy::no_effect)] +#![allow(clippy::unnecessary_operation)] + +fn ret_i31() -> i32 { + 23 +} + +fn concrete_arg(x: i32) {} + +fn generic_arg<T>(t: T) {} + +struct ConcreteStruct { + x: i32, +} + +struct GenericStruct<T> { + x: T, +} + +struct StructForMethodCallTest { + x: i32, +} + +impl StructForMethodCallTest { + fn concrete_arg(&self, x: i32) {} + + fn generic_arg<T>(&self, t: T) {} +} + +fn main() { + let s = StructForMethodCallTest { x: 10_i32 }; + + // Bad. + let x = 22; + let x = 0.12; + let x: _ = 13; + let x: [_; 3] = [1, 2, 3]; + let x: (_, i32) = (1, 2); + + let x = if true { (1, 2) } else { (3, 4) }; + + let x = match 1 { + 1 => 1, + _ => 2, + }; + + let x = loop { + break 1; + }; + + let x = 'outer0: loop { + { + 'inner0: loop { + break 3; + } + }; + break 2; + }; + + let x = GenericStruct { x: 1 }; + + generic_arg(10); + s.generic_arg(10); + let f = || -> _ { 1 }; + + // Good. + let x = 22_i32; + let x: f64 = 0.12; + let x = 0.12_f64; + let x: i32 = 13; + let x: [i32; 3] = [1, 2, 3]; + let x: (i32, i32) = (1, 2); + + let x: (i32, i32) = if true { (1, 2) } else { (3, 4) }; + + let x: i32 = match true { + true => 1, + _ => 2, + }; + + let x: i32 = loop { + break 1; + }; + + let x: i32 = 'outer1: loop { + 'inner1: loop { + break 'outer1 3; + } + }; + + let x = ConcreteStruct { x: 1 }; + + concrete_arg(10); + s.concrete_arg(10); + let f = || -> i32 { 1 }; +} diff --git a/tests/ui/default_numeric_fallback.stderr b/tests/ui/default_numeric_fallback.stderr new file mode 100644 index 00000000000..27629b994be --- /dev/null +++ b/tests/ui/default_numeric_fallback.stderr @@ -0,0 +1,187 @@ +error: default numeric fallback might occur + --> $DIR/default_numeric_fallback.rs:37:13 + | +LL | let x = 22; + | ^^ + | + = note: `-D clippy::default-numeric-fallback` implied by `-D warnings` + = help: consider adding suffix to avoid default numeric fallback + +error: default numeric fallback might occur + --> $DIR/default_numeric_fallback.rs:38:13 + | +LL | let x = 0.12; + | ^^^^ + | + = help: consider adding suffix to avoid default numeric fallback + +error: default numeric fallback might occur + --> $DIR/default_numeric_fallback.rs:39:16 + | +LL | let x: _ = 13; + | ^^ + | + = help: consider adding suffix to avoid default numeric fallback + +error: default numeric fallback might occur + --> $DIR/default_numeric_fallback.rs:40:22 + | +LL | let x: [_; 3] = [1, 2, 3]; + | ^ + | + = help: consider adding suffix to avoid default numeric fallback + +error: default numeric fallback might occur + --> $DIR/default_numeric_fallback.rs:40:25 + | +LL | let x: [_; 3] = [1, 2, 3]; + | ^ + | + = help: consider adding suffix to avoid default numeric fallback + +error: default numeric fallback might occur + --> $DIR/default_numeric_fallback.rs:40:28 + | +LL | let x: [_; 3] = [1, 2, 3]; + | ^ + | + = help: consider adding suffix to avoid default numeric fallback + +error: default numeric fallback might occur + --> $DIR/default_numeric_fallback.rs:41:24 + | +LL | let x: (_, i32) = (1, 2); + | ^ + | + = help: consider adding suffix to avoid default numeric fallback + +error: default numeric fallback might occur + --> $DIR/default_numeric_fallback.rs:41:27 + | +LL | let x: (_, i32) = (1, 2); + | ^ + | + = help: consider adding suffix to avoid default numeric fallback + +error: default numeric fallback might occur + --> $DIR/default_numeric_fallback.rs:43:24 + | +LL | let x = if true { (1, 2) } else { (3, 4) }; + | ^ + | + = help: consider adding suffix to avoid default numeric fallback + +error: default numeric fallback might occur + --> $DIR/default_numeric_fallback.rs:43:27 + | +LL | let x = if true { (1, 2) } else { (3, 4) }; + | ^ + | + = help: consider adding suffix to avoid default numeric fallback + +error: default numeric fallback might occur + --> $DIR/default_numeric_fallback.rs:43:40 + | +LL | let x = if true { (1, 2) } else { (3, 4) }; + | ^ + | + = help: consider adding suffix to avoid default numeric fallback + +error: default numeric fallback might occur + --> $DIR/default_numeric_fallback.rs:43:43 + | +LL | let x = if true { (1, 2) } else { (3, 4) }; + | ^ + | + = help: consider adding suffix to avoid default numeric fallback + +error: default numeric fallback might occur + --> $DIR/default_numeric_fallback.rs:45:19 + | +LL | let x = match 1 { + | ^ + | + = help: consider adding suffix to avoid default numeric fallback + +error: default numeric fallback might occur + --> $DIR/default_numeric_fallback.rs:46:9 + | +LL | 1 => 1, + | ^ + | + = help: consider adding suffix to avoid default numeric fallback + +error: default numeric fallback might occur + --> $DIR/default_numeric_fallback.rs:46:14 + | +LL | 1 => 1, + | ^ + | + = help: consider adding suffix to avoid default numeric fallback + +error: default numeric fallback might occur + --> $DIR/default_numeric_fallback.rs:47:14 + | +LL | _ => 2, + | ^ + | + = help: consider adding suffix to avoid default numeric fallback + +error: default numeric fallback might occur + --> $DIR/default_numeric_fallback.rs:51:15 + | +LL | break 1; + | ^ + | + = help: consider adding suffix to avoid default numeric fallback + +error: default numeric fallback might occur + --> $DIR/default_numeric_fallback.rs:57:23 + | +LL | break 3; + | ^ + | + = help: consider adding suffix to avoid default numeric fallback + +error: default numeric fallback might occur + --> $DIR/default_numeric_fallback.rs:60:15 + | +LL | break 2; + | ^ + | + = help: consider adding suffix to avoid default numeric fallback + +error: default numeric fallback might occur + --> $DIR/default_numeric_fallback.rs:63:32 + | +LL | let x = GenericStruct { x: 1 }; + | ^ + | + = help: consider adding suffix to avoid default numeric fallback + +error: default numeric fallback might occur + --> $DIR/default_numeric_fallback.rs:65:17 + | +LL | generic_arg(10); + | ^^ + | + = help: consider adding suffix to avoid default numeric fallback + +error: default numeric fallback might occur + --> $DIR/default_numeric_fallback.rs:66:19 + | +LL | s.generic_arg(10); + | ^^ + | + = help: consider adding suffix to avoid default numeric fallback + +error: default numeric fallback might occur + --> $DIR/default_numeric_fallback.rs:67:23 + | +LL | let f = || -> _ { 1 }; + | ^ + | + = help: consider adding suffix to avoid default numeric fallback + +error: aborting due to 23 previous errors + |
