From b18a91312e33f13e6b82f5a3385fc6a5d5381796 Mon Sep 17 00:00:00 2001 From: Maneren <49210777+Maneren@users.noreply.github.com> Date: Wed, 23 Aug 2023 05:16:05 +0200 Subject: [PATCH] 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. --- .../src/rules/eslint/no_loss_of_precision.rs | 87 ++++++++++++------- 1 file changed, 56 insertions(+), 31 deletions(-) diff --git a/crates/oxc_linter/src/rules/eslint/no_loss_of_precision.rs b/crates/oxc_linter/src/rules/eslint/no_loss_of_precision.rs index 62da2400e..c30161e12 100644 --- a/crates/oxc_linter/src/rules/eslint/no_loss_of_precision.rs +++ b/crates/oxc_linter/src/rules/eslint/no_loss_of_precision.rs @@ -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 = Lazy::new(|| { - Regex::new(r"-?0*(?P0|[1-9]\d*)?(?:\.(?P\d+))?(?:[eE](?P[+-]?\d+))?").unwrap() -}); - impl<'a> RawNum<'a> { fn new(num: &str) -> Option> { - 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::() { - 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::() 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 }