Skip to content

Commit 1263e05

Browse files
JeremyMoeglichryan-m-walker
authored andcommitted
fix(noPrecisionLoss): correctly handle large hex literals (biomejs#8172)
1 parent 4d61dd6 commit 1263e05

File tree

9 files changed

+303
-21
lines changed

9 files changed

+303
-21
lines changed

.changeset/odd-kings-obey.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Fixed [#8145](https://github.com/biomejs/biome/issues/8145): handling of large hex literals, which previously caused both false positives and false negatives.
6+
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: 59 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
use std::num::IntErrorKind;
2-
use std::ops::RangeInclusive;
3-
41
use biome_analyze::context::RuleContext;
52
use biome_analyze::{Ast, Rule, RuleDiagnostic, RuleSource, declare_lint_rule};
63
use biome_console::markup;
@@ -120,21 +117,68 @@ fn is_precision_lost_in_base_10(num: &str) -> Option<bool> {
120117
}
121118

122119
fn is_precision_lost_in_base_other(num: &str, radix: u8) -> bool {
123-
let parsed = match i64::from_str_radix(num, radix as u32) {
124-
Ok(x) => x,
125-
Err(e) => {
126-
return matches!(
127-
e.kind(),
128-
IntErrorKind::PosOverflow | IntErrorKind::NegOverflow
129-
);
130-
}
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,
131128
};
132129

133-
const MAX_SAFE_INTEGER: i64 = 2_i64.pow(53) - 1;
134-
const MIN_SAFE_INTEGER: i64 = -MAX_SAFE_INTEGER;
135-
const SAFE_RANGE: RangeInclusive<i64> = MIN_SAFE_INTEGER..=MAX_SAFE_INTEGER;
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)
137+
for c in num.chars().rev() {
138+
let digit = match c.to_digit(radix as u32) {
139+
Some(d) => d,
140+
None => return false,
141+
};
142+
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+
}
163+
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+
&& max_bit_index - min + 1 > 53
173+
{
174+
return true;
175+
}
176+
}
177+
178+
current_bit_index += bits_per_digit;
179+
}
136180

137-
!SAFE_RANGE.contains(&parsed)
181+
false
138182
}
139183

140184
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/noPrecisionLoss/invalid.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,17 @@ var x = 0B10000000000_0000000000000000000000000000_000000000000001
3333
var x = 0o4_00000000000000_001
3434
var x = 0O4_0000000000000000_1
3535
var x = 0x2_0000000000001
36-
var x = 0X200000_0000000_1
36+
var x = 0X200000_0000000_1
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
46+
var x = 0x10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000;
47+
var x = 0o200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000;
48+
var x = 0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000;
49+
var x = 0b10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000;

0 commit comments

Comments
 (0)