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:
Maneren 2023-08-23 05:16:05 +02:00 committed by GitHub
parent 9c50bc0b9c
commit b18a91312e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23

View file

@ -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
}