-
-
Notifications
You must be signed in to change notification settings - Fork 482
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(linter/eslint): Implement prefer-numeric-literals (#4109)
Rule Detail: [link](https://eslint.org/docs/latest/rules/prefer-numeric-literals) --- This lacks the fix, which I intend to work on as a follow up or if preferred I can leave the PR open an hack on it this week. One interesting note is that `eslint(radix)` does not handle: ``` "function *f(){ yield(Number).parseInt('11', 2) }", // { "ecmaVersion": 6 }, ``` Maybe it is an idea to introduce a helper `is_parseint_stmt` which returns the `call_expr`? As the code is almost identical to the `radix` rule. --------- Co-authored-by: wenzhe <mysteryven@gmail.com>
- Loading branch information
1 parent
bb33bcc
commit eaf834f
Showing
3 changed files
with
676 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
291 changes: 291 additions & 0 deletions
291
crates/oxc_linter/src/rules/eslint/prefer_numeric_literals.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,291 @@ | ||
use oxc_ast::{ | ||
ast::{ | ||
Argument, CallExpression, Expression, IdentifierReference, MemberExpression, | ||
StaticMemberExpression, | ||
}, | ||
AstKind, | ||
}; | ||
use oxc_diagnostics::OxcDiagnostic; | ||
use oxc_macros::declare_oxc_lint; | ||
use oxc_span::Span; | ||
use phf::{phf_map, Map}; | ||
|
||
use crate::{ast_util::get_symbol_id_of_variable, context::LintContext, rule::Rule, AstNode}; | ||
|
||
fn prefer_numeric_literals_diagnostic(span0: Span, x0: &str) -> OxcDiagnostic { | ||
OxcDiagnostic::warn(format!("Use {x0} literals instead of parseInt().")).with_label(span0) | ||
} | ||
|
||
#[derive(Debug, Default, Clone)] | ||
pub struct PreferNumericLiterals; | ||
|
||
const RADIX_MAP: Map<&'static str, &'static str> = phf_map! { | ||
"2" => "binary", | ||
"8" => "octal", | ||
"16" => "hexadecimal", | ||
}; | ||
|
||
declare_oxc_lint!( | ||
/// ### What it does | ||
/// | ||
/// Disallow parseInt() and Number.parseInt() in favor of binary, octal, and hexadecimal | ||
/// literals. | ||
/// | ||
/// ### Why is this bad? | ||
/// | ||
/// The parseInt() and Number.parseInt() functions can be used to turn binary, octal, and | ||
/// hexadecimal strings into integers. As binary, octal, and hexadecimal literals are supported | ||
/// in ES6, this rule encourages use of those numeric literals instead of parseInt() or | ||
/// Number.parseInt(). | ||
/// | ||
/// ### Example | ||
/// ```javascript | ||
/// parseInt("111110111", 2) === 503; | ||
/// parseInt(`111110111`, 2) === 503; | ||
/// parseInt("767", 8) === 503; | ||
/// parseInt("1F7", 16) === 503; | ||
/// Number.parseInt("111110111", 2) === 503; | ||
/// Number.parseInt("767", 8) === 503; | ||
/// Number.parseInt("1F7", 16) === 503; | ||
/// ``` | ||
PreferNumericLiterals, | ||
style, | ||
); | ||
|
||
impl Rule for PreferNumericLiterals { | ||
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { | ||
if let AstKind::CallExpression(call_expr) = node.kind() { | ||
match &call_expr.callee.without_parenthesized() { | ||
Expression::Identifier(ident) if ident.name == "parseInt" => { | ||
if is_parse_int_call(ctx, ident, None) { | ||
check_arguments(call_expr, ctx); | ||
} | ||
} | ||
Expression::StaticMemberExpression(member_expr) => { | ||
if let Expression::Identifier(ident) = &member_expr.object { | ||
if is_parse_int_call(ctx, ident, Some(member_expr)) { | ||
check_arguments(call_expr, ctx); | ||
} | ||
} else if let Expression::ParenthesizedExpression(paren_expr) = | ||
&member_expr.object | ||
{ | ||
if let Expression::Identifier(ident) = &paren_expr.expression { | ||
if is_parse_int_call(ctx, ident, Some(member_expr)) { | ||
check_arguments(call_expr, ctx); | ||
} | ||
} | ||
} | ||
} | ||
Expression::ChainExpression(chain_expr) => { | ||
if let Some(MemberExpression::StaticMemberExpression(member_expr)) = | ||
chain_expr.expression.as_member_expression() | ||
{ | ||
if let Expression::Identifier(ident) = &member_expr.object { | ||
if is_parse_int_call(ctx, ident, Some(member_expr)) { | ||
check_arguments(call_expr, ctx); | ||
} | ||
} | ||
} | ||
} | ||
_ => {} | ||
} | ||
} | ||
} | ||
} | ||
|
||
fn is_string_type(arg: &Argument) -> bool { | ||
matches!(arg, Argument::StringLiteral(_) | Argument::TemplateLiteral(_)) | ||
} | ||
|
||
fn is_parse_int_call( | ||
ctx: &LintContext, | ||
ident: &IdentifierReference, | ||
static_member_expr: Option<&StaticMemberExpression>, | ||
) -> bool { | ||
if let Some(member_expr) = static_member_expr { | ||
return ident.name == "Number" | ||
&& member_expr.property.name == "parseInt" | ||
&& get_symbol_id_of_variable(ident, ctx).is_none(); | ||
} | ||
|
||
get_symbol_id_of_variable(ident, ctx).is_none() | ||
} | ||
|
||
fn check_arguments<'a>(call_expr: &CallExpression<'a>, ctx: &LintContext<'a>) { | ||
if call_expr.arguments.len() != 2 { | ||
return; | ||
} | ||
|
||
let string_arg = &call_expr.arguments[0]; | ||
if !is_string_type(string_arg) || !string_arg.to_expression().is_immutable_value() { | ||
return; | ||
} | ||
|
||
let radix_arg = &call_expr.arguments[1]; | ||
let Expression::NumericLiteral(numeric_lit) = &radix_arg.to_expression() else { | ||
return; | ||
}; | ||
|
||
if let Some(name) = RADIX_MAP.get(numeric_lit.raw) { | ||
ctx.diagnostic(prefer_numeric_literals_diagnostic(call_expr.span, name)); | ||
} | ||
} | ||
|
||
#[test] | ||
fn test() { | ||
use crate::tester::Tester; | ||
|
||
let pass = vec![ | ||
"parseInt(1);", | ||
"parseInt(1, 3);", | ||
"Number.parseInt(1);", | ||
"Number.parseInt(1, 3);", | ||
"0b111110111 === 503;", | ||
"0o767 === 503;", | ||
"0x1F7 === 503;", | ||
"a[parseInt](1,2);", | ||
"parseInt(foo);", | ||
"parseInt(foo, 2);", | ||
"Number.parseInt(foo);", | ||
"Number.parseInt(foo, 2);", | ||
"parseInt(11, 2);", | ||
"Number.parseInt(1, 8);", | ||
"parseInt(1e5, 16);", | ||
"parseInt('11', '2');", | ||
"Number.parseInt('11', '8');", | ||
"parseInt(/foo/, 2);", | ||
"parseInt(`11${foo}`, 2);", | ||
"parseInt('11', 2n);", // { "ecmaVersion": 2020 }, | ||
"Number.parseInt('11', 8n);", // { "ecmaVersion": 2020 }, | ||
"parseInt('11', 16n);", // { "ecmaVersion": 2020 }, | ||
"parseInt(`11`, 16n);", // { "ecmaVersion": 2020 }, | ||
"parseInt(1n, 2);", // { "ecmaVersion": 2020 }, | ||
r#"class C { #parseInt; foo() { Number.#parseInt("111110111", 2); } }"#, // { "ecmaVersion": 2022 } | ||
]; | ||
|
||
let fail = vec![ | ||
r#"parseInt("111110111", 2) === 503;"#, | ||
r#"parseInt("767", 8) === 503;"#, | ||
r#"parseInt("1F7", 16) === 255;"#, | ||
r#"Number.parseInt("111110111", 2) === 503;"#, | ||
r#"Number.parseInt("767", 8) === 503;"#, | ||
r#"Number.parseInt("1F7", 16) === 255;"#, | ||
"parseInt('7999', 8);", | ||
"parseInt('1234', 2);", | ||
"parseInt('1234.5', 8);", | ||
"parseInt('1️⃣3️⃣3️⃣7️⃣', 16);", | ||
"Number.parseInt('7999', 8);", | ||
"Number.parseInt('1234', 2);", | ||
"Number.parseInt('1234.5', 8);", | ||
"Number.parseInt('1️⃣3️⃣3️⃣7️⃣', 16);", | ||
"parseInt(`111110111`, 2) === 503;", | ||
"parseInt(`767`, 8) === 503;", | ||
"parseInt(`1F7`, 16) === 255;", | ||
"parseInt('', 8);", | ||
"parseInt(``, 8);", | ||
"parseInt(`7999`, 8);", | ||
"parseInt(`1234`, 2);", | ||
"parseInt(`1234.5`, 8);", | ||
"parseInt('11', 2)", | ||
"Number.parseInt('67', 8)", | ||
"5+parseInt('A', 16)", | ||
"function *f(){ yield(Number).parseInt('11', 2) }", // { "ecmaVersion": 6 }, | ||
"function *f(){ yield(Number.parseInt)('67', 8) }", // { "ecmaVersion": 6 }, | ||
"function *f(){ yield(parseInt)('A', 16) }", // { "ecmaVersion": 6 }, | ||
"function *f(){ yield Number.parseInt('11', 2) }", // { "ecmaVersion": 6 }, | ||
"function *f(){ yield/**/Number.parseInt('67', 8) }", // { "ecmaVersion": 6 }, | ||
"function *f(){ yield(parseInt('A', 16)) }", // { "ecmaVersion": 6 }, | ||
"parseInt('11', 2)+5", | ||
"Number.parseInt('17', 8)+5", | ||
"parseInt('A', 16)+5", | ||
"parseInt('11', 2)in foo", | ||
"Number.parseInt('17', 8)in foo", | ||
"parseInt('A', 16)in foo", | ||
"parseInt('11', 2) in foo", | ||
"Number.parseInt('17', 8)/**/in foo", | ||
"(parseInt('A', 16))in foo", | ||
"/* comment */Number.parseInt('11', 2);", | ||
"Number/**/.parseInt('11', 2);", | ||
"Number// | ||
.parseInt('11', 2);", | ||
"Number./**/parseInt('11', 2);", | ||
"Number.parseInt(/**/'11', 2);", | ||
"Number.parseInt('11', /**/2);", | ||
"Number.parseInt('11', 2)/* comment */;", | ||
"parseInt/**/('11', 2);", | ||
"parseInt(// | ||
'11', 2);", | ||
"parseInt('11'/**/, 2);", | ||
"parseInt(`11`/**/, 2);", | ||
"parseInt('11', 2 /**/);", | ||
"parseInt('11', 2)//comment | ||
;", | ||
r#"parseInt?.("1F7", 16) === 255;"#, | ||
r#"Number?.parseInt("1F7", 16) === 255;"#, | ||
r#"Number?.parseInt?.("1F7", 16) === 255;"#, | ||
r#"(Number?.parseInt)("1F7", 16) === 255;"#, | ||
r#"(Number?.parseInt)?.("1F7", 16) === 255;"#, | ||
"parseInt('1_0', 2);", | ||
"Number.parseInt('5_000', 8);", | ||
"parseInt('0_1', 16);", | ||
"Number.parseInt('0_0', 16);", | ||
r#" | ||
parseInt("767", 8) === 503; | ||
function foo() { | ||
function parseInt() { | ||
throw new Error() | ||
} | ||
} | ||
"#, | ||
]; | ||
|
||
// let fix = vec![ | ||
// (r#"parseInt("111110111", 2) === 503;"#, "0b111110111 === 503;", None), | ||
// (r#"parseInt("767", 8) === 503;"#, "0o767 === 503;", None), | ||
// (r#"parseInt("1F7", 16) === 255;"#, "0x1F7 === 255;", None), | ||
// (r#"Number.parseInt("111110111", 2) === 503;"#, "0b111110111 === 503;", None), | ||
// (r#"Number.parseInt("767", 8) === 503;"#, "0o767 === 503;", None), | ||
// (r#"Number.parseInt("1F7", 16) === 255;"#, "0x1F7 === 255;", None), | ||
// ("parseInt(`111110111`, 2) === 503;", "0b111110111 === 503;", None), | ||
// ("parseInt(`767`, 8) === 503;", "0o767 === 503;", None), | ||
// ("parseInt(`1F7`, 16) === 255;", "0x1F7 === 255;", None), | ||
// ("parseInt('11', 2)", "0b11", None), | ||
// ("Number.parseInt('67', 8)", "0o67", None), | ||
// ("5+parseInt('A', 16)", "5+0xA", None), | ||
// ("function *f(){ yield(Number).parseInt('11', 2) }", "function *f(){ yield 0b11 }", None), | ||
// ("function *f(){ yield(Number.parseInt)('67', 8) }", "function *f(){ yield 0o67 }", None), | ||
// ("function *f(){ yield(parseInt)('A', 16) }", "function *f(){ yield 0xA }", None), | ||
// ("function *f(){ yield Number.parseInt('11', 2) }", "function *f(){ yield 0b11 }", None), | ||
// ( | ||
// "function *f(){ yield/**/Number.parseInt('67', 8) }", | ||
// "function *f(){ yield/**/0o67 }", | ||
// None, | ||
// ), | ||
// ("function *f(){ yield(parseInt('A', 16)) }", "function *f(){ yield(0xA) }", None), | ||
// ("parseInt('11', 2)+5", "0b11+5", None), | ||
// ("Number.parseInt('17', 8)+5", "0o17+5", None), | ||
// ("parseInt('A', 16)+5", "0xA+5", None), | ||
// ("parseInt('11', 2)in foo", "0b11 in foo", None), | ||
// ("Number.parseInt('17', 8)in foo", "0o17 in foo", None), | ||
// ("parseInt('A', 16)in foo", "0xA in foo", None), | ||
// ("parseInt('11', 2) in foo", "0b11 in foo", None), | ||
// ("Number.parseInt('17', 8)/**/in foo", "0o17/**/in foo", None), | ||
// ("(parseInt('A', 16))in foo", "(0xA)in foo", None), | ||
// ("/* comment */Number.parseInt('11', 2);", "/* comment */0b11;", None), | ||
// ("Number.parseInt('11', 2)/* comment */;", "0b11/* comment */;", None), | ||
// ( | ||
// "parseInt('11', 2)//comment | ||
// ;", | ||
// "0b11//comment | ||
// ;", | ||
// None, | ||
// ), | ||
// (r#"parseInt?.("1F7", 16) === 255;"#, "0x1F7 === 255;", None), | ||
// (r#"Number?.parseInt("1F7", 16) === 255;"#, "0x1F7 === 255;", None), | ||
// (r#"Number?.parseInt?.("1F7", 16) === 255;"#, "0x1F7 === 255;", None), | ||
// (r#"(Number?.parseInt)("1F7", 16) === 255;"#, "0x1F7 === 255;", None), | ||
// (r#"(Number?.parseInt)?.("1F7", 16) === 255;"#, "0x1F7 === 255;", None), | ||
// ]; | ||
// Tester::new(PreferNumericLiterals::NAME, pass, fail).expect_fix(fix).test_and_snapshot(); | ||
Tester::new(PreferNumericLiterals::NAME, pass, fail).test_and_snapshot(); | ||
} |
Oops, something went wrong.