Skip to content

Commit

Permalink
feat(linter) eslint plugin unicorn: no zero fractions (#1464)
Browse files Browse the repository at this point in the history
  • Loading branch information
camc314 committed Nov 21, 2023
1 parent d64ed0a commit 10be84a
Show file tree
Hide file tree
Showing 3 changed files with 312 additions and 0 deletions.
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]
1const 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]
1const 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]
1foo(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]
1const 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]
1const 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]
1const 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]
1const 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]
1const 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]
1const 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]
1const 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]
1const 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]
1const 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]
1const 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]
1const 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]
1const 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]
11.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]
11.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]
11.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]
1a = .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]
1a = .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]
1function 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]
1function 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]
1function foo(){return.0+.1}
· ──
╰────
help: Replace the number literal with `0`


0 comments on commit 10be84a

Please sign in to comment.