sqrust_rules/ambiguous/
floating_point_comparison.rs1use sqrust_core::{Diagnostic, FileContext, Rule};
2use crate::capitalisation::SkipMap;
3
4pub struct FloatingPointComparison;
5
6impl Rule for FloatingPointComparison {
7 fn name(&self) -> &'static str {
8 "Ambiguous/FloatingPointComparison"
9 }
10
11 fn check(&self, ctx: &FileContext) -> Vec<Diagnostic> {
12 let source = &ctx.source;
13 let bytes = source.as_bytes();
14 let len = bytes.len();
15 let skip = SkipMap::build(source);
16
17 let mut diags = Vec::new();
18 let mut i = 0;
19
20 while i < len {
21 if !skip.is_code(i) {
22 i += 1;
23 continue;
24 }
25
26 let (is_eq, op_len) = if bytes[i] == b'=' && (i == 0 || (bytes[i - 1] != b'!' && bytes[i - 1] != b'<' && bytes[i - 1] != b'>')) {
28 (true, 1)
29 } else if i + 1 < len && bytes[i] == b'!' && bytes[i + 1] == b'=' {
30 (true, 2)
31 } else if i + 1 < len && bytes[i] == b'<' && bytes[i + 1] == b'>' {
32 (true, 2)
33 } else {
34 (false, 1)
35 };
36
37 if !is_eq {
38 i += 1;
39 continue;
40 }
41
42 let op_start = i;
43 i += op_len;
44
45 while i < len && (bytes[i] == b' ' || bytes[i] == b'\t' || bytes[i] == b'\n' || bytes[i] == b'\r') {
47 i += 1;
48 }
49
50 let float_start = i;
52 if i < len && (bytes[i] == b'+' || bytes[i] == b'-') {
54 i += 1;
55 }
56 let digit_start = i;
58 while i < len && bytes[i].is_ascii_digit() {
59 i += 1;
60 }
61 if i < len && bytes[i] == b'.' && i > digit_start {
63 i += 1;
64 let frac_start = i;
66 while i < len && bytes[i].is_ascii_digit() {
67 i += 1;
68 }
69 if i > frac_start {
70 let followed_by_word = i < len && (bytes[i].is_ascii_alphabetic() || bytes[i] == b'_');
72 if !followed_by_word {
73 let (line, col) = offset_to_line_col(source, op_start);
75 diags.push(Diagnostic {
76 rule: self.name(),
77 message: "Exact equality comparison with floating-point literal; floating-point values are imprecise — consider using a range check or ROUND()".to_string(),
78 line,
79 col,
80 });
81 continue;
82 }
83 }
84 }
85 i = float_start;
87 }
88
89 diags
90 }
91}
92
93fn offset_to_line_col(source: &str, offset: usize) -> (usize, usize) {
94 let before = &source[..offset.min(source.len())];
95 let line = before.chars().filter(|&c| c == '\n').count() + 1;
96 let col = before.rfind('\n').map(|p| offset - p - 1).unwrap_or(offset) + 1;
97 (line, col)
98}