Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(linter) eslint plugin unicorn: no zero fractions #1464

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions crates/oxc_linter/src/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
137 changes: 137 additions & 0 deletions crates/oxc_linter/src/rules/unicorn/no_zero_fractions.rs
Original file line number Diff line number Diff line change
@@ -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();
}
173 changes: 173 additions & 0 deletions crates/oxc_linter/src/snapshots/no_zero_fractions.snap
Original file line number Diff line number Diff line change
@@ -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`


Loading