diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 5f9f90367..1f1f4dd18 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -171,6 +171,7 @@ mod unicorn { pub mod no_useless_fallback_in_spread; pub mod no_useless_promise_resolve_reject; pub mod no_useless_switch_case; + pub mod no_zero_fractions; pub mod number_literal_case; pub mod prefer_add_event_listener; pub mod prefer_array_flat_map; @@ -333,6 +334,7 @@ oxc_macros::declare_all_lint_rules! { unicorn::no_useless_fallback_in_spread, unicorn::no_useless_promise_resolve_reject, unicorn::no_useless_switch_case, + unicorn::no_zero_fractions, unicorn::number_literal_case, unicorn::prefer_add_event_listener, unicorn::prefer_array_flat_map, diff --git a/crates/oxc_linter/src/rules/unicorn/no_zero_fractions.rs b/crates/oxc_linter/src/rules/unicorn/no_zero_fractions.rs new file mode 100644 index 000000000..f45cb84f7 --- /dev/null +++ b/crates/oxc_linter/src/rules/unicorn/no_zero_fractions.rs @@ -0,0 +1,137 @@ +use oxc_ast::AstKind; +use oxc_diagnostics::{ + miette::{self, Diagnostic}, + thiserror::Error, +}; +use oxc_macros::declare_oxc_lint; +use oxc_span::Span; + +use crate::{context::LintContext, rule::Rule, AstNode, Fix}; + +#[derive(Debug, Error, Diagnostic)] +enum NoZeroFractionsDiagnostic { + #[error("eslint-plugin-unicorn(no-zero-fractions): Don't use a zero fraction in the number.")] + #[diagnostic(severity(warning), help("Replace the number literal with `{1}`"))] + ZeroFraction(#[label] Span, String), + #[error("eslint-plugin-unicorn(no-zero-fractions): Don't use a dangling dot in the number.")] + #[diagnostic(severity(warning), help("Replace the number literal with `{1}`"))] + DanglingDot(#[label] Span, String), +} + +#[derive(Debug, Default, Clone)] +pub struct NoZeroFractions; + +declare_oxc_lint!( + /// ### What it does + /// + /// Prevents the use of zero fractions. + /// + /// ### Why is this bad? + /// + /// There is no difference in JavaScript between, for example, `1`, `1.0` and `1.`, so prefer the former for consistency and brevity. + /// + /// ### Example + /// ```javascript + /// // Bad + /// const foo = 1.0; + /// const foo = -1.0; + /// const foo = 123_456.000_000; + /// + /// // Good + /// const foo = 1; + /// const foo = -1; + /// const foo = 123456; + /// const foo = 1.1; + /// ``` + NoZeroFractions, + style +); + +impl Rule for NoZeroFractions { + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + let AstKind::NumberLiteral(number_literal) = node.kind() else { + return; + }; + + let Some((fmt, is_dangling_dot)) = format_raw(number_literal.raw) else { return }; + if fmt == number_literal.raw { + return; + }; + + ctx.diagnostic_with_fix( + if is_dangling_dot { + NoZeroFractionsDiagnostic::DanglingDot(number_literal.span, fmt.clone()) + } else { + NoZeroFractionsDiagnostic::ZeroFraction(number_literal.span, fmt.clone()) + }, + || Fix::new(fmt, number_literal.span), + ); + } +} + +fn format_raw(raw: &str) -> Option<(String, bool)> { + let (before, after_and_dot) = raw.split_once('.')?; + let mut after_parts = after_and_dot.splitn(2, |c: char| !c.is_ascii_digit() && c != '_'); + let dot_and_fractions = after_parts.next()?; + let after = after_parts.next().unwrap_or(""); + + let fixed_dot_and_fractions = + dot_and_fractions.trim_end_matches(|c: char| c == '0' || c == '.' || c == '_'); + let formatted = format!( + "{}{}{}{}", + if before.is_empty() && fixed_dot_and_fractions.is_empty() { "0" } else { before }, + if fixed_dot_and_fractions.is_empty() { "" } else { "." }, + fixed_dot_and_fractions, + after + ); + + Some((formatted, dot_and_fractions.is_empty())) +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + r#"const foo = "123.1000""#, + r#"foo("123.1000")"#, + r"const foo = 1", + r"const foo = 1 + 2", + r"const foo = -1", + r"const foo = 123123123", + r"const foo = 1.1", + r"const foo = -1.1", + r"const foo = 123123123.4", + r"const foo = 1e3", + r"1 .toString()", + ]; + + let fail = vec![ + r"const foo = 1.0", + r"const foo = 1.0 + 1", + r"foo(1.0 + 1)", + r"const foo = 1.00", + r"const foo = 1.00000", + r"const foo = -1.0", + r"const foo = 123123123.0", + r"const foo = 123.11100000000", + r"const foo = 1.", + r"const foo = +1.", + r"const foo = -1.", + r"const foo = 1.e10", + r"const foo = +1.e-10", + r"const foo = -1.e+10", + r"const foo = (1.).toString()", + r"1.00.toFixed(2)", + r"1.00 .toFixed(2)", + r"(1.00).toFixed(2)", + r"1.00?.toFixed(2)", + r"a = .0;", + r"a = .0.toString()", + r"function foo(){return.0}", + r"function foo(){return.0.toString()}", + r"function foo(){return.0+.1}", + ]; + + Tester::new_without_config(NoZeroFractions::NAME, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/no_zero_fractions.snap b/crates/oxc_linter/src/snapshots/no_zero_fractions.snap new file mode 100644 index 000000000..b57e35db7 --- /dev/null +++ b/crates/oxc_linter/src/snapshots/no_zero_fractions.snap @@ -0,0 +1,173 @@ +--- +source: crates/oxc_linter/src/tester.rs +expression: no_zero_fractions +--- + ⚠ eslint-plugin-unicorn(no-zero-fractions): Don't use a zero fraction in the number. + ╭─[no_zero_fractions.tsx:1:1] + 1 │ const foo = 1.0 + · ─── + ╰──── + help: Replace the number literal with `1` + + ⚠ eslint-plugin-unicorn(no-zero-fractions): Don't use a zero fraction in the number. + ╭─[no_zero_fractions.tsx:1:1] + 1 │ const foo = 1.0 + 1 + · ─── + ╰──── + help: Replace the number literal with `1` + + ⚠ eslint-plugin-unicorn(no-zero-fractions): Don't use a zero fraction in the number. + ╭─[no_zero_fractions.tsx:1:1] + 1 │ foo(1.0 + 1) + · ─── + ╰──── + help: Replace the number literal with `1` + + ⚠ eslint-plugin-unicorn(no-zero-fractions): Don't use a zero fraction in the number. + ╭─[no_zero_fractions.tsx:1:1] + 1 │ const foo = 1.00 + · ──── + ╰──── + help: Replace the number literal with `1` + + ⚠ eslint-plugin-unicorn(no-zero-fractions): Don't use a zero fraction in the number. + ╭─[no_zero_fractions.tsx:1:1] + 1 │ const foo = 1.00000 + · ─────── + ╰──── + help: Replace the number literal with `1` + + ⚠ eslint-plugin-unicorn(no-zero-fractions): Don't use a zero fraction in the number. + ╭─[no_zero_fractions.tsx:1:1] + 1 │ const foo = -1.0 + · ─── + ╰──── + help: Replace the number literal with `1` + + ⚠ eslint-plugin-unicorn(no-zero-fractions): Don't use a zero fraction in the number. + ╭─[no_zero_fractions.tsx:1:1] + 1 │ const foo = 123123123.0 + · ─────────── + ╰──── + help: Replace the number literal with `123123123` + + ⚠ eslint-plugin-unicorn(no-zero-fractions): Don't use a zero fraction in the number. + ╭─[no_zero_fractions.tsx:1:1] + 1 │ const foo = 123.11100000000 + · ─────────────── + ╰──── + help: Replace the number literal with `123.111` + + ⚠ eslint-plugin-unicorn(no-zero-fractions): Don't use a dangling dot in the number. + ╭─[no_zero_fractions.tsx:1:1] + 1 │ const foo = 1. + · ── + ╰──── + help: Replace the number literal with `1` + + ⚠ eslint-plugin-unicorn(no-zero-fractions): Don't use a dangling dot in the number. + ╭─[no_zero_fractions.tsx:1:1] + 1 │ const foo = +1. + · ── + ╰──── + help: Replace the number literal with `1` + + ⚠ eslint-plugin-unicorn(no-zero-fractions): Don't use a dangling dot in the number. + ╭─[no_zero_fractions.tsx:1:1] + 1 │ const foo = -1. + · ── + ╰──── + help: Replace the number literal with `1` + + ⚠ eslint-plugin-unicorn(no-zero-fractions): Don't use a dangling dot in the number. + ╭─[no_zero_fractions.tsx:1:1] + 1 │ const foo = 1.e10 + · ───── + ╰──── + help: Replace the number literal with `110` + + ⚠ eslint-plugin-unicorn(no-zero-fractions): Don't use a dangling dot in the number. + ╭─[no_zero_fractions.tsx:1:1] + 1 │ const foo = +1.e-10 + · ────── + ╰──── + help: Replace the number literal with `1-10` + + ⚠ eslint-plugin-unicorn(no-zero-fractions): Don't use a dangling dot in the number. + ╭─[no_zero_fractions.tsx:1:1] + 1 │ const foo = -1.e+10 + · ────── + ╰──── + help: Replace the number literal with `1+10` + + ⚠ eslint-plugin-unicorn(no-zero-fractions): Don't use a dangling dot in the number. + ╭─[no_zero_fractions.tsx:1:1] + 1 │ const foo = (1.).toString() + · ── + ╰──── + help: Replace the number literal with `1` + + ⚠ eslint-plugin-unicorn(no-zero-fractions): Don't use a zero fraction in the number. + ╭─[no_zero_fractions.tsx:1:1] + 1 │ 1.00.toFixed(2) + · ──── + ╰──── + help: Replace the number literal with `1` + + ⚠ eslint-plugin-unicorn(no-zero-fractions): Don't use a zero fraction in the number. + ╭─[no_zero_fractions.tsx:1:1] + 1 │ 1.00 .toFixed(2) + · ──── + ╰──── + help: Replace the number literal with `1` + + ⚠ eslint-plugin-unicorn(no-zero-fractions): Don't use a zero fraction in the number. + ╭─[no_zero_fractions.tsx:1:1] + 1 │ (1.00).toFixed(2) + · ──── + ╰──── + help: Replace the number literal with `1` + + ⚠ eslint-plugin-unicorn(no-zero-fractions): Don't use a zero fraction in the number. + ╭─[no_zero_fractions.tsx:1:1] + 1 │ 1.00?.toFixed(2) + · ──── + ╰──── + help: Replace the number literal with `1` + + ⚠ eslint-plugin-unicorn(no-zero-fractions): Don't use a zero fraction in the number. + ╭─[no_zero_fractions.tsx:1:1] + 1 │ a = .0; + · ── + ╰──── + help: Replace the number literal with `0` + + ⚠ eslint-plugin-unicorn(no-zero-fractions): Don't use a zero fraction in the number. + ╭─[no_zero_fractions.tsx:1:1] + 1 │ a = .0.toString() + · ── + ╰──── + help: Replace the number literal with `0` + + ⚠ eslint-plugin-unicorn(no-zero-fractions): Don't use a zero fraction in the number. + ╭─[no_zero_fractions.tsx:1:1] + 1 │ function foo(){return.0} + · ── + ╰──── + help: Replace the number literal with `0` + + ⚠ eslint-plugin-unicorn(no-zero-fractions): Don't use a zero fraction in the number. + ╭─[no_zero_fractions.tsx:1:1] + 1 │ function foo(){return.0.toString()} + · ── + ╰──── + help: Replace the number literal with `0` + + ⚠ eslint-plugin-unicorn(no-zero-fractions): Don't use a zero fraction in the number. + ╭─[no_zero_fractions.tsx:1:1] + 1 │ function foo(){return.0+.1} + · ── + ╰──── + help: Replace the number literal with `0` + +