summaryrefslogtreecommitdiffstats
path: root/src/tools/clippy/clippy_lints/src/float_literal.rs
blob: f2e0798096378a3a0eeb9c9e9196e8f639bf03e3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
use clippy_utils::diagnostics::span_lint_and_sugg;
use clippy_utils::numeric_literal;
use if_chain::if_chain;
use rustc_ast::ast::{self, 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::{declare_lint_pass, declare_tool_lint};
use std::fmt;

declare_clippy_lint! {
    /// ### What it does
    /// Checks for float literals with a precision greater
    /// than that supported by the underlying type.
    ///
    /// ### Why is this bad?
    /// Rust will truncate the literal silently.
    ///
    /// ### Example
    /// ```rust
    /// let v: f32 = 0.123_456_789_9;
    /// println!("{}", v); //  0.123_456_789
    /// ```
    ///
    /// Use instead:
    /// ```rust
    /// 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 is this bad?
    /// Rust will silently lose precision during
    /// conversion to a float.
    ///
    /// ### Example
    /// ```rust
    /// let _: f32 = 16_777_217.0; // 16_777_216.0
    /// ```
    ///
    /// Use instead:
    /// ```rust
    /// 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"
}

declare_lint_pass!(FloatLiteral => [EXCESSIVE_PRECISION, LOSSY_FLOAT_LITERAL]);

impl<'tcx> LateLintPass<'tcx> for FloatLiteral {
    fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx hir::Expr<'_>) {
        let ty = cx.typeck_results().expr_ty(expr);
        if_chain! {
            if let ty::Float(fty) = *ty.kind();
            if let hir::ExprKind::Lit(ref lit) = expr.kind;
            if let LitKind::Float(sym, lit_float_ty) = lit.node;
            then {
                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(ast::FloatTy::F32) => Some("f32"),
                    LitFloatType::Suffixed(ast::FloatTy::F64) => Some("f64"),
                    LitFloatType::Unsuffixed => None
                };
                let (is_whole, mut float_str) = match fty {
                    FloatTy::F32 => {
                        let value = sym_str.parse::<f32>().unwrap();

                        (value.fract() == 0.0, formatter.format(value))
                    },
                    FloatTy::F64 => {
                        let value = sym_str.parse::<f64>().unwrap();

                        (value.fract() == 0.0, formatter.format(value))
                    },
                };

                if is_whole && !sym_str.contains(|c| c == 'e' || c == 'E') {
                    // Normalize the literal by stripping the fractional portion
                    if sym_str.split('.').next().unwrap() != float_str {
                        // 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");
                        }

                        span_lint_and_sugg(
                            cx,
                            LOSSY_FLOAT_LITERAL,
                            expr.span,
                            "literal cannot be represented as the underlying type without loss of precision",
                            "consider changing the type or replacing it with",
                            numeric_literal::format(&float_str, type_suffix, true),
                            Applicability::MachineApplicable,
                        );
                    }
                } else if digits > max as usize && float_str.len() < sym_str.len() {
                    span_lint_and_sugg(
                        cx,
                        EXCESSIVE_PRECISION,
                        expr.span,
                        "float has excessive precision",
                        "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::F32 => f32::DIGITS,
        FloatTy::F64 => f64::DIGITS,
    }
}

/// Counts the digits excluding leading zeros
#[must_use]
fn count_digits(s: &str) -> usize {
    // Note that s does not contain the f32/64 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!("{:e}", f),
            Self::UpperExp => format!("{:E}", f),
            Self::Normal => format!("{}", f),
        }
    }
}