Skip to content

Commit a21f9e4

Browse files
feat(linter): implement unicorn/prefer-bigint-literals rule (#15923)
This PR adds unicorn/prefer-bigint-literals rule, issue #684 [rule doc](https://github.com/sindresorhus/eslint-plugin-unicorn/blob/v62.0.0/docs/rules/prefer-bigint-literals.md) [rule source](https://raw.githubusercontent.com/sindresorhus/eslint-plugin-unicorn/main/test/prefer-bigint-literals.js) --------- Co-authored-by: Cameron Clark <[email protected]>
1 parent 622cb5e commit a21f9e4

File tree

4 files changed

+399
-0
lines changed

4 files changed

+399
-0
lines changed

crates/oxc_linter/src/generated/rule_runner_impls.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3472,6 +3472,12 @@ impl RuleRunner for crate::rules::unicorn::prefer_at::PreferAt {
34723472
const RUN_FUNCTIONS: RuleRunFunctionsImplemented = RuleRunFunctionsImplemented::Run;
34733473
}
34743474

3475+
impl RuleRunner for crate::rules::unicorn::prefer_bigint_literals::PreferBigintLiterals {
3476+
const NODE_TYPES: Option<&AstTypesBitset> =
3477+
Some(&AstTypesBitset::from_types(&[AstType::CallExpression]));
3478+
const RUN_FUNCTIONS: RuleRunFunctionsImplemented = RuleRunFunctionsImplemented::Run;
3479+
}
3480+
34753481
impl RuleRunner for crate::rules::unicorn::prefer_blob_reading_methods::PreferBlobReadingMethods {
34763482
const NODE_TYPES: Option<&AstTypesBitset> =
34773483
Some(&AstTypesBitset::from_types(&[AstType::CallExpression]));

crates/oxc_linter/src/rules.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,7 @@ pub(crate) mod unicorn {
464464
pub mod prefer_array_index_of;
465465
pub mod prefer_array_some;
466466
pub mod prefer_at;
467+
pub mod prefer_bigint_literals;
467468
pub mod prefer_blob_reading_methods;
468469
pub mod prefer_class_fields;
469470
pub mod prefer_classlist_toggle;
@@ -1215,6 +1216,7 @@ oxc_macros::declare_all_lint_rules! {
12151216
unicorn::numeric_separators_style,
12161217
unicorn::prefer_classlist_toggle,
12171218
unicorn::prefer_class_fields,
1219+
unicorn::prefer_bigint_literals,
12181220
unicorn::prefer_response_static_json,
12191221
unicorn::prefer_top_level_await,
12201222
unicorn::prefer_at,
Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
use oxc_ast::{AstKind, ast::Expression};
2+
use oxc_diagnostics::OxcDiagnostic;
3+
use oxc_macros::declare_oxc_lint;
4+
use oxc_span::{GetSpan, Span};
5+
use oxc_syntax::number::NumberBase;
6+
7+
use crate::{AstNode, context::LintContext, rule::Rule};
8+
9+
fn prefer_bigint_literals_diagnostic(span: Span) -> OxcDiagnostic {
10+
OxcDiagnostic::warn("Prefer bigint literals over `BigInt(...)`.")
11+
.with_help("Use a bigint literal (e.g. `123n`) instead of calling `BigInt` with a literal argument.")
12+
.with_label(span)
13+
}
14+
15+
#[derive(Debug, Default, Clone)]
16+
pub struct PreferBigintLiterals;
17+
18+
declare_oxc_lint!(
19+
/// ### What it does
20+
///
21+
/// Requires using BigInt literals (e.g. `123n`) instead of calling the `BigInt()` constructor
22+
/// with literal arguments such as numbers or numeric strings
23+
///
24+
/// ### Why is this bad?
25+
///
26+
/// Using `BigInt(…)` with literal values is unnecessarily verbose and less idiomatic than using
27+
/// a BigInt literal.
28+
///
29+
/// ### Examples
30+
///
31+
/// Examples of **incorrect** code for this rule:
32+
/// ```js
33+
/// BigInt(0);
34+
/// BigInt(123);
35+
/// BigInt(0xFF);
36+
/// BigInt(1e3);
37+
/// BigInt("42");
38+
/// BigInt("0x10");
39+
/// ```
40+
///
41+
/// Examples of **correct** code for this rule:
42+
/// ```js
43+
/// 0n;
44+
/// 123n;
45+
/// 0xFFn;
46+
/// 1000n;
47+
/// // Non-integer, dynamic, or non-literal input:
48+
/// BigInt(x);
49+
/// BigInt("not-a-number");
50+
/// BigInt("1.23");
51+
/// ```
52+
PreferBigintLiterals,
53+
unicorn,
54+
style,
55+
fix
56+
);
57+
58+
impl Rule for PreferBigintLiterals {
59+
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
60+
let AstKind::CallExpression(call) = node.kind() else { return };
61+
let Some(reference) = call.callee.get_identifier_reference() else {
62+
return;
63+
};
64+
65+
if reference.name != "BigInt" || call.optional || call.arguments.len() != 1 {
66+
return;
67+
}
68+
69+
let arg = &call.arguments[0];
70+
71+
let Some(argument_expression) = arg.as_expression() else {
72+
return;
73+
};
74+
75+
if argument_expression.is_big_int_literal() {
76+
return;
77+
}
78+
79+
match argument_expression.get_inner_expression() {
80+
Expression::StringLiteral(string_literal) => {
81+
if let Some(replacement) = bigint_literal_from_string(&string_literal.value) {
82+
ctx.diagnostic_with_fix(
83+
prefer_bigint_literals_diagnostic(arg.span()),
84+
|fixer| fixer.replace(call.span, replacement),
85+
);
86+
}
87+
}
88+
Expression::NumericLiteral(numeric_literal) => {
89+
if numeric_literal.value.fract() != 0.0 {
90+
return;
91+
}
92+
93+
let raw_text = numeric_literal.raw.as_ref().map_or_else(
94+
|| {
95+
debug_assert!(false, "ASTs from the linter should always have raw values");
96+
ctx.source_range(numeric_literal.span)
97+
},
98+
|raw| raw.as_str(),
99+
);
100+
101+
if let Some(replacement) =
102+
bigint_literal_from_numeric(raw_text, numeric_literal.base)
103+
{
104+
ctx.diagnostic_with_fix(
105+
prefer_bigint_literals_diagnostic(arg.span()),
106+
|fixer| fixer.replace(call.span, replacement),
107+
);
108+
} else {
109+
ctx.diagnostic(prefer_bigint_literals_diagnostic(arg.span()));
110+
}
111+
}
112+
113+
_ => {}
114+
}
115+
}
116+
}
117+
118+
fn matches_js_integer_literal(s: &str) -> Option<NumberBase> {
119+
let s = s.trim();
120+
let mut chars = s.chars();
121+
122+
match chars.next() {
123+
Some('0') => match chars.next() {
124+
Some('b' | 'B') => {
125+
chars.all(|char| matches!(char, '0' | '1')).then_some(NumberBase::Binary)
126+
}
127+
128+
Some('o' | 'O') => {
129+
chars.all(|char| matches!(char, '0'..='7')).then_some(NumberBase::Octal)
130+
}
131+
132+
Some('x' | 'X') => {
133+
chars.all(|char| char.is_ascii_hexdigit()).then_some(NumberBase::Hex)
134+
}
135+
Some('0'..='9') => {
136+
chars.all(|char| char.is_ascii_digit()).then_some(NumberBase::Decimal)
137+
}
138+
None => Some(NumberBase::Decimal),
139+
_ => None,
140+
},
141+
Some('1'..='9') => chars.all(|char| char.is_ascii_digit()).then_some(NumberBase::Decimal),
142+
_ => None,
143+
}
144+
}
145+
146+
fn bigint_literal_from_string(raw: &str) -> Option<String> {
147+
let trimmed = raw.trim();
148+
149+
let base = matches_js_integer_literal(trimmed)?;
150+
151+
match base {
152+
NumberBase::Binary | NumberBase::Octal | NumberBase::Hex => Some(format!("{trimmed}n")),
153+
NumberBase::Decimal => Some(format!("{}n", trim_leading_zeros(trimmed))),
154+
NumberBase::Float => {
155+
unreachable!();
156+
}
157+
}
158+
}
159+
160+
fn trim_leading_zeros(raw: &str) -> &str {
161+
let trimmed = raw.trim_start_matches('0');
162+
if trimmed.is_empty() { "0" } else { trimmed }
163+
}
164+
165+
fn bigint_literal_from_numeric(raw: &str, base: NumberBase) -> Option<String> {
166+
let literal = match base {
167+
NumberBase::Binary | NumberBase::Hex => format!("{raw}n"),
168+
NumberBase::Octal => {
169+
if raw.starts_with("0o") || raw.starts_with("0O") {
170+
format!("{raw}n")
171+
} else {
172+
// Legacy octal like `0777` is invalid as a BigInt `0777n`, so normalize to `0o`.
173+
format!("0o{}n", trim_leading_zeros(raw))
174+
}
175+
}
176+
NumberBase::Decimal => format!("{}n", trim_leading_zeros(raw)),
177+
NumberBase::Float => return None,
178+
};
179+
Some(literal)
180+
}
181+
182+
#[test]
183+
fn test() {
184+
use crate::tester::Tester;
185+
let pass = vec![
186+
r"1n",
187+
r"BigInt()",
188+
r"BigInt(1, 1)",
189+
r"BigInt(...[1])",
190+
r"BigInt(true)",
191+
r"BigInt(null)",
192+
r"new BigInt(1)",
193+
r"Not_BigInt(1)",
194+
r#"BigInt("1.0")"#,
195+
r#"BigInt("1.1")"#,
196+
r#"BigInt("1e3")"#,
197+
r"BigInt(`1`)",
198+
r#"BigInt("1" + "2")"#,
199+
r"BigInt?.(1)",
200+
r"BigInt(1.1)",
201+
r"typeof BigInt",
202+
r"BigInt(1n)",
203+
r#"BigInt("not-number")"#,
204+
r#"BigInt("1_2")"#,
205+
r#"BigInt("1\\\n2")"#,
206+
r#"String.raw`BigInt("\u{31}")`"#,
207+
];
208+
let fail: Vec<&str> = vec![
209+
r#"BigInt("0")"#,
210+
r#"BigInt(" 0 ")"#,
211+
r#"BigInt("9007199254740993")"#,
212+
r#"BigInt("0B11")"#,
213+
r#"BigInt("0O777")"#,
214+
r#"BigInt("0XFe")"#,
215+
r"BigInt(0)",
216+
r"BigInt(0B11_11)",
217+
r"BigInt(0O777_777)",
218+
r"BigInt(0XFe_fE)",
219+
r"BigInt(0777)",
220+
r"BigInt(0888)",
221+
r"BigInt(1.0)",
222+
r"BigInt(1e2)",
223+
r"BigInt(/* comment */1)",
224+
r"BigInt(9007199254740993)",
225+
r"BigInt(0x20000000000001)",
226+
r"BigInt(9_007_199_254_740_993)",
227+
r"BigInt(0x20_00_00_00_00_00_01)",
228+
];
229+
let fix = vec![
230+
(r"BigInt('42')", "42n"),
231+
(r"BigInt(' 0xFF ')", "0xFFn"),
232+
(r"BigInt(0)", "0n"),
233+
(r"BigInt(0B11_11)", "0B11_11n"),
234+
(r"BigInt(0O777_777)", "0O777_777n"),
235+
(r"BigInt(0777)", "0o777n"),
236+
(r"BigInt(0888)", "888n"),
237+
(r#"BigInt("0777")"#, "777n"),
238+
(r#"BigInt("0888")"#, "888n"),
239+
(r#"BigInt("0b1010")"#, "0b1010n"),
240+
(r#"BigInt("0B0011")"#, "0B0011n"),
241+
(r#"BigInt("0O123")"#, "0O123n"),
242+
(r#"BigInt(" 0001 ")"#, "1n"),
243+
(
244+
r"BigInt('9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999')",
245+
"9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999n",
246+
),
247+
(
248+
r"BigInt(9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999)",
249+
"9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999n",
250+
),
251+
];
252+
253+
Tester::new(PreferBigintLiterals::NAME, PreferBigintLiterals::PLUGIN, pass, fail)
254+
.expect_fix(fix)
255+
.test_and_snapshot();
256+
}

0 commit comments

Comments
 (0)