Skip to content

Commit f2ff3ab

Browse files
chore: refine no_precision_loss logic and extend tests
1 parent 2e11f89 commit f2ff3ab

File tree

8 files changed

+213
-122
lines changed

8 files changed

+213
-122
lines changed

.changeset/odd-kings-obey.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44

55
Fixed [#8145](https://github.com/biomejs/biome/issues/8145): handling of large hex literals, which previously caused both false positives and false negatives.
66

7-
This affects [`no-precision-loss`](https://biomejs.dev/linter/rules/no-precision-loss/) and [`no-constant-math-min-max-clamp`](https://biomejs.dev/linter/rules/no-constant-math-min-max-clamp/).
7+
This affects [`noPrecisionLoss`](https://biomejs.dev/linter/rules/no-precision-loss/) and [`noConstantMathMinMaxClamp`](https://biomejs.dev/linter/rules/no-constant-math-min-max-clamp/).

crates/biome_js_analyze/src/lint/correctness/no_precision_loss.rs

Lines changed: 50 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -117,64 +117,68 @@ fn is_precision_lost_in_base_10(num: &str) -> Option<bool> {
117117
}
118118

119119
fn is_precision_lost_in_base_other(num: &str, radix: u8) -> bool {
120-
let mut msb: Option<u64> = None;
121-
let mut lsb: Option<u64> = None;
122-
let mut current_bit_index: u64 = 0;
123-
124-
// Iterate over digits in reverse order (least significant first)
120+
// radix is passed down from split_into_radix_and_number which guarantees
121+
// that radix is 2, 8, 16. We've already filtered out the 10 case.
122+
let bits_per_digit = match radix {
123+
16 => 4,
124+
8 => 3,
125+
2 => 1,
126+
// Shouldn't ever happen
127+
_ => return false,
128+
};
129+
130+
// We want to find the positions of the last set bit and the first set bit.
131+
// The distance between them (max - min + 1) is the number of significant bits.
132+
// If this distance > 53, the number cannot be exactly represented in an f64 (which has 53 bits of significand).
133+
let mut min_bit_index: Option<u32> = None;
134+
let mut current_bit_index: u32 = 0;
135+
136+
// Iterate over digits in reverse order (from last to first digit)
125137
for c in num.chars().rev() {
126-
if c == '_' {
127-
continue;
128-
}
129138
let digit = match c.to_digit(radix as u32) {
130139
Some(d) => d,
131-
None => return false, // Should not happen for valid literals
140+
None => return false,
132141
};
133142

134-
// Check bits of the digit
135-
let bits_per_digit = match radix {
136-
16 => 4,
137-
8 => 3,
138-
2 => 1,
139-
_ => unreachable!("radix must be 2, 8, or 16"),
140-
};
143+
if digit != 0 {
144+
if min_bit_index.is_none() {
145+
// Found the first non-zero digit (contains the first set bit of the number)
146+
let trailing_zeros = digit.trailing_zeros();
147+
min_bit_index = Some(current_bit_index + trailing_zeros);
148+
}
149+
150+
// Calculate the last set bit for the current digit
151+
let last_bit_in_digit = (u32::BITS - digit.leading_zeros()) - 1;
152+
let max_bit_index = current_bit_index + last_bit_in_digit;
153+
154+
// Check for overflow (exponent > 1023)
155+
// In IEEE 754 double precision:
156+
// - The exponent bias is 1023.
157+
// - The maximum valid exponent is 1023 (representing 2^1023).
158+
// - 2^1024 overflows to Infinity.
159+
// Thus, if the last set bit is at index 1024 or greater, the number overflows.
160+
if max_bit_index >= 1024 {
161+
return true;
162+
}
141163

142-
for i in 0..bits_per_digit {
143-
if (digit >> i) & 1 == 1 {
144-
let bit_pos = current_bit_index + i;
145-
if lsb.is_none() {
146-
lsb = Some(bit_pos);
164+
// Check for precision loss
165+
// In IEEE 754 double precision:
166+
// - The significand (mantissa) has 53 bits of precision (52 stored bits + 1 implicit leading bit).
167+
// - If the distance between the last set bit and the first set bit
168+
// exceeds 53 bits, the number cannot be exactly represented, as the lower bits would be truncated.
169+
// Span = max - min + 1
170+
// We know min_bit_index is Some because we set it above if it was None
171+
if let Some(min) = min_bit_index {
172+
if max_bit_index - min + 1 > 53 {
173+
return true;
147174
}
148-
msb = Some(bit_pos);
149175
}
150176
}
177+
151178
current_bit_index += bits_per_digit;
152179
}
153180

154-
if let (Some(msb), Some(lsb)) = (msb, lsb) {
155-
// Check for overflow (exponent > 1023)
156-
// In IEEE 754 double precision:
157-
// - The exponent bias is 1023.
158-
// - The maximum valid exponent is 1023 (representing 2^1023).
159-
// - 2^1024 overflows to Infinity.
160-
// Thus, if the most significant bit is at index 1024 or greater, the number overflows.
161-
if msb >= 1024 {
162-
return true;
163-
}
164-
165-
// Check for precision loss
166-
// In IEEE 754 double precision:
167-
// - The significand (mantissa) has 53 bits of precision (52 stored bits + 1 implicit leading bit).
168-
// - If the distance between the most significant bit (MSB) and the least significant bit (LSB)
169-
// exceeds 53 bits, the number cannot be exactly represented, as the lower bits would be truncated.
170-
// Span = MSB - LSB + 1
171-
let span = msb - lsb + 1;
172-
173-
span > 53
174-
} else {
175-
// Value is 0
176-
false
177-
}
181+
false
178182
}
179183

180184
fn remove_leading_zeros(num: &str) -> &str {

crates/biome_js_analyze/tests/specs/correctness/noConstantMathMinMaxClamp/invalid.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,5 @@ globalThis.Math.min(0, Math.max(100, x));
1717
Math.min(0, globalThis.Math.max(100, x));
1818

1919
foo(Math.min(0, Math.max(100, x)));
20+
21+
Math.min(0x10000000000000000, Math.max(0x20000000000000000, x));

crates/biome_js_analyze/tests/specs/correctness/noConstantMathMinMaxClamp/invalid.js.snap

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Math.min(0, globalThis.Math.max(100, x));
2424
2525
foo(Math.min(0, Math.max(100, x)));
2626
27+
Math.min(0x10000000000000000, Math.max(0x20000000000000000, x));
2728
```
2829

2930
# Diagnostics
@@ -328,6 +329,7 @@ invalid.js:19:5 lint/correctness/noConstantMathMinMaxClamp FIXABLE ━━━
328329
> 19 │ foo(Math.min(0, Math.max(100, x)));
329330
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
330331
20 │
332+
21 │ Math.min(0x10000000000000000, Math.max(0x20000000000000000, x));
331333
332334
i It always evaluates to 0.
333335
@@ -336,6 +338,7 @@ invalid.js:19:5 lint/correctness/noConstantMathMinMaxClamp FIXABLE ━━━
336338
> 19 │ foo(Math.min(0, Math.max(100, x)));
337339
│ ^
338340
20 │
341+
21 │ Math.min(0x10000000000000000, Math.max(0x20000000000000000, x));
339342
340343
i Unsafe fix: Swap 0 with 100.
341344
@@ -344,6 +347,34 @@ invalid.js:19:5 lint/correctness/noConstantMathMinMaxClamp FIXABLE ━━━
344347
19 │ - foo(Math.min(0,·Math.max(100,·x)));
345348
19 │ + foo(Math.min(100,·Math.max(0,·x)));
346349
20 20 │
350+
21 21 │ Math.min(0x10000000000000000, Math.max(0x20000000000000000, x));
351+
352+
353+
```
354+
355+
```
356+
invalid.js:21:1 lint/correctness/noConstantMathMinMaxClamp FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
357+
358+
× This Math.min/Math.max combination leads to a constant result.
359+
360+
19 │ foo(Math.min(0, Math.max(100, x)));
361+
20 │
362+
> 21 │ Math.min(0x10000000000000000, Math.max(0x20000000000000000, x));
363+
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
364+
365+
i It always evaluates to 0x10000000000000000.
366+
367+
19 │ foo(Math.min(0, Math.max(100, x)));
368+
20 │
369+
> 21 │ Math.min(0x10000000000000000, Math.max(0x20000000000000000, x));
370+
│ ^^^^^^^^^^^^^^^^^^^
371+
372+
i Unsafe fix: Swap 0x10000000000000000 with 0x20000000000000000.
373+
374+
19 19 │ foo(Math.min(0, Math.max(100, x)));
375+
20 20 │
376+
21 │ - Math.min(0x10000000000000000,·Math.max(0x20000000000000000,·x));
377+
21 │ + Math.min(0x20000000000000000,·Math.max(0x10000000000000000,·x));
347378
348379
349380
```

crates/biome_js_analyze/tests/specs/correctness/noConstantMathMinMaxClamp/invalid_large_hex.js

Lines changed: 0 additions & 2 deletions
This file was deleted.

crates/biome_js_analyze/tests/specs/correctness/noConstantMathMinMaxClamp/invalid_large_hex.js.snap

Lines changed: 0 additions & 38 deletions
This file was deleted.

crates/biome_js_analyze/tests/specs/correctness/noPrecisionLoss/invalid.js

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,16 @@ var x = 0o4_00000000000000_001
3434
var x = 0O4_0000000000000000_1
3535
var x = 0x2_0000000000001
3636
var x = 0X200000_0000000_1
37-
// From repro_issue.js
38-
var x = 0x20000000000001; // 2^53 + 1 (Invalid, precision loss)
39-
var x = 9007199254740993; // 2^53 + 1 (Invalid, precision loss)
40-
var x = 0x10000000000000000000000001; // 2^100 + 1 (Invalid, precision loss)
41-
// 2^1024 (Overflow, Invalid)
37+
var x = 0x20000000000001;
38+
var x = 9007199254740993;
39+
40+
// 2^100 + 1
41+
var x = 0x10000000000000000000000001;
42+
var x = 0b10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001; // 2^100 + 1 (Invalid, precision loss)
43+
var x = 0o2000000000000000000000000000000001;
44+
45+
// Infinity cases
4246
var x = 0x10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000;
47+
var x = 0o200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000;
48+
var x = 0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000;
49+
var x = 0b10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000;

0 commit comments

Comments
 (0)