diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 80b54221d..bfc0a1557 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -246,6 +246,7 @@ mod jsx_a11y { } mod oxc { + pub mod approx_constant; pub mod const_comparisons; pub mod double_comparisons; pub mod no_accumulating_spread; @@ -467,6 +468,7 @@ oxc_macros::declare_all_lint_rules! { jsx_a11y::scope, jsx_a11y::tab_index_no_positive, jsx_a11y::no_distracting_elements, + oxc::approx_constant, oxc::const_comparisons, oxc::double_comparisons, oxc::no_accumulating_spread, diff --git a/crates/oxc_linter/src/rules/oxc/approx_constant.rs b/crates/oxc_linter/src/rules/oxc/approx_constant.rs new file mode 100644 index 000000000..f713dce1d --- /dev/null +++ b/crates/oxc_linter/src/rules/oxc/approx_constant.rs @@ -0,0 +1,96 @@ +// Based on https://github.com/rust-lang/rust-clippy//blob/c9a43b18f11219fa70fe632b29518581fcd589c8/clippy_lints/src/approx_const.rs +// https://rust-lang.github.io/rust-clippy/master/#approx_constant +use oxc_ast::AstKind; +use oxc_diagnostics::{ + miette::{self, Diagnostic}, + thiserror::{self, Error}, +}; +use oxc_macros::declare_oxc_lint; +use oxc_span::Span; +use std::f64::consts as f64; + +use crate::{context::LintContext, rule::Rule, AstNode}; + +#[derive(Debug, Error, Diagnostic)] +#[error("oxc(approx-constant): Approximate value of `{1}` found.")] +#[diagnostic(severity(warning), help("Use `Math.{1}` instead"))] +struct ApproxConstantDiagnostic(#[label] pub Span, pub &'static str); + +#[derive(Debug, Default, Clone)] +pub struct ApproxConstant; + +declare_oxc_lint!( + /// ### What it does + /// + /// Disallows the use of approximate constants, instead preferring the use of the constants in the `Math` object. + /// + /// ### Why is this bad? + /// + /// Approximate constants are not as accurate as the constants in the `Math` object. + /// + /// ### Example + /// ```javascript + /// ``` + ApproxConstant, + suspicious +); + +impl Rule for ApproxConstant { + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + let AstKind::NumberLiteral(number_literal) = node.kind() else { + return; + }; + + let number_lit_str = number_literal.value.to_string(); + for (constant, name, min_digits) in &KNOWN_CONSTS { + if is_approx_const(*constant, &number_lit_str, *min_digits) { + ctx.diagnostic(ApproxConstantDiagnostic(number_literal.span, name)); + } + } + } +} + +const KNOWN_CONSTS: [(f64, &str, usize); 8] = [ + (f64::E, "E", 4), + (f64::LN_10, "LN10", 4), + (f64::LN_2, "LN2", 4), + (f64::LOG2_E, "LOG2E", 4), + (f64::LOG10_E, "LOG10E", 4), + (f64::PI, "PI", 4), + (f64::FRAC_1_SQRT_2, "SQRT1_2", 4), + (f64::SQRT_2, "SQRT2", 4), +]; + +#[must_use] +fn is_approx_const(constant: f64, value: &str, min_digits: usize) -> bool { + if value.len() <= min_digits { + false + } else if constant.to_string().starts_with(value) { + // The value is a truncated constant + true + } else { + let round_const = format!("{constant:.*}", value.len() - 2); + value == round_const + } +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec!["const x = 1234;"]; + + let fail = vec![ + "const getArea = (radius) => 3.141 * radius * radius;", + "let e = 2.718281", // E + "let ln10 = 2.302585", // LN10 + "let ln2 = 0.693147", // LN2 + "let log10e = 0.434294", // LOG10E + "let log2e = 1.442695", // LOG2E + "let pi = 3.141592", // PI + "let sqrt12 = 0.707106", // SQRT1_2 + "let sqrt2 = 1.414213", // SQRT2 + ]; + + Tester::new_without_config(ApproxConstant::NAME, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/approx_constant.snap b/crates/oxc_linter/src/snapshots/approx_constant.snap new file mode 100644 index 000000000..dbe522ca4 --- /dev/null +++ b/crates/oxc_linter/src/snapshots/approx_constant.snap @@ -0,0 +1,68 @@ +--- +source: crates/oxc_linter/src/tester.rs +expression: approx_constant +--- + ⚠ oxc(approx-constant): Approximate value of `PI` found. + ╭─[approx_constant.tsx:1:1] + 1 │ const getArea = (radius) => 3.141 * radius * radius; + · ───── + ╰──── + help: Use `Math.PI` instead + + ⚠ oxc(approx-constant): Approximate value of `E` found. + ╭─[approx_constant.tsx:1:1] + 1 │ let e = 2.718281 + · ──────── + ╰──── + help: Use `Math.E` instead + + ⚠ oxc(approx-constant): Approximate value of `LN10` found. + ╭─[approx_constant.tsx:1:1] + 1 │ let ln10 = 2.302585 + · ──────── + ╰──── + help: Use `Math.LN10` instead + + ⚠ oxc(approx-constant): Approximate value of `LN2` found. + ╭─[approx_constant.tsx:1:1] + 1 │ let ln2 = 0.693147 + · ──────── + ╰──── + help: Use `Math.LN2` instead + + ⚠ oxc(approx-constant): Approximate value of `LOG10E` found. + ╭─[approx_constant.tsx:1:1] + 1 │ let log10e = 0.434294 + · ──────── + ╰──── + help: Use `Math.LOG10E` instead + + ⚠ oxc(approx-constant): Approximate value of `LOG2E` found. + ╭─[approx_constant.tsx:1:1] + 1 │ let log2e = 1.442695 + · ──────── + ╰──── + help: Use `Math.LOG2E` instead + + ⚠ oxc(approx-constant): Approximate value of `PI` found. + ╭─[approx_constant.tsx:1:1] + 1 │ let pi = 3.141592 + · ──────── + ╰──── + help: Use `Math.PI` instead + + ⚠ oxc(approx-constant): Approximate value of `SQRT1_2` found. + ╭─[approx_constant.tsx:1:1] + 1 │ let sqrt12 = 0.707106 + · ──────── + ╰──── + help: Use `Math.SQRT1_2` instead + + ⚠ oxc(approx-constant): Approximate value of `SQRT2` found. + ╭─[approx_constant.tsx:1:1] + 1 │ let sqrt2 = 1.414213 + · ──────── + ╰──── + help: Use `Math.SQRT2` instead + +