| use clippy_config::Conf; | |
| use clippy_utils::diagnostics::span_lint_and_then; | |
| use clippy_utils::{ExprUseNode, expr_use_ctxt, numeric_literal}; | |
| use rustc_ast::ast::{LitFloatType, LitKind}; | |
| use rustc_errors::Applicability; | |
| use rustc_hir as hir; | |
| use rustc_lint::{LateContext, LateLintPass}; | |
| use rustc_middle::ty::{self, FloatTy}; | |
| use rustc_session::impl_lint_pass; | |
| use std::fmt; | |
| declare_clippy_lint! { | |
| /// ### What it does | |
| /// Checks for float literals with a precision greater | |
| /// than that supported by the underlying type. | |
| /// | |
| /// The lint is suppressed for literals with over `const_literal_digits_threshold` digits. | |
| /// | |
| /// ### Why is this bad? | |
| /// Rust will truncate the literal silently. | |
| /// | |
| /// ### Example | |
| /// ```no_run | |
| /// let v: f32 = 0.123_456_789_9; | |
| /// println!("{}", v); // 0.123_456_789 | |
| /// ``` | |
| /// | |
| /// Use instead: | |
| /// ```no_run | |
| /// let v: f64 = 0.123_456_789_9; | |
| /// println!("{}", v); // 0.123_456_789_9 | |
| /// ``` | |
| #[clippy::version = "pre 1.29.0"] | |
| pub EXCESSIVE_PRECISION, | |
| style, | |
| "excessive precision for float literal" | |
| } | |
| declare_clippy_lint! { | |
| /// ### What it does | |
| /// Checks for whole number float literals that | |
| /// cannot be represented as the underlying type without loss. | |
| /// | |
| /// ### Why restrict this? | |
| /// If the value was intended to be exact, it will not be. | |
| /// This may be especially surprising when the lost precision is to the left of the decimal point. | |
| /// | |
| /// ### Example | |
| /// ```no_run | |
| /// let _: f32 = 16_777_217.0; // 16_777_216.0 | |
| /// ``` | |
| /// | |
| /// Use instead: | |
| /// ```no_run | |
| /// let _: f32 = 16_777_216.0; | |
| /// let _: f64 = 16_777_217.0; | |
| /// ``` | |
| #[clippy::version = "1.43.0"] | |
| pub LOSSY_FLOAT_LITERAL, | |
| restriction, | |
| "lossy whole number float literals" | |
| } | |
| impl_lint_pass!(FloatLiteral => [EXCESSIVE_PRECISION, LOSSY_FLOAT_LITERAL]); | |
| pub struct FloatLiteral { | |
| const_literal_digits_threshold: usize, | |
| } | |
| impl FloatLiteral { | |
| pub fn new(conf: &'static Conf) -> Self { | |
| Self { | |
| const_literal_digits_threshold: conf.const_literal_digits_threshold, | |
| } | |
| } | |
| } | |
| impl<'tcx> LateLintPass<'tcx> for FloatLiteral { | |
| fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx hir::Expr<'_>) { | |
| if let hir::ExprKind::Lit(lit) = expr.kind | |
| && let LitKind::Float(sym, lit_float_ty) = lit.node | |
| && let ty::Float(fty) = *cx.typeck_results().expr_ty(expr).kind() | |
| { | |
| let sym_str = sym.as_str(); | |
| let formatter = FloatFormat::new(sym_str); | |
| // Try to bail out if the float is for sure fine. | |
| // If its within the 2 decimal digits of being out of precision we | |
| // check if the parsed representation is the same as the string | |
| // since we'll need the truncated string anyway. | |
| let digits = count_digits(sym_str); | |
| let max = max_digits(fty); | |
| let type_suffix = match lit_float_ty { | |
| LitFloatType::Suffixed(FloatTy::F16) => Some("f16"), | |
| LitFloatType::Suffixed(FloatTy::F32) => Some("f32"), | |
| LitFloatType::Suffixed(FloatTy::F64) => Some("f64"), | |
| LitFloatType::Suffixed(FloatTy::F128) => Some("f128"), | |
| LitFloatType::Unsuffixed => None, | |
| }; | |
| let (is_whole, is_inf, mut float_str) = match fty { | |
| FloatTy::F16 | FloatTy::F128 => { | |
| // FIXME(f16_f128): do a check like the others when parsing is available | |
| return; | |
| }, | |
| FloatTy::F32 => { | |
| let value = sym_str.parse::<f32>().unwrap(); | |
| (value.fract() == 0.0, value.is_infinite(), formatter.format(value)) | |
| }, | |
| FloatTy::F64 => { | |
| let value = sym_str.parse::<f64>().unwrap(); | |
| (value.fract() == 0.0, value.is_infinite(), formatter.format(value)) | |
| }, | |
| }; | |
| if is_inf { | |
| return; | |
| } | |
| if is_whole && !sym_str.contains(['e', 'E']) { | |
| // Normalize the literal by stripping the fractional portion | |
| if sym_str.split('.').next().unwrap() != float_str { | |
| span_lint_and_then( | |
| cx, | |
| LOSSY_FLOAT_LITERAL, | |
| expr.span, | |
| "literal cannot be represented as the underlying type without loss of precision", | |
| |diag| { | |
| // If the type suffix is missing the suggestion would be | |
| // incorrectly interpreted as an integer so adding a `.0` | |
| // suffix to prevent that. | |
| if type_suffix.is_none() { | |
| float_str.push_str(".0"); | |
| } | |
| diag.span_suggestion_verbose( | |
| expr.span, | |
| "consider changing the type or replacing it with", | |
| numeric_literal::format(&float_str, type_suffix, true), | |
| Applicability::MachineApplicable, | |
| ); | |
| }, | |
| ); | |
| } | |
| } else if digits > max as usize && count_digits(&float_str) < digits { | |
| if digits >= self.const_literal_digits_threshold | |
| && matches!(expr_use_ctxt(cx, expr).use_node(cx), ExprUseNode::ConstStatic(_)) | |
| { | |
| // If a big enough number of digits is specified and it's a constant | |
| // we assume the user is definining a constant, and excessive precision is ok | |
| return; | |
| } | |
| span_lint_and_then( | |
| cx, | |
| EXCESSIVE_PRECISION, | |
| expr.span, | |
| "float has excessive precision", | |
| |diag| { | |
| if digits >= self.const_literal_digits_threshold | |
| && let Some(let_stmt) = maybe_let_stmt(cx, expr) | |
| { | |
| diag.span_note(let_stmt.span, "consider making it a `const` item"); | |
| } | |
| diag.span_suggestion_verbose( | |
| expr.span, | |
| "consider changing the type or truncating it to", | |
| numeric_literal::format(&float_str, type_suffix, true), | |
| Applicability::MachineApplicable, | |
| ); | |
| }, | |
| ); | |
| } | |
| } | |
| } | |
| } | |
| #[must_use] | |
| fn max_digits(fty: FloatTy) -> u32 { | |
| match fty { | |
| FloatTy::F16 => f16::DIGITS, | |
| FloatTy::F32 => f32::DIGITS, | |
| FloatTy::F64 => f64::DIGITS, | |
| FloatTy::F128 => f128::DIGITS, | |
| } | |
| } | |
| /// Counts the digits excluding leading zeros | |
| #[must_use] | |
| fn count_digits(s: &str) -> usize { | |
| // Note that s does not contain the `f{16,32,64,128}` suffix, and underscores have been stripped | |
| s.chars() | |
| .filter(|c| *c != '-' && *c != '.') | |
| .take_while(|c| *c != 'e' && *c != 'E') | |
| .fold(0, |count, c| { | |
| // leading zeros | |
| if c == '0' && count == 0 { count } else { count + 1 } | |
| }) | |
| } | |
| enum FloatFormat { | |
| LowerExp, | |
| UpperExp, | |
| Normal, | |
| } | |
| impl FloatFormat { | |
| #[must_use] | |
| fn new(s: &str) -> Self { | |
| s.chars() | |
| .find_map(|x| match x { | |
| 'e' => Some(Self::LowerExp), | |
| 'E' => Some(Self::UpperExp), | |
| _ => None, | |
| }) | |
| .unwrap_or(Self::Normal) | |
| } | |
| fn format<T>(&self, f: T) -> String | |
| where | |
| T: fmt::UpperExp + fmt::LowerExp + fmt::Display, | |
| { | |
| match self { | |
| Self::LowerExp => format!("{f:e}"), | |
| Self::UpperExp => format!("{f:E}"), | |
| Self::Normal => format!("{f}"), | |
| } | |
| } | |
| } | |
| fn maybe_let_stmt<'a>(cx: &LateContext<'a>, expr: &hir::Expr<'_>) -> Option<&'a hir::LetStmt<'a>> { | |
| let parent = cx.tcx.parent_hir_node(expr.hir_id); | |
| match parent { | |
| hir::Node::LetStmt(let_stmt) => Some(let_stmt), | |
| _ => None, | |
| } | |
| } |