mirror of
https://github.com/danbulant/oxc
synced 2026-05-25 04:42:10 +00:00
refactor loss-of-precision rule to remove the slow regex (#775)
This achieves around 2x speedup in the rule execution by using a manual parser instead of a regex.
This commit is contained in:
parent
9c50bc0b9c
commit
b18a91312e
1 changed files with 56 additions and 31 deletions
|
|
@ -1,4 +1,3 @@
|
|||
use once_cell::sync::Lazy;
|
||||
use oxc_ast::ast::NumberLiteral;
|
||||
use oxc_ast::AstKind;
|
||||
use oxc_diagnostics::{
|
||||
|
|
@ -7,7 +6,6 @@ use oxc_diagnostics::{
|
|||
};
|
||||
use oxc_macros::declare_oxc_lint;
|
||||
use oxc_span::Span;
|
||||
use regex::Regex;
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crate::{context::LintContext, rule::Rule, AstNode};
|
||||
|
|
@ -77,35 +75,62 @@ impl PartialEq for ScientificNotation<'_> {
|
|||
}
|
||||
}
|
||||
|
||||
static RE: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new(r"-?0*(?P<int>0|[1-9]\d*)?(?:\.(?P<frac>\d+))?(?:[eE](?P<exp>[+-]?\d+))?").unwrap()
|
||||
});
|
||||
|
||||
impl<'a> RawNum<'a> {
|
||||
fn new(num: &str) -> Option<RawNum<'_>> {
|
||||
if let Some(captures) = RE.captures(num) {
|
||||
let int = captures.name("int").map_or("0", |m| m.as_str());
|
||||
let frac = captures.name("frac").map_or("", |m| m.as_str());
|
||||
let exp = captures.name("exp").map_or("0", |m| m.as_str());
|
||||
// remove sign
|
||||
let num = num.trim_start_matches(['+', '-']);
|
||||
|
||||
let exp = match exp.parse::<isize>() {
|
||||
Ok(x) => x,
|
||||
Err(_) => return None,
|
||||
};
|
||||
let (int, num_without_int) = {
|
||||
// skip leading zeros
|
||||
let num_without_zeros = num.trim_start_matches('0');
|
||||
|
||||
Some(RawNum { int, frac, exp })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
// 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) -> ScientificNotation<'a> {
|
||||
let scientific = self.exp != 0;
|
||||
let precision = self.frac.len();
|
||||
if self.int.starts_with('0') {
|
||||
let frac_zeros = self.frac.chars().take_while(|&ch| ch == '0').count();
|
||||
#[allow(clippy::cast_possible_wrap)]
|
||||
let exp = self.exp - 1 - self.frac.chars().take_while(|&ch| ch == '0').count() as isize;
|
||||
self.frac = self.frac.trim_start_matches('0');
|
||||
let exp = self.exp - 1 - frac_zeros as isize;
|
||||
self.frac = &self.frac[frac_zeros..];
|
||||
|
||||
match self.frac.len() {
|
||||
0 => ScientificNotation {
|
||||
|
|
@ -142,17 +167,17 @@ impl<'a> RawNum<'a> {
|
|||
precision,
|
||||
}
|
||||
} else {
|
||||
ScientificNotation {
|
||||
int: &self.int[..1],
|
||||
frac: Cow::Owned(
|
||||
let frac = if self.frac.is_empty() {
|
||||
Cow::Borrowed(&self.int[1..])
|
||||
} else {
|
||||
Cow::Owned(
|
||||
format!("{}{}", &self.int[1..], self.frac)
|
||||
.trim_end_matches('0')
|
||||
.to_string(),
|
||||
),
|
||||
exp,
|
||||
scientific,
|
||||
precision,
|
||||
}
|
||||
)
|
||||
};
|
||||
|
||||
ScientificNotation { int: &self.int[..1], frac, exp, scientific, precision }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -184,17 +209,17 @@ impl NoLossOfPrecision {
|
|||
|
||||
fn base_ten_loses_precision(node: &'_ NumberLiteral) -> bool {
|
||||
let raw = Self::get_raw(node);
|
||||
let raw = if let Some(s) = Self::normalize(&raw) { s } else { return true };
|
||||
let Some(raw) = Self::normalize(&raw) else { return true };
|
||||
|
||||
if raw.frac.len() >= 100 {
|
||||
return true;
|
||||
}
|
||||
let stored = match (raw.scientific, raw.precision) {
|
||||
(true, _) => format!("{:.1$e}", node.value, raw.frac.len()),
|
||||
(false, 0) => format!("{}", node.value),
|
||||
(false, 0) => node.value.to_string(),
|
||||
(false, precision) => format!("{:.1$}", node.value, precision),
|
||||
};
|
||||
let stored = if let Some(s) = Self::normalize(&stored) { s } else { return true };
|
||||
let Some(stored) = Self::normalize(&stored) else { return true };
|
||||
raw != stored
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue