Skip to main content

wowlab_tidy/languages/rust/rules/style/
excessive_float_precision.rs

1#[cfg(test)]
2use googletest::prelude::*;
3use ra_ap_syntax::ast::{self, LiteralKind};
4use winnow::token::take_while;
5
6use crate::{AstCtx, Example, Violation, infra::parse};
7
8#[rustfmt::skip]
9const EXAMPLES: &[Example] = &[
10    Example {
11        label: "f32 with too many digits",
12        code: "fn f() { let _x: f32 = 1.23456789012345_f32; }",
13        pass: false,
14    },
15    Example {
16        label: "f32 with ok precision",
17        code: "fn f() { let _x: f32 = 1.234567_f32; }",
18        pass: true,
19    },
20    Example {
21        label: "f64 with too many digits",
22        code: "fn f() { let _x = 3.14159265358979323846_f64; }",
23        pass: false,
24    },
25    Example {
26        label: "f64 with ok precision",
27        code: "fn f() { let _x = 3.141592653589793_f64; }",
28        pass: true,
29    },
30    Example {
31        label: "unsuffixed float is fine",
32        code: "fn f() { let _x = 3.14159265358979323846; }",
33        pass: true,
34    },
35    Example {
36        label: "excessive precision in test",
37        code: "#[cfg(test)]\nmod tests {\n    fn t() { let _x = 1.23456789012345_f32; }\n}",
38        pass: true,
39    },
40];
41
42crate::ast_rule!(
43    excessive_float_precision,
44    "Flag float literals with more significant digits than the type can represent.",
45    "Extra digits beyond what f32/f64 can represent are misleading. They suggest precision that does not exist.",
46);
47
48/// f32 has ~7 significant decimal digits, f64 has ~16.
49const F32_MAX_SIGNIFICANT: usize = 8;
50const F64_MAX_SIGNIFICANT: usize = 17;
51
52fn check_excessive_float_precision(ctx: &AstCtx<'_>) -> Vec<Violation> {
53    ctx.nodes::<ast::Literal>()
54        .filter(|literal| !ctx.is_in_test(literal))
55        .filter_map(|literal| {
56            let LiteralKind::FloatNumber(number) = literal.kind() else {
57                return None;
58            };
59            let repr = number.to_string();
60            let (max_digits, type_name) = match number.suffix()? {
61                "f32" => (F32_MAX_SIGNIFICANT, "f32"),
62                "f64" => (F64_MAX_SIGNIFICANT, "f64"),
63                _ => return None,
64            };
65            let sig_digits = count_significant_digits(&repr);
66
67            (sig_digits > max_digits).then(|| {
68                ctx.violation(
69                    &literal,
70                    format!(
71                        "float literal has {sig_digits} significant digits but {type_name} only supports ~{} — excess digits are silently lost",
72                        max_digits - 1
73                    ),
74                )
75            })
76        })
77        .collect()
78}
79
80fn count_significant_digits(s: &str) -> usize {
81    let mut input = s;
82    let body = parse::try_parse(&mut input, take_while(0.., |c: char| c != 'f')).unwrap_or(s);
83    let cleaned: String = body.chars().filter(|&c| c != '_').collect();
84
85    let mut inp = cleaned.as_str();
86    let _ = parse::try_parse(&mut inp, take_while(0.., |c: char| c.is_ascii_digit()));
87
88    if parse::try_parse(&mut inp, '.').is_none() {
89        return 0;
90    }
91
92    let all_digits: String = cleaned.chars().filter(char::is_ascii_digit).collect();
93    let mut digit_input = all_digits.as_str();
94    let _ = parse::try_parse(&mut digit_input, take_while(0.., '0'));
95
96    digit_input.len()
97}
98
99crate::tidy_ast_test!(check_excessive_float_precision, {
100    crate::example_tests!(EXAMPLES, check_excessive_float_precision);
101
102    #[gtest]
103    fn count_digits() -> Result<()> {
104        verify_eq!(count_significant_digits("3.14_f32"), 3)?;
105        verify_eq!(count_significant_digits("0.001_f32"), 1)?;
106        verify_eq!(count_significant_digits("1.234567890_f64"), 10)?;
107        verify_eq!(count_significant_digits("100_f32"), 0)?;
108
109        Ok(())
110    }
111});