| use clippy_utils::diagnostics::span_lint_hir_and_then; | |
| use clippy_utils::numeric_literal; | |
| use clippy_utils::source::snippet_opt; | |
| use rustc_ast::ast::{LitFloatType, LitIntType, LitKind}; | |
| use rustc_errors::Applicability; | |
| use rustc_hir::intravisit::{Visitor, walk_expr, walk_pat, walk_stmt}; | |
| use rustc_hir::{ | |
| Block, Body, ConstContext, Expr, ExprKind, FnRetTy, HirId, Lit, Pat, PatExpr, PatExprKind, PatKind, Stmt, StmtKind, | |
| StructTailExpr, | |
| }; | |
| use rustc_lint::{LateContext, LateLintPass, LintContext as _, declare_lint_pass}; | |
| use rustc_middle::ty::{self, FloatTy, IntTy, PolyFnSig, Ty}; | |
| use std::iter; | |
| 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 restrict this? | |
| /// To ensure that every numeric type is chosen explicitly rather than implicitly. | |
| /// | |
| /// ### Known problems | |
| /// This lint is implemented using a custom algorithm independent of rustc's inference, | |
| /// which results in many false positives and false negatives. | |
| /// | |
| /// ### Example | |
| /// ```no_run | |
| /// let i = 10; | |
| /// let f = 1.23; | |
| /// ``` | |
| /// | |
| /// Use instead: | |
| /// ```no_run | |
| /// let i = 10_i32; | |
| /// let f = 1.23_f64; | |
| /// ``` | |
| #[clippy::version = "1.52.0"] | |
| pub DEFAULT_NUMERIC_FALLBACK, | |
| restriction, | |
| "usage of unconstrained numeric literals which may cause default numeric fallback." | |
| } | |
| declare_lint_pass!(DefaultNumericFallback => [DEFAULT_NUMERIC_FALLBACK]); | |
| impl<'tcx> LateLintPass<'tcx> for DefaultNumericFallback { | |
| fn check_body(&mut self, cx: &LateContext<'tcx>, body: &Body<'tcx>) { | |
| // NOTE: this is different from `clippy_utils::is_inside_always_const_context`. | |
| // Inline const supports type inference. | |
| let is_parent_const = matches!( | |
| cx.tcx.hir_body_const_context(cx.tcx.hir_body_owner_def_id(body.id())), | |
| Some( | |
| ConstContext::Const { | |
| allow_const_fn_promotion: true | |
| } | ConstContext::Static(_) | |
| ) | |
| ); | |
| let mut visitor = NumericFallbackVisitor::new(cx, is_parent_const); | |
| visitor.visit_body(body); | |
| } | |
| } | |
| struct NumericFallbackVisitor<'a, 'tcx> { | |
| /// Stack manages type bound of exprs. The top element holds current expr type. | |
| ty_bounds: Vec<ExplicitTyBound>, | |
| cx: &'a LateContext<'tcx>, | |
| } | |
| impl<'a, 'tcx> NumericFallbackVisitor<'a, 'tcx> { | |
| fn new(cx: &'a LateContext<'tcx>, is_parent_const: bool) -> Self { | |
| Self { | |
| ty_bounds: vec![if is_parent_const { | |
| ExplicitTyBound(true) | |
| } else { | |
| ExplicitTyBound(false) | |
| }], | |
| cx, | |
| } | |
| } | |
| /// Check whether a passed literal has potential to cause fallback or not. | |
| fn check_lit(&self, lit: Lit, lit_ty: Ty<'tcx>, emit_hir_id: HirId) { | |
| if matches!(self.ty_bounds.last(), Some(ExplicitTyBound(false))) | |
| && matches!( | |
| lit.node, | |
| LitKind::Int(_, LitIntType::Unsuffixed) | LitKind::Float(_, LitFloatType::Unsuffixed) | |
| ) | |
| && !lit.span.in_external_macro(self.cx.sess().source_map()) | |
| { | |
| let (suffix, is_float) = match lit_ty.kind() { | |
| ty::Int(IntTy::I32) => ("i32", false), | |
| ty::Float(FloatTy::F64) => ("f64", true), | |
| _ => return, | |
| }; | |
| span_lint_hir_and_then( | |
| self.cx, | |
| DEFAULT_NUMERIC_FALLBACK, | |
| emit_hir_id, | |
| lit.span, | |
| "default numeric fallback might occur", | |
| |diag| { | |
| let src = if let Some(src) = snippet_opt(self.cx, lit.span) { | |
| src | |
| } else { | |
| match lit.node { | |
| LitKind::Int(src, _) => format!("{src}"), | |
| LitKind::Float(src, _) => format!("{src}"), | |
| _ => unreachable!("Default numeric fallback never results in other types"), | |
| } | |
| }; | |
| let sugg = numeric_literal::format(&src, Some(suffix), is_float); | |
| diag.span_suggestion(lit.span, "consider adding suffix", sugg, Applicability::MaybeIncorrect); | |
| }, | |
| ); | |
| } | |
| } | |
| } | |
| impl<'tcx> Visitor<'tcx> for NumericFallbackVisitor<'_, 'tcx> { | |
| fn visit_expr(&mut self, expr: &'tcx Expr<'_>) { | |
| match &expr.kind { | |
| ExprKind::Block( | |
| Block { | |
| stmts, expr: Some(_), .. | |
| }, | |
| _, | |
| ) => { | |
| if let Some(fn_sig) = self.cx.tcx.parent_hir_node(expr.hir_id).fn_sig() | |
| && let FnRetTy::Return(_ty) = fn_sig.decl.output | |
| { | |
| // We cannot check the exact type since it's a `hir::Ty` which does not implement `is_numeric` | |
| self.ty_bounds.push(ExplicitTyBound(true)); | |
| for stmt in *stmts { | |
| self.visit_stmt(stmt); | |
| } | |
| self.ty_bounds.pop(); | |
| // Ignore return expr since we know its type was inferred from return ty | |
| return; | |
| } | |
| }, | |
| // Ignore return expr since we know its type was inferred from return ty | |
| ExprKind::Ret(_) => return, | |
| ExprKind::Call(func, args) => { | |
| if let Some(fn_sig) = fn_sig_opt(self.cx, func.hir_id) { | |
| for (expr, bound) in iter::zip(*args, fn_sig.skip_binder().inputs()) { | |
| // If is from macro, try to use last bound type (typically pushed when visiting stmt), | |
| // otherwise push found arg type, then visit arg, | |
| if expr.span.from_expansion() { | |
| self.visit_expr(expr); | |
| } else { | |
| self.ty_bounds.push((*bound).into()); | |
| self.visit_expr(expr); | |
| self.ty_bounds.pop(); | |
| } | |
| } | |
| return; | |
| } | |
| }, | |
| ExprKind::MethodCall(_, receiver, 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) | |
| .instantiate_identity() | |
| .skip_norm_wip() | |
| .skip_binder(); | |
| for (expr, bound) in iter::zip(iter::once(*receiver).chain(args.iter()), fn_sig.inputs()) { | |
| self.ty_bounds.push((*bound).into()); | |
| self.visit_expr(expr); | |
| self.ty_bounds.pop(); | |
| } | |
| return; | |
| } | |
| }, | |
| ExprKind::Struct(_, fields, base) => { | |
| let ty = self.cx.typeck_results().expr_ty(expr); | |
| if let Some(adt_def) = ty.ty_adt_def() | |
| && adt_def.is_struct() | |
| && let Some(variant) = adt_def.variants().iter().next() | |
| { | |
| let fields_def = &variant.fields; | |
| // Push field type then visit each field expr. | |
| for field in *fields { | |
| let bound = fields_def.iter().find_map(|f_def| { | |
| if f_def.ident(self.cx.tcx) == field.ident { | |
| Some(self.cx.tcx.type_of(f_def.did).instantiate_identity().skip_norm_wip()) | |
| } else { | |
| None | |
| } | |
| }); | |
| self.ty_bounds.push(bound.into()); | |
| self.visit_expr(field.expr); | |
| self.ty_bounds.pop(); | |
| } | |
| // Visit base with no bound. | |
| if let StructTailExpr::Base(base) = base { | |
| self.ty_bounds.push(ExplicitTyBound(false)); | |
| self.visit_expr(base); | |
| self.ty_bounds.pop(); | |
| } | |
| return; | |
| } | |
| }, | |
| ExprKind::Lit(lit) => { | |
| let ty = self.cx.typeck_results().expr_ty(expr); | |
| self.check_lit(*lit, ty, expr.hir_id); | |
| return; | |
| }, | |
| _ => {}, | |
| } | |
| walk_expr(self, expr); | |
| } | |
| fn visit_pat(&mut self, pat: &'tcx Pat<'_>) { | |
| if let PatKind::Expr(&PatExpr { | |
| hir_id, | |
| kind: PatExprKind::Lit { lit, .. }, | |
| .. | |
| }) = pat.kind | |
| { | |
| let ty = self.cx.typeck_results().node_type(hir_id); | |
| self.check_lit(lit, ty, hir_id); | |
| return; | |
| } | |
| walk_pat(self, pat); | |
| } | |
| fn visit_stmt(&mut self, stmt: &'tcx Stmt<'_>) { | |
| match stmt.kind { | |
| // we cannot check the exact type since it's a hir::Ty which does not implement `is_numeric` | |
| StmtKind::Let(local) => self.ty_bounds.push(ExplicitTyBound(local.ty.is_some())), | |
| _ => self.ty_bounds.push(ExplicitTyBound(false)), | |
| } | |
| walk_stmt(self, stmt); | |
| self.ty_bounds.pop(); | |
| } | |
| } | |
| fn fn_sig_opt<'tcx>(cx: &LateContext<'tcx>, hir_id: HirId) -> Option<PolyFnSig<'tcx>> { | |
| let node_ty = cx.typeck_results().node_type_opt(hir_id)?; | |
| // We can't use `Ty::fn_sig` because it automatically performs args, this may result in FNs. | |
| match node_ty.kind() { | |
| ty::FnDef(def_id, _) => Some(cx.tcx.fn_sig(*def_id).instantiate_identity().skip_norm_wip()), | |
| ty::FnPtr(sig_tys, hdr) => Some(sig_tys.with(*hdr)), | |
| _ => None, | |
| } | |
| } | |
| /// Wrapper around a `bool` to make the meaning of the value clearer | |
| #[derive(Debug, Clone, Copy)] | |
| struct ExplicitTyBound(pub bool); | |
| impl<'tcx> From<Ty<'tcx>> for ExplicitTyBound { | |
| fn from(v: Ty<'tcx>) -> Self { | |
| Self(v.is_numeric()) | |
| } | |
| } | |
| impl<'tcx> From<Option<Ty<'tcx>>> for ExplicitTyBound { | |
| fn from(v: Option<Ty<'tcx>>) -> Self { | |
| Self(v.is_some_and(Ty::is_numeric)) | |
| } | |
| } |