Skip to content
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
151 changes: 70 additions & 81 deletions crates/oxc_linter/src/rules/eslint/radix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,8 @@ use oxc_macros::declare_oxc_lint;
use oxc_span::{GetSpan, Span};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::Value;

use crate::{
AstNode,
context::LintContext,
rule::{DefaultRuleConfig, Rule},
};
use crate::{AstNode, context::LintContext, rule::Rule};

fn missing_parameters(span: Span) -> OxcDiagnostic {
OxcDiagnostic::warn("Missing parameters.").with_label(span)
Expand All @@ -25,15 +20,13 @@ fn missing_radix(span: Span) -> OxcDiagnostic {
.with_label(span)
}

fn redundant_radix(span: Span) -> OxcDiagnostic {
OxcDiagnostic::warn("Redundant radix parameter.").with_label(span)
}

fn invalid_radix(span: Span) -> OxcDiagnostic {
OxcDiagnostic::warn("Invalid radix parameter, must be an integer between 2 and 36.")
.with_label(span)
}

/// `RadixType` has no affect, it only here for backward compatibility.
/// Without it, the linter will report unknown rule configuration error.
#[derive(Debug, Default, Clone, JsonSchema, Deserialize, Serialize)]
#[serde(rename_all = "camelCase", default, deny_unknown_fields)]
pub struct Radix(RadixType);
Expand All @@ -43,14 +36,16 @@ pub struct Radix(RadixType);
enum RadixType {
/// Always require the radix parameter when using `parseInt()`.
#[default]
#[schemars(skip)]
Always,
/// Only require the radix parameter when necessary.
#[schemars(skip)]
AsNeeded,
}

// doc: https://github.com/eslint/eslint/blob/v9.9.1/docs/src/rules/radix.md
// code: https://github.com/eslint/eslint/blob/v9.9.1/lib/rules/radix.js
// test: https://github.com/eslint/eslint/blob/v9.9.1/tests/lib/rules/radix.js
// doc: https://github.com/eslint/eslint/blob/v10.0.0/docs/src/rules/radix.md
// code: https://github.com/eslint/eslint/blob/v10.0.0/lib/rules/radix.js
// test: https://github.com/eslint/eslint/blob/v10.0.0/tests/lib/rules/radix.js

declare_oxc_lint!(
/// ### What it does
Expand Down Expand Up @@ -85,26 +80,22 @@ declare_oxc_lint!(
);

impl Rule for Radix {
fn from_configuration(value: Value) -> Result<Self, serde_json::error::Error> {
serde_json::from_value::<DefaultRuleConfig<Self>>(value).map(DefaultRuleConfig::into_inner)
}

fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
let AstKind::CallExpression(call_expr) = node.kind() else {
return;
};

match call_expr.callee.without_parentheses() {
Expression::Identifier(ident) if Self::is_global_parse_int_ident(ident, ctx) => {
Self::check_arguments(self, call_expr, ctx);
Self::check_arguments(call_expr, ctx);
}
Expression::StaticMemberExpression(member_expr)
if member_expr.property.name == "parseInt" =>
{
if let Expression::Identifier(ident) = member_expr.object.without_parentheses()
&& Self::is_global_number_ident(ident, ctx)
{
Self::check_arguments(self, call_expr, ctx);
Self::check_arguments(call_expr, ctx);
}
}
Expression::ChainExpression(chain_expr) => {
Expand All @@ -113,7 +104,7 @@ impl Rule for Radix {
&& member_expr.static_property_name() == Some("parseInt")
&& Self::is_global_number_ident(ident, ctx)
{
Self::check_arguments(self, call_expr, ctx);
Self::check_arguments(call_expr, ctx);
}
}
_ => {}
Expand All @@ -130,11 +121,11 @@ impl Radix {
ident.name == "parseInt" && ctx.is_reference_to_global_variable(ident)
}

fn check_arguments(&self, call_expr: &CallExpression, ctx: &LintContext) {
fn check_arguments(call_expr: &CallExpression, ctx: &LintContext) {
match call_expr.arguments.len() {
0 => ctx.diagnostic(missing_parameters(call_expr.span)),
1 => {
if matches!(&self.0, RadixType::Always) {
ctx.diagnostic_with_dangerous_fix(missing_radix(call_expr.span), |fixer| {
let first_arg = &call_expr.arguments[0];
let end = call_expr.span.end;
let check_span = Span::new(first_arg.span().start, end);
Expand All @@ -143,28 +134,19 @@ impl Radix {
.chars()
.find_map(|c| if c == ',' { Some(" 10,") } else { None })
.unwrap_or(", 10");

ctx.diagnostic_with_dangerous_fix(missing_radix(call_expr.span), |fixer| {
fixer.insert_text_before_range(Span::empty(end - 1), insert_param)
});
}
fixer.insert_text_before_range(Span::empty(end - 1), insert_param)
});
}
_ => {
let radix_arg = &call_expr.arguments[1];
if matches!(&self.0, RadixType::AsNeeded) && is_default_radix(radix_arg) {
ctx.diagnostic(redundant_radix(radix_arg.span()));
} else if !is_valid_radix(radix_arg) {
if !is_valid_radix(radix_arg) {
ctx.diagnostic(invalid_radix(radix_arg.span()));
}
}
}
}
}

fn is_default_radix(node: &Argument) -> bool {
node.to_expression().is_specific_raw_number_literal("10")
}

fn is_valid_radix(node: &Argument) -> bool {
let expr = node.to_expression();

Expand All @@ -181,8 +163,6 @@ fn is_valid_radix(node: &Argument) -> bool {

#[test]
fn test() {
use serde_json::json;

use crate::tester::Tester;

let pass = vec![
Expand All @@ -194,43 +174,30 @@ fn test() {
(r#"parseInt("10", 10.0);"#, None, None),
(r#"parseInt("10", foo);"#, None, None),
(r#"Number.parseInt("10", foo);"#, None, None),
(r#"parseInt("10", 10);"#, Some(json!(["always"])), None),
(r#"parseInt("10");"#, Some(json!(["as-needed"])), None),
(r#"parseInt("10", 8);"#, Some(json!(["as-needed"])), None),
(r#"parseInt("10", foo);"#, Some(json!(["as-needed"])), None),
("parseInt", None, None),
("Number.foo();", None, None),
("Number[parseInt]();", None, None),
("class C { #parseInt; foo() { Number.#parseInt(); } }", None, None),
("class C { #parseInt; foo() { Number.#parseInt(foo); } }", None, None),
("class C { #parseInt; foo() { Number.#parseInt(foo, 'bar'); } }", None, None),
(
"class C { #parseInt; foo() { Number.#parseInt(foo, 10); } }",
Some(json!(["as-needed"])),
None,
),
("class C { #parseInt; foo() { Number.#parseInt(); } }", None, None), // { "ecmaVersion": 2022 },
("class C { #parseInt; foo() { Number.#parseInt(foo); } }", None, None), // { "ecmaVersion": 2022 },
("class C { #parseInt; foo() { Number.#parseInt(foo, 'bar'); } }", None, None), // { "ecmaVersion": 2022 },
("var parseInt; parseInt();", None, None),
("var parseInt; parseInt(foo);", Some(json!(["always"])), None),
("var parseInt; parseInt(foo, 10);", Some(json!(["as-needed"])), None),
("var Number; Number.parseInt();", None, None),
("var Number; Number.parseInt(foo);", Some(json!(["always"])), None),
("var Number; Number.parseInt(foo, 10);", Some(json!(["as-needed"])), None),
// ("/* globals parseInt:off */ parseInt(foo);", Some(json!(["always"]))),
(
"Number.parseInt(foo, 10);",
Some(json!(["as-needed"])),
Some(serde_json::json!({"globals": {"Number": "off"} })),
),
(r#"function *f(){ yield(Number).parseInt("10", foo) }"#, None, None), // { "ecmaVersion": 6 },
// ("/* globals parseInt:off */ parseInt(foo);", None, None),
("Number.parseInt(foo);", None, Some(serde_json::json!({"globals": {"Number": "off"} }))),
(r#"parseInt("10", 10);"#, Some(serde_json::json!(["always"])), None),
(r#"parseInt("10", 10);"#, Some(serde_json::json!(["as-needed"])), None),
(r#"parseInt("10", 8);"#, Some(serde_json::json!(["always"])), None),
(r#"parseInt("10", 8);"#, Some(serde_json::json!(["as-needed"])), None),
(r#"parseInt("10", foo);"#, Some(serde_json::json!(["always"])), None),
(r#"parseInt("10", foo);"#, Some(serde_json::json!(["as-needed"])), None),
];

let fail = vec![
("parseInt();", Some(json!(["as-needed"])), None),
("parseInt();", None, None),
(r#"parseInt("10");"#, None, None),
(r#"parseInt("10",);"#, None, None),
(r#"parseInt("10",);"#, None, None), // { "ecmaVersion": 2017 },
(r#"parseInt((0, "10"));"#, None, None),
(r#"parseInt((0, "10"),);"#, None, None),
(r#"parseInt((0, "10"),);"#, None, None), // { "ecmaVersion": 2017 },
(r#"parseInt("10", null);"#, None, None),
(r#"parseInt("10", undefined);"#, None, None),
(r#"parseInt("10", true);"#, None, None),
Expand All @@ -240,36 +207,58 @@ fn test() {
(r#"parseInt("10", 37);"#, None, None),
(r#"parseInt("10", 10.5);"#, None, None),
("Number.parseInt();", None, None),
("Number.parseInt();", Some(json!(["as-needed"])), None),
(r#"Number.parseInt("10");"#, None, None),
(r#"Number.parseInt("10", 1);"#, None, None),
(r#"Number.parseInt("10", 37);"#, None, None),
(r#"Number.parseInt("10", 10.5);"#, None, None),
(r#"parseInt("10", 10);"#, Some(json!(["as-needed"])), None),
(r#"parseInt?.("10");"#, None, None),
(r#"Number.parseInt?.("10");"#, None, None),
(r#"Number?.parseInt("10");"#, None, None),
(r#"(Number?.parseInt)("10");"#, None, None),
("function *f(){ yield(Number).parseInt() }", None, None), // { "ecmaVersion": 6 },
("{ let parseInt; } parseInt();", None, None),
("{ let Number; } Number.parseInt();", None, None),
("{ let Number; } (Number?.parseInt)();", None, None),
(r#"parseInt?.("10");"#, None, None), // { "ecmaVersion": 2020 },
(r#"Number.parseInt?.("10");"#, None, None), // { "ecmaVersion": 2020 },
(r#"Number?.parseInt("10");"#, None, None), // { "ecmaVersion": 2020 },
(r#"(Number?.parseInt)("10");"#, None, None), // { "ecmaVersion": 2020 },
("parseInt();", Some(serde_json::json!(["always"])), None),
("parseInt();", Some(serde_json::json!(["as-needed"])), None),
(r#"parseInt("10");"#, Some(serde_json::json!(["always"])), None),
(r#"parseInt("10");"#, Some(serde_json::json!(["as-needed"])), None),
(r#"parseInt("10", 1);"#, Some(serde_json::json!(["always"])), None),
(r#"parseInt("10", 1);"#, Some(serde_json::json!(["as-needed"])), None),
("Number.parseInt();", Some(serde_json::json!(["always"])), None),
("Number.parseInt();", Some(serde_json::json!(["as-needed"])), None),
];

let fix = vec![
("parseInt(10)", "parseInt(10, 10)", Some(json!(["always"]))),
("parseInt(10,)", "parseInt(10, 10,)", Some(json!(["always"]))),
("parseInt(10 )", "parseInt(10 , 10)", Some(json!(["always"]))),
("parseInt(10, )", "parseInt(10, 10,)", Some(json!(["always"]))),
("parseInt(10)", "parseInt(10, 10)", Some(serde_json::json!(["always"]))),
("parseInt(10,)", "parseInt(10, 10,)", Some(serde_json::json!(["always"]))),
("parseInt(10 )", "parseInt(10 , 10)", Some(serde_json::json!(["always"]))),
(
"parseInt(10, )",
"parseInt(10, 10,)",
Some(serde_json::json!(["always"])),
),
(
r#"parseInt("123123" , )"#,
r#"parseInt("123123" , 10,)"#,
Some(json!(["always"])),
Some(serde_json::json!(["always"])),
),
(
r#"Number.parseInt("10")"#,
r#"Number.parseInt("10", 10)"#,
Some(serde_json::json!(["always"])),
),
(
r#"Number.parseInt("10",)"#,
r#"Number.parseInt("10", 10,)"#,
Some(serde_json::json!(["always"])),
),
(
r#"Number.parseInt?.("10")"#,
r#"Number.parseInt?.("10", 10)"#,
Some(serde_json::json!(["always"])),
),
(
"parseInt(10, /** 213123 */)",
"parseInt(10, /** 213123 */ 10,)",
Some(serde_json::json!(["always"])),
),
(r#"Number.parseInt("10")"#, r#"Number.parseInt("10", 10)"#, Some(json!(["always"]))),
(r#"Number.parseInt("10",)"#, r#"Number.parseInt("10", 10,)"#, Some(json!(["always"]))),
(r#"Number.parseInt?.("10")"#, r#"Number.parseInt?.("10", 10)"#, Some(json!(["always"]))),
("parseInt(10, /** 213123 */)", "parseInt(10, /** 213123 */ 10,)", Some(json!(["always"]))),
];

Tester::new(Radix::NAME, Radix::PLUGIN, pass, fail).expect_fix(fix).test_and_snapshot();
Expand Down
69 changes: 38 additions & 31 deletions crates/oxc_linter/src/snapshots/eslint_radix.snap
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
---
source: crates/oxc_linter/src/tester.rs
---

⚠ eslint(radix): Missing parameters.
╭─[radix.tsx:1:1]
1 │ parseInt();
· ──────────
╰────

⚠ eslint(radix): Missing parameters.
╭─[radix.tsx:1:1]
1 │ parseInt();
Expand Down Expand Up @@ -96,12 +89,6 @@ source: crates/oxc_linter/src/tester.rs
· ─────────────────
╰────

⚠ eslint(radix): Missing parameters.
╭─[radix.tsx:1:1]
1 │ Number.parseInt();
· ─────────────────
╰────

⚠ eslint(radix): Missing radix parameter.
╭─[radix.tsx:1:1]
1 │ Number.parseInt("10");
Expand All @@ -127,12 +114,6 @@ source: crates/oxc_linter/src/tester.rs
· ────
╰────

⚠ eslint(radix): Redundant radix parameter.
╭─[radix.tsx:1:16]
1 │ parseInt("10", 10);
· ──
╰────

⚠ eslint(radix): Missing radix parameter.
╭─[radix.tsx:1:1]
1 │ parseInt?.("10");
Expand Down Expand Up @@ -162,25 +143,51 @@ source: crates/oxc_linter/src/tester.rs
help: Add radix parameter `10` for parsing decimal numbers.

⚠ eslint(radix): Missing parameters.
╭─[radix.tsx:1:21]
1 │ function *f(){ yield(Number).parseInt() }
· ───────────────────
╭─[radix.tsx:1:1]
1 │ parseInt();
· ──────────
╰────

⚠ eslint(radix): Missing parameters.
╭─[radix.tsx:1:19]
1 │ { let parseInt; } parseInt();
· ──────────
╭─[radix.tsx:1:1]
1 │ parseInt();
· ──────────
╰────

⚠ eslint(radix): Missing radix parameter.
╭─[radix.tsx:1:1]
1 │ parseInt("10");
· ──────────────
╰────
help: Add radix parameter `10` for parsing decimal numbers.

⚠ eslint(radix): Missing radix parameter.
╭─[radix.tsx:1:1]
1 │ parseInt("10");
· ──────────────
╰────
help: Add radix parameter `10` for parsing decimal numbers.

⚠ eslint(radix): Invalid radix parameter, must be an integer between 2 and 36.
╭─[radix.tsx:1:16]
1 │ parseInt("10", 1);
· ─
╰────

⚠ eslint(radix): Invalid radix parameter, must be an integer between 2 and 36.
╭─[radix.tsx:1:16]
1 │ parseInt("10", 1);
· ─
╰────

⚠ eslint(radix): Missing parameters.
╭─[radix.tsx:1:17]
1 │ { let Number; } Number.parseInt();
· ─────────────────
╭─[radix.tsx:1:1]
1 │ Number.parseInt();
· ─────────────────
╰────

⚠ eslint(radix): Missing parameters.
╭─[radix.tsx:1:17]
1 │ { let Number; } (Number?.parseInt)();
· ────────────────────
╭─[radix.tsx:1:1]
1 │ Number.parseInt();
· ─────────────────
╰────
Loading