mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
fix(linter/unicorn/prefer-bigint-literals): unwrap redundant constructor call; don't auto-fix for lost precision (#25690)
`prefer-bigint-literals` now simplifies global `BigInt(123n)` calls to
`123n` and withholds numeric autofixes that would change the runtime
bigint value. Rounded number literals still receive a diagnostic with an
“Integer literal loses precision” note.
- Decimal integer checks compare the full stored integer, including
trailing zeros. For example, `BigInt(1000000000000000100)` evaluates to
`1000000000000000128n`, so replacing it with `1000000000000000100n` is
unsafe.
- Binary, octal, and hexadecimal checks use significant binary digits
without narrowing the value to `u64`. This blocks unsafe fixes such as
`BigInt(0xffffffffffffffff)` while preserving valid fixes above
`u64::MAX`, such as `BigInt(0x10000000000000000)`.
- Calls to locally bound `BigInt` functions, parameters, and imports are
left intact.
- Replacements preserve token boundaries: `BigInt(1n)in {'1':0}` becomes
`1n in {'1':0}`. The same padding applies to number and string arguments
and adjacent `instanceof`, `as`, and `satisfies` keywords.
The precision helpers are shared through `utils/number.rs`. Bigint fixes
use exact decimal integer equality, while `no-loss-of-precision` retains
its existing decimal significant-digit behavior. The non-decimal
correction also applies to `no-loss-of-precision`. Regression tests
cover rounded and exact large integers, shadowed calls, and token
boundaries.
---------
Co-authored-by: Cameron Clark <cameron.clark@hey.com>
This commit is contained in:
1 parent
b5e8eff8da
commit
598ac03f31
6 files changed
+718
-522
No files matched your search
@@ -1,12 +1,9 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use cow_utils::CowUtils;
|
||||
use oxc_ast::{AstKind, ast::NumericLiteral};
|
||||
use oxc_ast::AstKind;
|
||||
use oxc_diagnostics::OxcDiagnostic;
|
||||
use oxc_macros::declare_oxc_lint;
|
||||
use oxc_span::Span;
|
||||
|
||||
use crate::{AstNode, context::LintContext, rule::Rule};
|
||||
use crate::{AstNode, context::LintContext, rule::Rule, utils::loses_precision};
|
||||
|
||||
fn no_loss_of_precision_diagnostic(span: Span) -> OxcDiagnostic {
|
||||
OxcDiagnostic::warn("This number literal will lose precision at runtime.")
|
||||
@@ -91,7 +88,7 @@ declare_oxc_lint!(
|
||||
impl Rule for NoLossOfPrecision {
|
||||
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
|
||||
match node.kind() {
|
||||
AstKind::NumericLiteral(node) if Self::lose_precision(node) => {
|
||||
AstKind::NumericLiteral(node) if loses_precision(node) => {
|
||||
ctx.diagnostic(no_loss_of_precision_diagnostic(node.span));
|
||||
}
|
||||
_ => {}
|
||||
@@ -99,509 +96,6 @@ impl Rule for NoLossOfPrecision {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RawNum<'a> {
|
||||
int: &'a str,
|
||||
frac: &'a str,
|
||||
exp: isize,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ScientificNotation<'a> {
|
||||
int: &'a str,
|
||||
frac: Cow<'a, str>,
|
||||
exp: isize,
|
||||
}
|
||||
|
||||
impl PartialEq for ScientificNotation<'_> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
if self.int == other.int && self.frac == other.frac {
|
||||
if self.int == "0" && self.frac.is_empty() {
|
||||
return true;
|
||||
}
|
||||
return self.exp == other.exp;
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> RawNum<'a> {
|
||||
fn new(num: &str) -> Option<RawNum<'_>> {
|
||||
// remove sign
|
||||
let num = num.trim_start_matches(['+', '-']);
|
||||
|
||||
let (int, num_without_int) = {
|
||||
// skip leading zeros
|
||||
let num_without_zeros = num.trim_start_matches('0');
|
||||
|
||||
// read the integer part and store the end index
|
||||
let int_end = num_without_zeros
|
||||
.chars()
|
||||
.position(|ch| !ch.is_ascii_digit())
|
||||
.unwrap_or(num_without_zeros.len());
|
||||
|
||||
// if no integer part was found, default to 0
|
||||
let int = if int_end == 0 { "0" } else { &num_without_zeros[..int_end] };
|
||||
|
||||
// make a slice of the rest of the string
|
||||
let num_without_int = &num_without_zeros[int_end..];
|
||||
|
||||
(int, num_without_int)
|
||||
};
|
||||
|
||||
// if next char is a dot, parse the fractional part
|
||||
let (frac, num_without_frac) =
|
||||
num_without_int.strip_prefix('.').map_or(("", num_without_int), |num_without_dot| {
|
||||
let frac_end = num_without_dot
|
||||
.chars()
|
||||
.position(|ch| !ch.is_ascii_digit())
|
||||
.unwrap_or(num_without_dot.len());
|
||||
|
||||
// slice the fractional part and the rest of the string
|
||||
let frac = &num_without_dot[..frac_end];
|
||||
let num_without_frac = &num_without_dot[frac_end..];
|
||||
|
||||
(frac, num_without_frac)
|
||||
});
|
||||
|
||||
// if next char is an e, treat the rest as the exponent
|
||||
let exp =
|
||||
num_without_frac.strip_prefix(['e', 'E']).map_or("0", |num_without_e| num_without_e);
|
||||
|
||||
let Ok(exp) = exp.parse::<isize>() else {
|
||||
return None;
|
||||
};
|
||||
|
||||
Some(RawNum { int, frac, exp })
|
||||
}
|
||||
|
||||
fn normalize(&mut self, parse_as_float: bool) -> ScientificNotation<'a> {
|
||||
if self.int == "0" && !self.frac.is_empty() {
|
||||
let frac_zeros = self.frac.chars().take_while(|&ch| ch == '0').count();
|
||||
#[expect(clippy::cast_possible_wrap)]
|
||||
let exp = self.exp - 1 - frac_zeros as isize;
|
||||
self.frac = &self.frac[frac_zeros..];
|
||||
|
||||
match self.frac.len() {
|
||||
0 => ScientificNotation { int: "0", frac: Cow::Borrowed(""), exp },
|
||||
1 => ScientificNotation { int: &self.frac[..1], frac: Cow::Borrowed(""), exp },
|
||||
_ => ScientificNotation {
|
||||
int: &self.frac[..1],
|
||||
frac: Cow::Borrowed(&self.frac[1..]),
|
||||
exp,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
#[expect(clippy::cast_possible_wrap)]
|
||||
let exp = self.exp + self.int.len() as isize - 1;
|
||||
if self.int.len() == 1 {
|
||||
ScientificNotation { int: self.int, frac: Cow::Borrowed(self.frac), exp }
|
||||
} else {
|
||||
let frac = if self.frac.is_empty() {
|
||||
if parse_as_float {
|
||||
Cow::Borrowed(&self.int[1..])
|
||||
} else {
|
||||
let int_trimmed = self.int.trim_end_matches('0');
|
||||
if int_trimmed.len() == 1 {
|
||||
Cow::Borrowed("")
|
||||
} else {
|
||||
Cow::Borrowed(&int_trimmed[1..])
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Cow::Owned(format!("{}{}", &self.int[1..], self.frac))
|
||||
};
|
||||
|
||||
ScientificNotation { int: &self.int[..1], frac, exp }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NoLossOfPrecision {
|
||||
const MAX_OBVIOUSLY_SAFE_SIGNIFICANT_DIGITS: usize = 15;
|
||||
|
||||
fn strip_numeric_separators(raw: &str) -> Cow<'_, str> {
|
||||
if !raw.as_bytes().contains(&b'_') {
|
||||
return Cow::Borrowed(raw);
|
||||
}
|
||||
raw.cow_replace('_', "")
|
||||
}
|
||||
|
||||
fn non_base_ten_literal_is_exact(raw: &str) -> bool {
|
||||
let raw = raw.trim_start_matches(['+', '-']);
|
||||
let (radix, digits) =
|
||||
if let Some(digits) = raw.strip_prefix("0b").or_else(|| raw.strip_prefix("0B")) {
|
||||
(2, digits)
|
||||
} else if let Some(digits) = raw.strip_prefix("0o").or_else(|| raw.strip_prefix("0O")) {
|
||||
(8, digits)
|
||||
} else if let Some(digits) = raw.strip_prefix("0x").or_else(|| raw.strip_prefix("0X")) {
|
||||
(16, digits)
|
||||
} else {
|
||||
(8, raw.trim_start_matches('0'))
|
||||
};
|
||||
|
||||
let mut bit_len = 0;
|
||||
for byte in digits.bytes() {
|
||||
if byte == b'_' {
|
||||
continue;
|
||||
}
|
||||
|
||||
let digit = match byte {
|
||||
b'0'..=b'9' => byte - b'0',
|
||||
b'a'..=b'f' => byte - b'a' + 10,
|
||||
b'A'..=b'F' => byte - b'A' + 10,
|
||||
_ => return false,
|
||||
};
|
||||
|
||||
if bit_len == 0 {
|
||||
if digit == 0 {
|
||||
continue;
|
||||
}
|
||||
bit_len = match radix {
|
||||
2 => 1,
|
||||
8 => match digit {
|
||||
1 => 1,
|
||||
2 | 3 => 2,
|
||||
_ => 3,
|
||||
},
|
||||
_ => match digit {
|
||||
1 => 1,
|
||||
2 | 3 => 2,
|
||||
4..=7 => 3,
|
||||
_ => 4,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
bit_len += match radix {
|
||||
2 => 1,
|
||||
8 => 3,
|
||||
_ => 4,
|
||||
};
|
||||
}
|
||||
|
||||
if bit_len > 53 {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn base_ten_literal_is_safe(raw: &str, value: f64) -> bool {
|
||||
if !value.is_finite() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let raw = raw.trim_start_matches(['+', '-']);
|
||||
let exponent_start = raw.find(['e', 'E']).unwrap_or(raw.len());
|
||||
if exponent_start != raw.len() {
|
||||
return false;
|
||||
}
|
||||
let mantissa = &raw[..exponent_start];
|
||||
|
||||
let mut digit_index = 0;
|
||||
let mut first_non_zero = None;
|
||||
let mut last_non_zero = None;
|
||||
let mut fractional_has_non_zero = false;
|
||||
let mut dot_digit_index = None;
|
||||
|
||||
for byte in mantissa.bytes() {
|
||||
match byte {
|
||||
b'_' => continue,
|
||||
b'.' => {
|
||||
dot_digit_index = Some(digit_index);
|
||||
continue;
|
||||
}
|
||||
b'0'..=b'9' => {}
|
||||
_ => return false,
|
||||
}
|
||||
|
||||
if byte != b'0' {
|
||||
first_non_zero.get_or_insert(digit_index);
|
||||
last_non_zero = Some(digit_index);
|
||||
fractional_has_non_zero |= dot_digit_index.is_some();
|
||||
}
|
||||
digit_index += 1;
|
||||
}
|
||||
|
||||
let Some(first_non_zero) = first_non_zero else {
|
||||
return true;
|
||||
};
|
||||
if value == 0.0 {
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Some(dot_digit_index) = dot_digit_index
|
||||
&& !fractional_has_non_zero
|
||||
{
|
||||
return dot_digit_index - first_non_zero <= Self::MAX_OBVIOUSLY_SAFE_SIGNIFICANT_DIGITS;
|
||||
}
|
||||
|
||||
let last_significant =
|
||||
if dot_digit_index.is_some() { digit_index - 1 } else { last_non_zero.unwrap() };
|
||||
last_significant - first_non_zero < Self::MAX_OBVIOUSLY_SAFE_SIGNIFICANT_DIGITS
|
||||
}
|
||||
|
||||
fn not_base_ten_loses_precision(node: &'_ NumericLiteral) -> bool {
|
||||
let raw = node.raw.as_ref().unwrap().as_str();
|
||||
if Self::non_base_ten_literal_is_exact(raw) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let raw = Self::strip_numeric_separators(raw);
|
||||
let raw = raw.cow_to_ascii_uppercase();
|
||||
#[expect(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
|
||||
// AST always store number as f64, need a cast to format in bin/oct/hex
|
||||
let value = node.value as u64;
|
||||
let suffix = if raw.starts_with("0B") {
|
||||
format!("{value:b}")
|
||||
} else if raw.starts_with("0X") {
|
||||
format!("{value:x}")
|
||||
} else {
|
||||
format!("{value:o}")
|
||||
};
|
||||
!raw.ends_with(&suffix.cow_to_ascii_uppercase().as_ref())
|
||||
}
|
||||
|
||||
fn base_ten_loses_precision(node: &'_ NumericLiteral) -> bool {
|
||||
let raw = node.raw.as_ref().unwrap().as_str();
|
||||
if Self::base_ten_literal_is_safe(raw, node.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let raw = Self::strip_numeric_separators(raw);
|
||||
let Some(raw) = Self::normalize(&raw, false) else {
|
||||
return true;
|
||||
};
|
||||
|
||||
let total_significant_digits = raw.int.len() + raw.frac.len();
|
||||
|
||||
if total_significant_digits > 100 {
|
||||
return true;
|
||||
}
|
||||
|
||||
let stored = to_precision(node.value, total_significant_digits);
|
||||
|
||||
let Some(stored) = Self::normalize(&stored, true) else {
|
||||
return true;
|
||||
};
|
||||
raw != stored
|
||||
}
|
||||
|
||||
fn normalize(num: &str, parse_as_float: bool) -> Option<ScientificNotation<'_>> {
|
||||
let coefficient = num.trim_start_matches(['+', '-']);
|
||||
let exp_start =
|
||||
coefficient.find('e').or_else(|| coefficient.find('E')).unwrap_or(coefficient.len());
|
||||
let parse_as_float = parse_as_float || coefficient[..exp_start].contains('.');
|
||||
Some(RawNum::new(num)?.normalize(parse_as_float))
|
||||
}
|
||||
|
||||
pub fn lose_precision(node: &'_ NumericLiteral) -> bool {
|
||||
if node.base.is_base_10() {
|
||||
Self::base_ten_loses_precision(node)
|
||||
} else {
|
||||
Self::not_base_ten_loses_precision(node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `flt_str_to_exp` - used in `to_precision`
|
||||
///
|
||||
/// This function traverses a string representing a number,
|
||||
/// returning the floored log10 of this number.
|
||||
#[expect(clippy::cast_possible_truncation)]
|
||||
#[expect(clippy::cast_possible_wrap)]
|
||||
fn flt_str_to_exp(flt: &str) -> i32 {
|
||||
let mut non_zero_encountered = false;
|
||||
let mut dot_encountered = false;
|
||||
for (i, c) in flt.char_indices() {
|
||||
if c == '.' {
|
||||
if non_zero_encountered {
|
||||
return (i as i32) - 1;
|
||||
}
|
||||
dot_encountered = true;
|
||||
} else if c != '0' {
|
||||
if dot_encountered {
|
||||
return 1 - (i as i32);
|
||||
}
|
||||
non_zero_encountered = true;
|
||||
}
|
||||
}
|
||||
(flt.len() as i32) - 1
|
||||
}
|
||||
|
||||
/// `round_to_precision` - used in `to_precision`
|
||||
///
|
||||
/// This procedure has two roles:
|
||||
/// - If there are enough or more than enough digits in the
|
||||
/// string to show the required precision, the number
|
||||
/// represented by these digits is rounded using string
|
||||
/// manipulation.
|
||||
/// - Else, zeroes are appended to the string.
|
||||
/// - Additionally, sometimes the exponent was wrongly computed and
|
||||
/// while up-rounding we find that we need an extra digit. When this
|
||||
/// happens, we return true so that the calling context can adjust
|
||||
/// the exponent. The string is kept at an exact length of `precision`.
|
||||
///
|
||||
/// When this procedure returns, `digits` is exactly `precision` long.
|
||||
fn round_to_precision(digits: &mut String, precision: usize) -> bool {
|
||||
if digits.len() > precision {
|
||||
let to_round = digits.split_off(precision);
|
||||
let mut digit =
|
||||
digits.pop().expect("already checked that length is bigger than precision") as u8;
|
||||
if let Some(first) = to_round.chars().next()
|
||||
&& first > '4'
|
||||
{
|
||||
digit += 1;
|
||||
}
|
||||
|
||||
if digit as char == ':' {
|
||||
// ':' is '9' + 1
|
||||
// need to propagate the increment backward
|
||||
let mut replacement = String::from("0");
|
||||
let mut propagated = false;
|
||||
for c in digits.chars().rev() {
|
||||
let d = match (c, propagated) {
|
||||
('0'..='8', false) => (c as u8 + 1) as char,
|
||||
(_, false) => '0',
|
||||
(_, true) => c,
|
||||
};
|
||||
replacement.push(d);
|
||||
if d != '0' {
|
||||
propagated = true;
|
||||
}
|
||||
}
|
||||
digits.clear();
|
||||
let replacement = if propagated {
|
||||
replacement.as_str()
|
||||
} else {
|
||||
digits.push('1');
|
||||
&replacement.as_str()[1..]
|
||||
};
|
||||
for c in replacement.chars().rev() {
|
||||
digits.push(c);
|
||||
}
|
||||
!propagated
|
||||
} else {
|
||||
digits.push(digit as char);
|
||||
false
|
||||
}
|
||||
} else {
|
||||
digits.push_str(&"0".repeat(precision - digits.len()));
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn fractional_digits_for_precision(num: f64, precision: usize) -> usize {
|
||||
let exponent = format!("{num:e}")
|
||||
.rsplit_once('e')
|
||||
.and_then(|(_, exp)| exp.parse::<isize>().ok())
|
||||
.unwrap_or_default();
|
||||
|
||||
if exponent.is_negative() {
|
||||
exponent.unsigned_abs().saturating_add(precision).saturating_add(1).max(100)
|
||||
} else {
|
||||
100
|
||||
}
|
||||
}
|
||||
|
||||
/// Mimics JavaScript's `Number.prototype.toPrecision()` method
|
||||
///
|
||||
/// The `toPrecision()` method returns a string representing the Number object to the specified precision.
|
||||
///
|
||||
/// More information:
|
||||
/// - [ECMAScript reference][spec]
|
||||
/// - [MDN documentation][mdn]
|
||||
///
|
||||
/// [spec]: https://tc39.es/ecma262/#sec-number.prototype.toprecision
|
||||
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toPrecision
|
||||
#[expect(clippy::cast_possible_truncation)]
|
||||
#[expect(clippy::cast_possible_wrap)]
|
||||
#[expect(clippy::cast_sign_loss)]
|
||||
pub fn to_precision(mut num: f64, precision: usize) -> String {
|
||||
// Validate precision range (1-100)
|
||||
debug_assert!((1..=100).contains(&precision), "Precision must be between 1 and 100");
|
||||
|
||||
// Handle non-finite numbers
|
||||
if !num.is_finite() {
|
||||
if num.is_nan() {
|
||||
return "NaN".to_string();
|
||||
} else if num.is_infinite() {
|
||||
return if num.is_sign_positive() { "Infinity" } else { "-Infinity" }.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
let precision_i32 = precision as i32;
|
||||
|
||||
// Handle sign
|
||||
let mut prefix = String::new();
|
||||
if num < 0.0 {
|
||||
prefix.push('-');
|
||||
num = -num;
|
||||
}
|
||||
|
||||
let mut suffix: String;
|
||||
let mut exponent: i32;
|
||||
|
||||
// Handle zero
|
||||
if num == 0.0 {
|
||||
suffix = "0".repeat(precision);
|
||||
exponent = 0;
|
||||
} else {
|
||||
// For very small numbers (e.g. 3e-308), we need more than 100 fractional digits
|
||||
// to keep enough significant digits for precision comparison.
|
||||
let fractional_digits = fractional_digits_for_precision(num, precision);
|
||||
suffix = format!("{num:.fractional_digits$}");
|
||||
|
||||
// Calculate exponent
|
||||
exponent = flt_str_to_exp(&suffix);
|
||||
|
||||
// Extract relevant digits only
|
||||
if exponent < 0 {
|
||||
suffix = suffix.split_off((1 - exponent) as usize);
|
||||
} else if let Some(n) = suffix.find('.') {
|
||||
suffix.remove(n);
|
||||
}
|
||||
|
||||
// Round to the specified precision
|
||||
if round_to_precision(&mut suffix, precision) {
|
||||
exponent += 1;
|
||||
}
|
||||
|
||||
// Decide between scientific and fixed notation
|
||||
let great_exp = exponent >= precision_i32;
|
||||
if exponent < -6 || great_exp {
|
||||
// Use scientific notation
|
||||
if precision > 1 {
|
||||
suffix.insert(1, '.');
|
||||
}
|
||||
suffix.push('e');
|
||||
if great_exp {
|
||||
suffix.push('+');
|
||||
}
|
||||
suffix.push_str(&exponent.to_string());
|
||||
|
||||
return prefix + &suffix;
|
||||
}
|
||||
}
|
||||
|
||||
// Use fixed-point notation
|
||||
let e_inc = exponent + 1;
|
||||
if e_inc == precision_i32 {
|
||||
return prefix + &suffix;
|
||||
}
|
||||
|
||||
if exponent >= 0 {
|
||||
suffix.insert(e_inc as usize, '.');
|
||||
} else {
|
||||
prefix.push('0');
|
||||
prefix.push('.');
|
||||
prefix.push_str(&"0".repeat(-e_inc as usize));
|
||||
}
|
||||
|
||||
prefix + &suffix
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
use crate::tester::Tester;
|
||||
@@ -669,6 +163,10 @@ fn test() {
|
||||
// "var x = 0377777777777777777", // '0'-prefixed octal literals and octal escape sequences are deprecated
|
||||
"var x = 0x1FFFFFFFFFFFFF",
|
||||
"var x = 0X1FFFFFFFFFFFFF",
|
||||
"var x = 0x10000000000000000",
|
||||
"var x = 0X1_0000_0000_0000_0000",
|
||||
"var x = 0o2000000000000000000000",
|
||||
"var x = 0b10000000000000000000000000000000000000000000000000000000000000000",
|
||||
"var x = true",
|
||||
"var x = 'abc'",
|
||||
"var x = ''",
|
||||
@@ -737,6 +235,10 @@ fn test() {
|
||||
"var x = 0400000000000000001",
|
||||
"var x = 0x20000000000001",
|
||||
"var x = 0X20000000000001",
|
||||
"var x = 0xffffffffffffffff",
|
||||
"var x = 0xffffffffffffffffffff",
|
||||
"var x = 0o1777777777777777777777",
|
||||
"var x = 0b1111111111111111111111111111111111111111111111111111111111111111",
|
||||
"var x = 5123_00000000000000000000000000_1", // { "ecmaVersion": 2021 },
|
||||
"var x = -5_12300000000000000000000_0000001", // { "ecmaVersion": 2021 },
|
||||
"var x = 123_00000000000000000000_00.0_0", // { "ecmaVersion": 2021 },
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
use oxc_ast::{AstKind, ast::Expression};
|
||||
use oxc_diagnostics::OxcDiagnostic;
|
||||
use oxc_macros::declare_oxc_lint;
|
||||
use oxc_semantic::IsGlobalReference;
|
||||
use oxc_span::{GetSpan, Span};
|
||||
use oxc_syntax::number::NumberBase;
|
||||
|
||||
use crate::{AstNode, context::LintContext, rule::Rule};
|
||||
use crate::{
|
||||
AstNode,
|
||||
context::LintContext,
|
||||
rule::Rule,
|
||||
utils::{integer_literal_loses_precision, pad_fix_with_token_boundary},
|
||||
};
|
||||
|
||||
fn prefer_bigint_literals_diagnostic(span: Span) -> OxcDiagnostic {
|
||||
OxcDiagnostic::warn("Prefer bigint literals over `BigInt(...)`.")
|
||||
@@ -64,7 +70,11 @@ impl Rule for PreferBigintLiterals {
|
||||
return;
|
||||
};
|
||||
|
||||
if reference.name != "BigInt" || call.optional || call.arguments.len() != 1 {
|
||||
if reference.name != "BigInt"
|
||||
|| !reference.is_global_reference(ctx.scoping())
|
||||
|| call.optional
|
||||
|| call.arguments.len() != 1
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -75,15 +85,27 @@ impl Rule for PreferBigintLiterals {
|
||||
};
|
||||
|
||||
if argument_expression.is_big_int_literal() {
|
||||
ctx.diagnostic_with_fix(prefer_bigint_literals_diagnostic(arg.span()), |fixer| {
|
||||
let mut replacement = ctx.source_range(argument_expression.span()).to_string();
|
||||
pad_fix_with_token_boundary(ctx.source_text(), call.span, &mut replacement);
|
||||
fixer.replace(call.span, replacement)
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
match argument_expression.get_inner_expression() {
|
||||
Expression::StringLiteral(string_literal) => {
|
||||
if let Some(replacement) = bigint_literal_from_string(&string_literal.value) {
|
||||
if let Some(mut replacement) = bigint_literal_from_string(&string_literal.value) {
|
||||
ctx.diagnostic_with_fix(
|
||||
prefer_bigint_literals_diagnostic(arg.span()),
|
||||
|fixer| fixer.replace(call.span, replacement),
|
||||
|fixer| {
|
||||
pad_fix_with_token_boundary(
|
||||
ctx.source_text(),
|
||||
call.span,
|
||||
&mut replacement,
|
||||
);
|
||||
fixer.replace(call.span, replacement)
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -100,12 +122,24 @@ impl Rule for PreferBigintLiterals {
|
||||
|raw| raw.as_str(),
|
||||
);
|
||||
|
||||
if let Some(replacement) =
|
||||
if integer_literal_loses_precision(numeric_literal) {
|
||||
ctx.diagnostic(
|
||||
prefer_bigint_literals_diagnostic(arg.span())
|
||||
.with_note("Integer literal loses precision"),
|
||||
);
|
||||
} else if let Some(mut replacement) =
|
||||
bigint_literal_from_numeric(raw_text, numeric_literal.base)
|
||||
{
|
||||
ctx.diagnostic_with_fix(
|
||||
prefer_bigint_literals_diagnostic(arg.span()),
|
||||
|fixer| fixer.replace(call.span, replacement),
|
||||
|fixer| {
|
||||
pad_fix_with_token_boundary(
|
||||
ctx.source_text(),
|
||||
call.span,
|
||||
&mut replacement,
|
||||
);
|
||||
fixer.replace(call.span, replacement)
|
||||
},
|
||||
);
|
||||
} else {
|
||||
ctx.diagnostic(prefer_bigint_literals_diagnostic(arg.span()));
|
||||
@@ -206,7 +240,11 @@ fn test() {
|
||||
r"BigInt?.(1)",
|
||||
r"BigInt(1.1)",
|
||||
r"typeof BigInt",
|
||||
r"BigInt(1n)",
|
||||
r"((BigInt) => BigInt(1n))(() => 2n)",
|
||||
r"function f(BigInt) { return BigInt(1); }",
|
||||
r"const BigInt = () => 2n; BigInt(1n);",
|
||||
r"import BigInt from 'custom-bigint'; BigInt('1');",
|
||||
r"function BigInt() { return 2n; } BigInt(1n);",
|
||||
r#"BigInt("not-number")"#,
|
||||
r#"BigInt("1_2")"#,
|
||||
r#"BigInt("1\\\n2")"#,
|
||||
@@ -235,16 +273,53 @@ fn test() {
|
||||
r"BigInt(0x20000000000001)",
|
||||
r"BigInt(9_007_199_254_740_993)",
|
||||
r"BigInt(0x20_00_00_00_00_00_01)",
|
||||
r"BigInt(0777777777777777777)",
|
||||
r"BigInt(1000000000000000100)",
|
||||
r"BigInt(100000000000000000000000)",
|
||||
r"BigInt(100_000_000_000_000_010_000)",
|
||||
r"BigInt(0xffffffffffffffff)",
|
||||
r"BigInt(0xffffffffffffffffffff)",
|
||||
r"BigInt(0b1111111111111111111111111111111111111111111111111111111111111111)",
|
||||
r"BigInt(0o1777777777777777777777)",
|
||||
r"BigInt(9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999)",
|
||||
];
|
||||
|
||||
let fix = vec![
|
||||
(r"BigInt('42')", "42n"),
|
||||
(r"BigInt(' 0xFF ')", "0xFFn"),
|
||||
(r"BigInt(0)", "0n"),
|
||||
(r"BigInt(1n)", r"1n"),
|
||||
(r"BigInt(1n)in {'1':0}", r"1n in {'1':0}"),
|
||||
(r"BigInt(1n)instanceof Object", r"1n instanceof Object"),
|
||||
(r"BigInt(1n)as bigint", r"1n as bigint"),
|
||||
(r"BigInt(1n)satisfies bigint", r"1n satisfies bigint"),
|
||||
(r"BigInt(1)in {'1':0}", r"1n in {'1':0}"),
|
||||
(r"BigInt('1')instanceof Object", r"1n instanceof Object"),
|
||||
(r"BigInt(1n).toString()", r"1n.toString()"),
|
||||
(r"BigInt(0B11_11)", "0B11_11n"),
|
||||
(r"BigInt(0O777_777)", "0O777_777n"),
|
||||
(r"BigInt(0777)", "0o777n"),
|
||||
(r"BigInt(0888)", "888n"),
|
||||
(r"BigInt(1000000000000000100)", r"BigInt(1000000000000000100)"),
|
||||
(r"BigInt(100000000000000000000000)", r"BigInt(100000000000000000000000)"),
|
||||
(r"BigInt(100_000_000_000_000_010_000)", r"BigInt(100_000_000_000_000_010_000)"),
|
||||
(r"BigInt(1000000000000000000)", "1000000000000000000n"),
|
||||
(r"BigInt(9007199254740992)", "9007199254740992n"),
|
||||
(r"BigInt(9_007_199_254_740_992)", "9_007_199_254_740_992n"),
|
||||
(r"BigInt(0xffffffffffffffff)", r"BigInt(0xffffffffffffffff)"),
|
||||
(r"BigInt(0xffffffffffffffffffff)", r"BigInt(0xffffffffffffffffffff)"),
|
||||
(
|
||||
r"BigInt(0b1111111111111111111111111111111111111111111111111111111111111111)",
|
||||
r"BigInt(0b1111111111111111111111111111111111111111111111111111111111111111)",
|
||||
),
|
||||
(r"BigInt(0o1777777777777777777777)", r"BigInt(0o1777777777777777777777)"),
|
||||
(r"BigInt(0x10000000000000000)", "0x10000000000000000n"),
|
||||
(r"BigInt(0X1_0000_0000_0000_0000)", "0X1_0000_0000_0000_0000n"),
|
||||
(r"BigInt(0o2000000000000000000000)", "0o2000000000000000000000n"),
|
||||
(
|
||||
r"BigInt(0b10000000000000000000000000000000000000000000000000000000000000000)",
|
||||
"0b10000000000000000000000000000000000000000000000000000000000000000n",
|
||||
),
|
||||
(r#"BigInt("0777")"#, "777n"),
|
||||
(r#"BigInt("0888")"#, "888n"),
|
||||
(r#"BigInt("0b1010")"#, "0b1010n"),
|
||||
@@ -255,10 +330,6 @@ fn test() {
|
||||
r"BigInt('9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999')",
|
||||
"9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999n",
|
||||
),
|
||||
(
|
||||
r"BigInt(9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999)",
|
||||
"9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999n",
|
||||
),
|
||||
(r"BigInt(1e2)", r"BigInt(1e2)"),
|
||||
(r"BigInt(1e6)", r"BigInt(1e6)"),
|
||||
(r"BigInt(1E6)", r"BigInt(1E6)"),
|
||||
|
||||
@@ -298,6 +298,38 @@ source: crates/oxc_linter/src/tester.rs
|
||||
help: Use a number literal representable by a 64-bit floating-point number, or use a `BigInt` literal (for example, `9007199254740993n`) for exact large integers.
|
||||
note: In JavaScript, `Number` values exactly represent integers only in the range -9007199254740991 to 9007199254740991 (`Number.MIN_SAFE_INTEGER` to `Number.MAX_SAFE_INTEGER`). `BigInt` supports arbitrarily large integers.
|
||||
|
||||
⚠ eslint(no-loss-of-precision): This number literal will lose precision at runtime.
|
||||
╭─[no_loss_of_precision.tsx:1:9]
|
||||
1 │ var x = 0xffffffffffffffff
|
||||
· ──────────────────
|
||||
╰────
|
||||
help: Use a number literal representable by a 64-bit floating-point number, or use a `BigInt` literal (for example, `9007199254740993n`) for exact large integers.
|
||||
note: In JavaScript, `Number` values exactly represent integers only in the range -9007199254740991 to 9007199254740991 (`Number.MIN_SAFE_INTEGER` to `Number.MAX_SAFE_INTEGER`). `BigInt` supports arbitrarily large integers.
|
||||
|
||||
⚠ eslint(no-loss-of-precision): This number literal will lose precision at runtime.
|
||||
╭─[no_loss_of_precision.tsx:1:9]
|
||||
1 │ var x = 0xffffffffffffffffffff
|
||||
· ──────────────────────
|
||||
╰────
|
||||
help: Use a number literal representable by a 64-bit floating-point number, or use a `BigInt` literal (for example, `9007199254740993n`) for exact large integers.
|
||||
note: In JavaScript, `Number` values exactly represent integers only in the range -9007199254740991 to 9007199254740991 (`Number.MIN_SAFE_INTEGER` to `Number.MAX_SAFE_INTEGER`). `BigInt` supports arbitrarily large integers.
|
||||
|
||||
⚠ eslint(no-loss-of-precision): This number literal will lose precision at runtime.
|
||||
╭─[no_loss_of_precision.tsx:1:9]
|
||||
1 │ var x = 0o1777777777777777777777
|
||||
· ────────────────────────
|
||||
╰────
|
||||
help: Use a number literal representable by a 64-bit floating-point number, or use a `BigInt` literal (for example, `9007199254740993n`) for exact large integers.
|
||||
note: In JavaScript, `Number` values exactly represent integers only in the range -9007199254740991 to 9007199254740991 (`Number.MIN_SAFE_INTEGER` to `Number.MAX_SAFE_INTEGER`). `BigInt` supports arbitrarily large integers.
|
||||
|
||||
⚠ eslint(no-loss-of-precision): This number literal will lose precision at runtime.
|
||||
╭─[no_loss_of_precision.tsx:1:9]
|
||||
1 │ var x = 0b1111111111111111111111111111111111111111111111111111111111111111
|
||||
· ──────────────────────────────────────────────────────────────────
|
||||
╰────
|
||||
help: Use a number literal representable by a 64-bit floating-point number, or use a `BigInt` literal (for example, `9007199254740993n`) for exact large integers.
|
||||
note: In JavaScript, `Number` values exactly represent integers only in the range -9007199254740991 to 9007199254740991 (`Number.MIN_SAFE_INTEGER` to `Number.MAX_SAFE_INTEGER`). `BigInt` supports arbitrarily large integers.
|
||||
|
||||
⚠ eslint(no-loss-of-precision): This number literal will lose precision at runtime.
|
||||
╭─[no_loss_of_precision.tsx:1:9]
|
||||
1 │ var x = 5123_00000000000000000000000000_1
|
||||
|
||||
@@ -127,6 +127,7 @@ source: crates/oxc_linter/src/tester.rs
|
||||
· ────────────────
|
||||
╰────
|
||||
help: Use a bigint literal (e.g. `123n`) instead of calling `BigInt` with a literal argument.
|
||||
note: Integer literal loses precision
|
||||
|
||||
⚠ unicorn(prefer-bigint-literals): Prefer bigint literals over `BigInt(...)`.
|
||||
╭─[prefer_bigint_literals.tsx:1:8]
|
||||
@@ -134,6 +135,7 @@ source: crates/oxc_linter/src/tester.rs
|
||||
· ────────────────
|
||||
╰────
|
||||
help: Use a bigint literal (e.g. `123n`) instead of calling `BigInt` with a literal argument.
|
||||
note: Integer literal loses precision
|
||||
|
||||
⚠ unicorn(prefer-bigint-literals): Prefer bigint literals over `BigInt(...)`.
|
||||
╭─[prefer_bigint_literals.tsx:1:8]
|
||||
@@ -141,6 +143,7 @@ source: crates/oxc_linter/src/tester.rs
|
||||
· ─────────────────────
|
||||
╰────
|
||||
help: Use a bigint literal (e.g. `123n`) instead of calling `BigInt` with a literal argument.
|
||||
note: Integer literal loses precision
|
||||
|
||||
⚠ unicorn(prefer-bigint-literals): Prefer bigint literals over `BigInt(...)`.
|
||||
╭─[prefer_bigint_literals.tsx:1:8]
|
||||
@@ -148,3 +151,76 @@ source: crates/oxc_linter/src/tester.rs
|
||||
· ──────────────────────
|
||||
╰────
|
||||
help: Use a bigint literal (e.g. `123n`) instead of calling `BigInt` with a literal argument.
|
||||
note: Integer literal loses precision
|
||||
|
||||
⚠ unicorn(prefer-bigint-literals): Prefer bigint literals over `BigInt(...)`.
|
||||
╭─[prefer_bigint_literals.tsx:1:8]
|
||||
1 │ BigInt(0777777777777777777)
|
||||
· ───────────────────
|
||||
╰────
|
||||
help: Use a bigint literal (e.g. `123n`) instead of calling `BigInt` with a literal argument.
|
||||
note: Integer literal loses precision
|
||||
|
||||
⚠ unicorn(prefer-bigint-literals): Prefer bigint literals over `BigInt(...)`.
|
||||
╭─[prefer_bigint_literals.tsx:1:8]
|
||||
1 │ BigInt(1000000000000000100)
|
||||
· ───────────────────
|
||||
╰────
|
||||
help: Use a bigint literal (e.g. `123n`) instead of calling `BigInt` with a literal argument.
|
||||
note: Integer literal loses precision
|
||||
|
||||
⚠ unicorn(prefer-bigint-literals): Prefer bigint literals over `BigInt(...)`.
|
||||
╭─[prefer_bigint_literals.tsx:1:8]
|
||||
1 │ BigInt(100000000000000000000000)
|
||||
· ────────────────────────
|
||||
╰────
|
||||
help: Use a bigint literal (e.g. `123n`) instead of calling `BigInt` with a literal argument.
|
||||
note: Integer literal loses precision
|
||||
|
||||
⚠ unicorn(prefer-bigint-literals): Prefer bigint literals over `BigInt(...)`.
|
||||
╭─[prefer_bigint_literals.tsx:1:8]
|
||||
1 │ BigInt(100_000_000_000_000_010_000)
|
||||
· ───────────────────────────
|
||||
╰────
|
||||
help: Use a bigint literal (e.g. `123n`) instead of calling `BigInt` with a literal argument.
|
||||
note: Integer literal loses precision
|
||||
|
||||
⚠ unicorn(prefer-bigint-literals): Prefer bigint literals over `BigInt(...)`.
|
||||
╭─[prefer_bigint_literals.tsx:1:8]
|
||||
1 │ BigInt(0xffffffffffffffff)
|
||||
· ──────────────────
|
||||
╰────
|
||||
help: Use a bigint literal (e.g. `123n`) instead of calling `BigInt` with a literal argument.
|
||||
note: Integer literal loses precision
|
||||
|
||||
⚠ unicorn(prefer-bigint-literals): Prefer bigint literals over `BigInt(...)`.
|
||||
╭─[prefer_bigint_literals.tsx:1:8]
|
||||
1 │ BigInt(0xffffffffffffffffffff)
|
||||
· ──────────────────────
|
||||
╰────
|
||||
help: Use a bigint literal (e.g. `123n`) instead of calling `BigInt` with a literal argument.
|
||||
note: Integer literal loses precision
|
||||
|
||||
⚠ unicorn(prefer-bigint-literals): Prefer bigint literals over `BigInt(...)`.
|
||||
╭─[prefer_bigint_literals.tsx:1:8]
|
||||
1 │ BigInt(0b1111111111111111111111111111111111111111111111111111111111111111)
|
||||
· ──────────────────────────────────────────────────────────────────
|
||||
╰────
|
||||
help: Use a bigint literal (e.g. `123n`) instead of calling `BigInt` with a literal argument.
|
||||
note: Integer literal loses precision
|
||||
|
||||
⚠ unicorn(prefer-bigint-literals): Prefer bigint literals over `BigInt(...)`.
|
||||
╭─[prefer_bigint_literals.tsx:1:8]
|
||||
1 │ BigInt(0o1777777777777777777777)
|
||||
· ────────────────────────
|
||||
╰────
|
||||
help: Use a bigint literal (e.g. `123n`) instead of calling `BigInt` with a literal argument.
|
||||
note: Integer literal loses precision
|
||||
|
||||
⚠ unicorn(prefer-bigint-literals): Prefer bigint literals over `BigInt(...)`.
|
||||
╭─[prefer_bigint_literals.tsx:1:8]
|
||||
1 │ BigInt(9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999)
|
||||
· ────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
╰────
|
||||
help: Use a bigint literal (e.g. `123n`) instead of calling `BigInt` with a literal argument.
|
||||
note: Integer literal loses precision
|
||||
@@ -22,6 +22,7 @@ mod jest;
|
||||
mod jsdoc;
|
||||
mod nextjs;
|
||||
mod node;
|
||||
mod number;
|
||||
mod promise;
|
||||
mod react;
|
||||
mod react_compiler;
|
||||
@@ -39,8 +40,8 @@ pub mod vue_casing;
|
||||
|
||||
pub use self::{
|
||||
comment::*, config::*, control_flow::*, express::*, jest::*, jsdoc::*, nextjs::*, node::*,
|
||||
promise::*, react::*, react_compiler::*, react_perf::*, regex::*, schemars::*, static_value::*,
|
||||
this_expression::*, typescript::*, unicorn::*, url::*, vitest::*, vue::*,
|
||||
number::*, promise::*, react::*, react_compiler::*, react_perf::*, regex::*, schemars::*,
|
||||
static_value::*, this_expression::*, typescript::*, unicorn::*, url::*, vitest::*, vue::*,
|
||||
};
|
||||
|
||||
/// List of Eslint rules that have TypeScript equivalents.
|
||||
|
||||
@@ -0,0 +1,514 @@
|
||||
use cow_utils::CowUtils;
|
||||
use oxc_ast::ast::NumericLiteral;
|
||||
use oxc_syntax::number::NumberBase;
|
||||
use std::borrow::Cow;
|
||||
|
||||
const MAX_OBVIOUSLY_SAFE_SIGNIFICANT_DIGITS: usize = 15;
|
||||
|
||||
struct RawNum<'a> {
|
||||
int: &'a str,
|
||||
frac: &'a str,
|
||||
exp: isize,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ScientificNotation<'a> {
|
||||
int: &'a str,
|
||||
frac: Cow<'a, str>,
|
||||
exp: isize,
|
||||
}
|
||||
|
||||
impl PartialEq for ScientificNotation<'_> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
if self.int == other.int && self.frac == other.frac {
|
||||
if self.int == "0" && self.frac.is_empty() {
|
||||
return true;
|
||||
}
|
||||
return self.exp == other.exp;
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> RawNum<'a> {
|
||||
fn new(num: &str) -> Option<RawNum<'_>> {
|
||||
// remove sign
|
||||
let num = num.trim_start_matches(['+', '-']);
|
||||
|
||||
let (int, num_without_int) = {
|
||||
// skip leading zeros
|
||||
let num_without_zeros = num.trim_start_matches('0');
|
||||
|
||||
// read the integer part and store the end index
|
||||
let int_end = num_without_zeros
|
||||
.chars()
|
||||
.position(|ch| !ch.is_ascii_digit())
|
||||
.unwrap_or(num_without_zeros.len());
|
||||
|
||||
// if no integer part was found, default to 0
|
||||
let int = if int_end == 0 { "0" } else { &num_without_zeros[..int_end] };
|
||||
|
||||
// make a slice of the rest of the string
|
||||
let num_without_int = &num_without_zeros[int_end..];
|
||||
|
||||
(int, num_without_int)
|
||||
};
|
||||
|
||||
// if next char is a dot, parse the fractional part
|
||||
let (frac, num_without_frac) =
|
||||
num_without_int.strip_prefix('.').map_or(("", num_without_int), |num_without_dot| {
|
||||
let frac_end = num_without_dot
|
||||
.chars()
|
||||
.position(|ch| !ch.is_ascii_digit())
|
||||
.unwrap_or(num_without_dot.len());
|
||||
|
||||
// slice the fractional part and the rest of the string
|
||||
let frac = &num_without_dot[..frac_end];
|
||||
let num_without_frac = &num_without_dot[frac_end..];
|
||||
|
||||
(frac, num_without_frac)
|
||||
});
|
||||
|
||||
// if next char is an e, treat the rest as the exponent
|
||||
let exp =
|
||||
num_without_frac.strip_prefix(['e', 'E']).map_or("0", |num_without_e| num_without_e);
|
||||
|
||||
let Ok(exp) = exp.parse::<isize>() else {
|
||||
return None;
|
||||
};
|
||||
|
||||
Some(RawNum { int, frac, exp })
|
||||
}
|
||||
|
||||
fn normalize(&mut self, parse_as_float: bool) -> ScientificNotation<'a> {
|
||||
if self.int == "0" && !self.frac.is_empty() {
|
||||
let frac_zeros = self.frac.chars().take_while(|&ch| ch == '0').count();
|
||||
#[expect(clippy::cast_possible_wrap)]
|
||||
let exp = self.exp - 1 - frac_zeros as isize;
|
||||
self.frac = &self.frac[frac_zeros..];
|
||||
|
||||
match self.frac.len() {
|
||||
0 => ScientificNotation { int: "0", frac: Cow::Borrowed(""), exp },
|
||||
1 => ScientificNotation { int: &self.frac[..1], frac: Cow::Borrowed(""), exp },
|
||||
_ => ScientificNotation {
|
||||
int: &self.frac[..1],
|
||||
frac: Cow::Borrowed(&self.frac[1..]),
|
||||
exp,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
#[expect(clippy::cast_possible_wrap)]
|
||||
let exp = self.exp + self.int.len() as isize - 1;
|
||||
if self.int.len() == 1 {
|
||||
ScientificNotation { int: self.int, frac: Cow::Borrowed(self.frac), exp }
|
||||
} else {
|
||||
let frac = if self.frac.is_empty() {
|
||||
if parse_as_float {
|
||||
Cow::Borrowed(&self.int[1..])
|
||||
} else {
|
||||
let int_trimmed = self.int.trim_end_matches('0');
|
||||
if int_trimmed.len() == 1 {
|
||||
Cow::Borrowed("")
|
||||
} else {
|
||||
Cow::Borrowed(&int_trimmed[1..])
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Cow::Owned(format!("{}{}", &self.int[1..], self.frac))
|
||||
};
|
||||
|
||||
ScientificNotation { int: &self.int[..1], frac, exp }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn strip_numeric_separators(raw: &str) -> Cow<'_, str> {
|
||||
if !raw.as_bytes().contains(&b'_') {
|
||||
return Cow::Borrowed(raw);
|
||||
}
|
||||
raw.cow_replace('_', "")
|
||||
}
|
||||
|
||||
fn non_base_ten_literal_is_exact(raw: &str) -> bool {
|
||||
let raw = raw.trim_start_matches(['+', '-']);
|
||||
let (radix, digits) =
|
||||
if let Some(digits) = raw.strip_prefix("0b").or_else(|| raw.strip_prefix("0B")) {
|
||||
(2, digits)
|
||||
} else if let Some(digits) = raw.strip_prefix("0o").or_else(|| raw.strip_prefix("0O")) {
|
||||
(8, digits)
|
||||
} else if let Some(digits) = raw.strip_prefix("0x").or_else(|| raw.strip_prefix("0X")) {
|
||||
(16, digits)
|
||||
} else {
|
||||
(8, raw.trim_start_matches('0'))
|
||||
};
|
||||
|
||||
let bits_per_digit = match radix {
|
||||
2 => 1,
|
||||
8 => 3,
|
||||
_ => 4,
|
||||
};
|
||||
let mut bit_len = 0;
|
||||
let mut trailing_zero_bits = 0;
|
||||
for byte in digits.bytes() {
|
||||
if byte == b'_' {
|
||||
continue;
|
||||
}
|
||||
|
||||
let digit = match byte {
|
||||
b'0'..=b'9' => byte - b'0',
|
||||
b'a'..=b'f' => byte - b'a' + 10,
|
||||
b'A'..=b'F' => byte - b'A' + 10,
|
||||
_ => return false,
|
||||
};
|
||||
|
||||
if bit_len == 0 {
|
||||
if digit == 0 {
|
||||
continue;
|
||||
}
|
||||
bit_len = match radix {
|
||||
2 => 1,
|
||||
8 => match digit {
|
||||
1 => 1,
|
||||
2 | 3 => 2,
|
||||
_ => 3,
|
||||
},
|
||||
_ => match digit {
|
||||
1 => 1,
|
||||
2 | 3 => 2,
|
||||
4..=7 => 3,
|
||||
_ => 4,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
bit_len += bits_per_digit;
|
||||
}
|
||||
|
||||
trailing_zero_bits = if digit == 0 {
|
||||
trailing_zero_bits + bits_per_digit
|
||||
} else {
|
||||
digit.trailing_zeros() as usize
|
||||
};
|
||||
}
|
||||
|
||||
// A Number can represent up to 53 significant binary digits. Trailing zero bits
|
||||
// are carried by its exponent and do not consume significand precision.
|
||||
bit_len - trailing_zero_bits <= 53
|
||||
}
|
||||
|
||||
fn base_ten_literal_is_safe(raw: &str, value: f64) -> bool {
|
||||
if !value.is_finite() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let raw = raw.trim_start_matches(['+', '-']);
|
||||
let exponent_start = raw.find(['e', 'E']).unwrap_or(raw.len());
|
||||
if exponent_start != raw.len() {
|
||||
return false;
|
||||
}
|
||||
let mantissa = &raw[..exponent_start];
|
||||
|
||||
let mut digit_index = 0;
|
||||
let mut first_non_zero = None;
|
||||
let mut last_non_zero = None;
|
||||
let mut fractional_has_non_zero = false;
|
||||
let mut dot_digit_index = None;
|
||||
|
||||
for byte in mantissa.bytes() {
|
||||
match byte {
|
||||
b'_' => continue,
|
||||
b'.' => {
|
||||
dot_digit_index = Some(digit_index);
|
||||
continue;
|
||||
}
|
||||
b'0'..=b'9' => {}
|
||||
_ => return false,
|
||||
}
|
||||
|
||||
if byte != b'0' {
|
||||
first_non_zero.get_or_insert(digit_index);
|
||||
last_non_zero = Some(digit_index);
|
||||
fractional_has_non_zero |= dot_digit_index.is_some();
|
||||
}
|
||||
digit_index += 1;
|
||||
}
|
||||
|
||||
let Some(first_non_zero) = first_non_zero else {
|
||||
return true;
|
||||
};
|
||||
if value == 0.0 {
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Some(dot_digit_index) = dot_digit_index
|
||||
&& !fractional_has_non_zero
|
||||
{
|
||||
return dot_digit_index - first_non_zero <= MAX_OBVIOUSLY_SAFE_SIGNIFICANT_DIGITS;
|
||||
}
|
||||
|
||||
let last_significant =
|
||||
if dot_digit_index.is_some() { digit_index - 1 } else { last_non_zero.unwrap() };
|
||||
last_significant - first_non_zero < MAX_OBVIOUSLY_SAFE_SIGNIFICANT_DIGITS
|
||||
}
|
||||
|
||||
fn not_base_ten_loses_precision(node: &'_ NumericLiteral) -> bool {
|
||||
let raw = node.raw.as_ref().unwrap().as_str();
|
||||
!node.value.is_finite() || !non_base_ten_literal_is_exact(raw)
|
||||
}
|
||||
|
||||
fn base_ten_loses_precision(node: &'_ NumericLiteral) -> bool {
|
||||
let raw = node.raw.as_ref().unwrap().as_str();
|
||||
if base_ten_literal_is_safe(raw, node.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let raw = strip_numeric_separators(raw);
|
||||
let Some(raw) = normalize(&raw, false) else {
|
||||
return true;
|
||||
};
|
||||
|
||||
let total_significant_digits = raw.int.len() + raw.frac.len();
|
||||
|
||||
if total_significant_digits > 100 {
|
||||
return true;
|
||||
}
|
||||
|
||||
let stored = to_precision(node.value, total_significant_digits);
|
||||
|
||||
let Some(stored) = normalize(&stored, true) else {
|
||||
return true;
|
||||
};
|
||||
raw != stored
|
||||
}
|
||||
|
||||
fn normalize(num: &str, parse_as_float: bool) -> Option<ScientificNotation<'_>> {
|
||||
let coefficient = num.trim_start_matches(['+', '-']);
|
||||
let exp_start =
|
||||
coefficient.find('e').or_else(|| coefficient.find('E')).unwrap_or(coefficient.len());
|
||||
let parse_as_float = parse_as_float || coefficient[..exp_start].contains('.');
|
||||
Some(RawNum::new(num)?.normalize(parse_as_float))
|
||||
}
|
||||
|
||||
pub fn loses_precision(node: &'_ NumericLiteral) -> bool {
|
||||
if node.base.is_base_10() {
|
||||
base_ten_loses_precision(node)
|
||||
} else {
|
||||
not_base_ten_loses_precision(node)
|
||||
}
|
||||
}
|
||||
|
||||
/// Unlike `loses_precision`, integer fixes must preserve every digit, including trailing zeros.
|
||||
pub fn integer_literal_loses_precision(node: &NumericLiteral) -> bool {
|
||||
let raw = node.raw.as_ref().unwrap().as_str();
|
||||
if node.base != NumberBase::Decimal || raw.contains(['e', 'E']) {
|
||||
return loses_precision(node);
|
||||
}
|
||||
|
||||
// Decimal integer literals in the safe integer range are always exact.
|
||||
if node.value <= 9_007_199_254_740_991.0 {
|
||||
return false;
|
||||
}
|
||||
|
||||
let stored = format!("{:.0}", node.value);
|
||||
!raw.bytes()
|
||||
.filter(|&byte| byte != b'_')
|
||||
.skip_while(|&byte| byte == b'0')
|
||||
.eq(stored.bytes().skip_while(|&byte| byte == b'0'))
|
||||
}
|
||||
|
||||
/// `flt_str_to_exp` - used in `to_precision`
|
||||
///
|
||||
/// This function traverses a string representing a number,
|
||||
/// returning the floored log10 of this number.
|
||||
#[expect(clippy::cast_possible_truncation)]
|
||||
#[expect(clippy::cast_possible_wrap)]
|
||||
fn flt_str_to_exp(flt: &str) -> i32 {
|
||||
let mut non_zero_encountered = false;
|
||||
let mut dot_encountered = false;
|
||||
for (i, c) in flt.char_indices() {
|
||||
if c == '.' {
|
||||
if non_zero_encountered {
|
||||
return (i as i32) - 1;
|
||||
}
|
||||
dot_encountered = true;
|
||||
} else if c != '0' {
|
||||
if dot_encountered {
|
||||
return 1 - (i as i32);
|
||||
}
|
||||
non_zero_encountered = true;
|
||||
}
|
||||
}
|
||||
(flt.len() as i32) - 1
|
||||
}
|
||||
|
||||
/// `round_to_precision` - used in `to_precision`
|
||||
///
|
||||
/// This procedure has two roles:
|
||||
/// - If there are enough or more than enough digits in the
|
||||
/// string to show the required precision, the number
|
||||
/// represented by these digits is rounded using string
|
||||
/// manipulation.
|
||||
/// - Else, zeroes are appended to the string.
|
||||
/// - Additionally, sometimes the exponent was wrongly computed and
|
||||
/// while up-rounding we find that we need an extra digit. When this
|
||||
/// happens, we return true so that the calling context can adjust
|
||||
/// the exponent. The string is kept at an exact length of `precision`.
|
||||
///
|
||||
/// When this procedure returns, `digits` is exactly `precision` long.
|
||||
fn round_to_precision(digits: &mut String, precision: usize) -> bool {
|
||||
if digits.len() > precision {
|
||||
let to_round = digits.split_off(precision);
|
||||
let mut digit =
|
||||
digits.pop().expect("already checked that length is bigger than precision") as u8;
|
||||
if let Some(first) = to_round.chars().next()
|
||||
&& first > '4'
|
||||
{
|
||||
digit += 1;
|
||||
}
|
||||
|
||||
if digit as char == ':' {
|
||||
// ':' is '9' + 1
|
||||
// need to propagate the increment backward
|
||||
let mut replacement = String::from("0");
|
||||
let mut propagated = false;
|
||||
for c in digits.chars().rev() {
|
||||
let d = match (c, propagated) {
|
||||
('0'..='8', false) => (c as u8 + 1) as char,
|
||||
(_, false) => '0',
|
||||
(_, true) => c,
|
||||
};
|
||||
replacement.push(d);
|
||||
if d != '0' {
|
||||
propagated = true;
|
||||
}
|
||||
}
|
||||
digits.clear();
|
||||
let replacement = if propagated {
|
||||
replacement.as_str()
|
||||
} else {
|
||||
digits.push('1');
|
||||
&replacement.as_str()[1..]
|
||||
};
|
||||
for c in replacement.chars().rev() {
|
||||
digits.push(c);
|
||||
}
|
||||
!propagated
|
||||
} else {
|
||||
digits.push(digit as char);
|
||||
false
|
||||
}
|
||||
} else {
|
||||
digits.push_str(&"0".repeat(precision - digits.len()));
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn fractional_digits_for_precision(num: f64, precision: usize) -> usize {
|
||||
let exponent = format!("{num:e}")
|
||||
.rsplit_once('e')
|
||||
.and_then(|(_, exp)| exp.parse::<isize>().ok())
|
||||
.unwrap_or_default();
|
||||
|
||||
if exponent.is_negative() {
|
||||
exponent.unsigned_abs().saturating_add(precision).saturating_add(1).max(100)
|
||||
} else {
|
||||
100
|
||||
}
|
||||
}
|
||||
|
||||
/// Mimics JavaScript's `Number.prototype.toPrecision()` method
|
||||
///
|
||||
/// The `toPrecision()` method returns a string representing the Number object to the specified precision.
|
||||
///
|
||||
/// More information:
|
||||
/// - [ECMAScript reference][spec]
|
||||
/// - [MDN documentation][mdn]
|
||||
///
|
||||
/// [spec]: https://tc39.es/ecma262/#sec-number.prototype.toprecision
|
||||
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toPrecision
|
||||
#[expect(clippy::cast_possible_truncation)]
|
||||
#[expect(clippy::cast_possible_wrap)]
|
||||
#[expect(clippy::cast_sign_loss)]
|
||||
fn to_precision(mut num: f64, precision: usize) -> String {
|
||||
// Validate precision range (1-100)
|
||||
debug_assert!((1..=100).contains(&precision), "Precision must be between 1 and 100");
|
||||
|
||||
// Handle non-finite numbers
|
||||
if !num.is_finite() {
|
||||
if num.is_nan() {
|
||||
return "NaN".to_string();
|
||||
} else if num.is_infinite() {
|
||||
return if num.is_sign_positive() { "Infinity" } else { "-Infinity" }.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
let precision_i32 = precision as i32;
|
||||
|
||||
// Handle sign
|
||||
let mut prefix = String::new();
|
||||
if num < 0.0 {
|
||||
prefix.push('-');
|
||||
num = -num;
|
||||
}
|
||||
|
||||
let mut suffix: String;
|
||||
let mut exponent: i32;
|
||||
|
||||
// Handle zero
|
||||
if num == 0.0 {
|
||||
suffix = "0".repeat(precision);
|
||||
exponent = 0;
|
||||
} else {
|
||||
// For very small numbers (e.g. 3e-308), we need more than 100 fractional digits
|
||||
// to keep enough significant digits for precision comparison.
|
||||
let fractional_digits = fractional_digits_for_precision(num, precision);
|
||||
suffix = format!("{num:.fractional_digits$}");
|
||||
|
||||
// Calculate exponent
|
||||
exponent = flt_str_to_exp(&suffix);
|
||||
|
||||
// Extract relevant digits only
|
||||
if exponent < 0 {
|
||||
suffix = suffix.split_off((1 - exponent) as usize);
|
||||
} else if let Some(n) = suffix.find('.') {
|
||||
suffix.remove(n);
|
||||
}
|
||||
|
||||
// Round to the specified precision
|
||||
if round_to_precision(&mut suffix, precision) {
|
||||
exponent += 1;
|
||||
}
|
||||
|
||||
// Decide between scientific and fixed notation
|
||||
let great_exp = exponent >= precision_i32;
|
||||
if exponent < -6 || great_exp {
|
||||
// Use scientific notation
|
||||
if precision > 1 {
|
||||
suffix.insert(1, '.');
|
||||
}
|
||||
suffix.push('e');
|
||||
if great_exp {
|
||||
suffix.push('+');
|
||||
}
|
||||
suffix.push_str(&exponent.to_string());
|
||||
|
||||
return prefix + &suffix;
|
||||
}
|
||||
}
|
||||
|
||||
// Use fixed-point notation
|
||||
let e_inc = exponent + 1;
|
||||
if e_inc == precision_i32 {
|
||||
return prefix + &suffix;
|
||||
}
|
||||
|
||||
if exponent >= 0 {
|
||||
suffix.insert(e_inc as usize, '.');
|
||||
} else {
|
||||
prefix.push('0');
|
||||
prefix.push('.');
|
||||
prefix.push_str(&"0".repeat(-e_inc as usize));
|
||||
}
|
||||
|
||||
prefix + &suffix
|
||||
}
|
||||
Reference in new issue
Block a user