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/number-literal-case #1271

Merged
merged 4 commits into from
Nov 15, 2023
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 @@ -166,6 +166,7 @@ mod unicorn {
pub mod no_unnecessary_await;
pub mod no_useless_fallback_in_spread;
pub mod no_useless_promise_resolve_reject;
pub mod number_literal_case;
pub mod prefer_add_event_listener;
pub mod prefer_array_flat_map;
pub mod prefer_blob_reading_methods;
Expand Down Expand Up @@ -320,6 +321,7 @@ oxc_macros::declare_all_lint_rules! {
unicorn::no_unnecessary_await,
unicorn::no_useless_fallback_in_spread,
unicorn::no_useless_promise_resolve_reject,
unicorn::number_literal_case,
unicorn::prefer_add_event_listener,
unicorn::prefer_array_flat_map,
unicorn::prefer_blob_reading_methods,
Expand Down
252 changes: 252 additions & 0 deletions crates/oxc_linter/src/rules/unicorn/number_literal_case.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
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 NumberLiteralCaseDiagnostic {
#[error("eslint-plugin-unicorn(number-literal-case): Unexpected number literal prefix in uppercase.")]
#[diagnostic(severity(warning), help("Use lowercase for the number literal prefix `{1}`."))]
UppercasePrefix(#[label] Span, &'static str),
#[error(
"eslint-plugin-unicorn(number-literal-case): Unexpected exponential notation in uppercase."
)]
#[diagnostic(severity(warning), help("Use lowercase for `e` in exponential notations."))]
UppercaseExponentialNotation(#[label] Span),
#[error(
"eslint-plugin-unicorn(number-literal-case): Unexpected hexadecimal digits in lowercase."
)]
#[diagnostic(severity(warning), help("Use uppercase for hexadecimal digits."))]
LowercaseHexadecimalDigits(#[label] Span),
#[error(
"eslint-plugin-unicorn(number-literal-case): Unexpected number literal prefix in uppercase and hexadecimal digits in lowercase."
)]
#[diagnostic(severity(warning), help("Use lowercase for the number literal prefix `{1}` and uppercase for hexadecimal digits."))]
UppercasePrefixAndLowercaseHexadecimalDigits(#[label] Span, &'static str),
}

#[derive(Debug, Default, Clone)]
pub struct NumberLiteralCase;

declare_oxc_lint!(
/// ### What it does
/// This rule enforces proper case for numeric literals.
///
/// ### Why is this bad?
/// When both an identifier and a number literal are in lower case, it can be hard to differentiate between them.
///
/// ### Example
/// ```javascript
/// // Fail
/// const foo = 0XFF;
/// const foo = 0xff;
/// const foo = 0Xff;
/// const foo = 0Xffn;
///
/// const foo = 0B10;
/// const foo = 0B10n;
///
/// const foo = 0O76;
/// const foo = 0O76n;
///
/// const foo = 2E-5;
///
/// // Pass
/// const foo = 0xFF;
/// const foo = 0b10;
/// const foo = 0o76;
/// const foo = 0xFFn;
/// const foo = 2e+5;
/// ```
NumberLiteralCase,
style
);

impl Rule for NumberLiteralCase {
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
let (raw_literal, raw_span) = match node.kind() {
AstKind::NumberLiteral(number) => (number.raw, number.span),
AstKind::BigintLiteral(number) => {
let span = number.span;
(span.source_text(ctx.source_text()), span)
}
_ => return,
};

if let Some((diagnostic, fixed_literal)) = check_number_literal(raw_literal, raw_span) {
ctx.diagnostic_with_fix(diagnostic, || Fix::new(fixed_literal, raw_span));
}
}
}

#[allow(clippy::cast_possible_truncation)]
fn check_number_literal(
number_literal: &str,
raw_span: Span,
) -> Option<(NumberLiteralCaseDiagnostic, String)> {
if number_literal.starts_with("0B") || number_literal.starts_with("0O") {
return Some((
NumberLiteralCaseDiagnostic::UppercasePrefix(
Span { start: raw_span.start + 1, end: raw_span.start + 2 },
if number_literal.starts_with("0B") { "0b" } else { "0o" },
),
number_literal.to_lowercase(),
));
}
if number_literal.starts_with("0X") || number_literal.starts_with("0x") {
let has_uppercase_prefix = number_literal.starts_with("0X");
let has_lowercase_digits = number_literal[2..].chars().any(|c| ('a'..='f').contains(&c));
if has_uppercase_prefix && has_lowercase_digits {
return Some((
NumberLiteralCaseDiagnostic::UppercasePrefixAndLowercaseHexadecimalDigits(
raw_span, "0x",
),
"0x".to_owned() + &digits_to_uppercase(&number_literal[2..]),
));
}
if has_uppercase_prefix {
return Some((
NumberLiteralCaseDiagnostic::UppercasePrefix(
Span { start: raw_span.start + 1, end: raw_span.start + 2 },
"0x",
),
"0x".to_owned() + &number_literal[2..],
));
}
if has_lowercase_digits {
return Some((
NumberLiteralCaseDiagnostic::LowercaseHexadecimalDigits(Span {
start: raw_span.start + 2,
end: raw_span.end,
}),
"0x".to_owned() + &digits_to_uppercase(&number_literal[2..]),
));
}
return None;
}
if let Some(index) = number_literal.find('E') {
let char_position = raw_span.start + index as u32;
return Some((
NumberLiteralCaseDiagnostic::UppercaseExponentialNotation(Span {
start: char_position,
end: char_position + 1,
}),
number_literal.to_lowercase(),
));
}
None
}

fn digits_to_uppercase(digits: &str) -> String {
let mut result = digits.to_uppercase();
if result.ends_with('N') {
result.truncate(result.len() - 1);
result.push('n');
}
result
}

#[test]
fn test() {
use crate::tester::Tester;

let pass = vec![
"var foo = 0777",
"var foo = 0888",
"const foo = 1234",
"const foo = 0b10",
"const foo = 0o1234567",
"const foo = 0xABCDEF",
"const foo = 1234n",
"const foo = 0b10n",
"const foo = 0o1234567n",
"const foo = 0xABCDEFn",
"const foo = NaN",
"const foo = +Infinity",
"const foo = -Infinity",
"const foo = 1.2e3",
"const foo = 1.2e-3",
"const foo = 1.2e+3",
"const foo = '0Xff'",
"const foo = '0Xffn'",
"const foo = 123_456",
"const foo = 0b10_10",
"const foo = 0o1_234_567",
"const foo = 0xDEED_BEEF",
"const foo = 123_456n",
"const foo = 0b10_10n",
"const foo = 0o1_234_567n",
"const foo = 0xDEED_BEEFn",
];

let fail = vec![
"const foo = 0B10",
"const foo = 0O1234567",
"const foo = 0XaBcDeF",
"const foo = 0B10n",
"const foo = 0O1234567n",
"const foo = 0XaBcDeFn",
"const foo = 0B0n",
"const foo = 0O0n",
"const foo = 0X0n",
"const foo = 1.2E3",
"const foo = 1.2E-3",
"const foo = 1.2E+3",
"
const foo = 255;

if (foo === 0xff) {
console.log('invalid');
}
",
"const foo = 0XdeEd_Beefn",
"console.log(BigInt(0B10 + 1.2E+3) + 0XdeEd_Beefn)",
];

let fix = vec![
("const foo = 0B10", "const foo = 0b10", None),
("const foo = 0O1234567", "const foo = 0o1234567", None),
("const foo = 0XaBcDeF", "const foo = 0xABCDEF", None),
("const foo = 0B10n", "const foo = 0b10n", None),
("const foo = 0O1234567n", "const foo = 0o1234567n", None),
("const foo = 0XaBcDeFn", "const foo = 0xABCDEFn", None),
("const foo = 0B0n", "const foo = 0b0n", None),
("const foo = 0O0n", "const foo = 0o0n", None),
("const foo = 0X0n", "const foo = 0x0n", None),
("const foo = 1.2E3", "const foo = 1.2e3", None),
("const foo = 1.2E-3", "const foo = 1.2e-3", None),
("const foo = 1.2E+3", "const foo = 1.2e+3", None),
(
"
const foo = 255;

if (foo === 0xff) {
console.log('invalid');
}
",
"
const foo = 255;

if (foo === 0xFF) {
console.log('invalid');
}
",
None,
),
("const foo = 0XdeEd_Beefn", "const foo = 0xDEED_BEEFn", None),
(
"console.log(BigInt(0B10 + 1.2E+3) + 0XdeEd_Beefn)",
"console.log(BigInt(0b10 + 1.2e+3) + 0xDEED_BEEFn)",
None,
),
];

Tester::new_without_config(NumberLiteralCase::NAME, pass, fail)
.expect_fix(fix)
.test_and_snapshot();
}
127 changes: 127 additions & 0 deletions crates/oxc_linter/src/snapshots/number_literal_case.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
---
source: crates/oxc_linter/src/tester.rs
assertion_line: 119
expression: number_literal_case
---
⚠ eslint-plugin-unicorn(number-literal-case): Unexpected number literal prefix in uppercase.
╭─[number_literal_case.tsx:1:1]
1 │ const foo = 0B10
· ─
╰────
help: Use lowercase for the number literal prefix `0b`.

⚠ eslint-plugin-unicorn(number-literal-case): Unexpected number literal prefix in uppercase.
╭─[number_literal_case.tsx:1:1]
1 │ const foo = 0O1234567
· ─
╰────
help: Use lowercase for the number literal prefix `0o`.

⚠ eslint-plugin-unicorn(number-literal-case): Unexpected number literal prefix in uppercase and hexadecimal digits in lowercase.
╭─[number_literal_case.tsx:1:1]
1 │ const foo = 0XaBcDeF
· ────────
╰────
help: Use lowercase for the number literal prefix `0x` and uppercase for hexadecimal digits.

⚠ eslint-plugin-unicorn(number-literal-case): Unexpected number literal prefix in uppercase.
╭─[number_literal_case.tsx:1:1]
1 │ const foo = 0B10n
· ─
╰────
help: Use lowercase for the number literal prefix `0b`.

⚠ eslint-plugin-unicorn(number-literal-case): Unexpected number literal prefix in uppercase.
╭─[number_literal_case.tsx:1:1]
1 │ const foo = 0O1234567n
· ─
╰────
help: Use lowercase for the number literal prefix `0o`.

⚠ eslint-plugin-unicorn(number-literal-case): Unexpected number literal prefix in uppercase and hexadecimal digits in lowercase.
╭─[number_literal_case.tsx:1:1]
1 │ const foo = 0XaBcDeFn
· ─────────
╰────
help: Use lowercase for the number literal prefix `0x` and uppercase for hexadecimal digits.

⚠ eslint-plugin-unicorn(number-literal-case): Unexpected number literal prefix in uppercase.
╭─[number_literal_case.tsx:1:1]
1 │ const foo = 0B0n
· ─
╰────
help: Use lowercase for the number literal prefix `0b`.

⚠ eslint-plugin-unicorn(number-literal-case): Unexpected number literal prefix in uppercase.
╭─[number_literal_case.tsx:1:1]
1 │ const foo = 0O0n
· ─
╰────
help: Use lowercase for the number literal prefix `0o`.

⚠ eslint-plugin-unicorn(number-literal-case): Unexpected number literal prefix in uppercase.
╭─[number_literal_case.tsx:1:1]
1 │ const foo = 0X0n
· ─
╰────
help: Use lowercase for the number literal prefix `0x`.

⚠ eslint-plugin-unicorn(number-literal-case): Unexpected exponential notation in uppercase.
╭─[number_literal_case.tsx:1:1]
1 │ const foo = 1.2E3
· ─
╰────
help: Use lowercase for `e` in exponential notations.

⚠ eslint-plugin-unicorn(number-literal-case): Unexpected exponential notation in uppercase.
╭─[number_literal_case.tsx:1:1]
1 │ const foo = 1.2E-3
· ─
╰────
help: Use lowercase for `e` in exponential notations.

⚠ eslint-plugin-unicorn(number-literal-case): Unexpected exponential notation in uppercase.
╭─[number_literal_case.tsx:1:1]
1 │ const foo = 1.2E+3
· ─
╰────
help: Use lowercase for `e` in exponential notations.

⚠ eslint-plugin-unicorn(number-literal-case): Unexpected hexadecimal digits in lowercase.
╭─[number_literal_case.tsx:3:1]
3 │
4 │ if (foo === 0xff) {
· ──
5 │ console.log('invalid');
╰────
help: Use uppercase for hexadecimal digits.

⚠ eslint-plugin-unicorn(number-literal-case): Unexpected number literal prefix in uppercase and hexadecimal digits in lowercase.
╭─[number_literal_case.tsx:1:1]
1 │ const foo = 0XdeEd_Beefn
· ────────────
╰────
help: Use lowercase for the number literal prefix `0x` and uppercase for hexadecimal digits.

⚠ eslint-plugin-unicorn(number-literal-case): Unexpected number literal prefix in uppercase.
╭─[number_literal_case.tsx:1:1]
1 │ console.log(BigInt(0B10 + 1.2E+3) + 0XdeEd_Beefn)
· ─
╰────
help: Use lowercase for the number literal prefix `0b`.

⚠ eslint-plugin-unicorn(number-literal-case): Unexpected exponential notation in uppercase.
╭─[number_literal_case.tsx:1:1]
1 │ console.log(BigInt(0B10 + 1.2E+3) + 0XdeEd_Beefn)
· ─
╰────
help: Use lowercase for `e` in exponential notations.

⚠ eslint-plugin-unicorn(number-literal-case): Unexpected number literal prefix in uppercase and hexadecimal digits in lowercase.
╭─[number_literal_case.tsx:1:1]
1 │ console.log(BigInt(0B10 + 1.2E+3) + 0XdeEd_Beefn)
· ────────────
╰────
help: Use lowercase for the number literal prefix `0x` and uppercase for hexadecimal digits.


Loading