diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 1d8af85a0..dbc94004d 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -113,6 +113,7 @@ mod eslint { pub mod no_var; pub mod no_void; pub mod no_with; + pub mod radix; pub mod require_yield; pub mod use_isnan; pub mod valid_typeof; @@ -467,6 +468,7 @@ oxc_macros::declare_all_lint_rules! { eslint::no_var, eslint::no_void, eslint::no_with, + eslint::radix, eslint::require_yield, eslint::use_isnan, eslint::valid_typeof, diff --git a/crates/oxc_linter/src/rules/eslint/radix.rs b/crates/oxc_linter/src/rules/eslint/radix.rs new file mode 100644 index 000000000..ceafe1ccb --- /dev/null +++ b/crates/oxc_linter/src/rules/eslint/radix.rs @@ -0,0 +1,228 @@ +use oxc_ast::{ + ast::{Argument, CallExpression, Expression}, + AstKind, +}; +use oxc_diagnostics::{ + miette::{self, Diagnostic}, + thiserror::Error, +}; +use oxc_macros::declare_oxc_lint; +use oxc_span::{GetSpan, Span}; + +use crate::{context::LintContext, rule::Rule, AstNode}; + +#[derive(Debug, Error, Diagnostic)] +enum RadixDiagnostic { + #[diagnostic(severity(warning))] + #[error("eslint(radix): Missing parameters.")] + MissingParameters(#[label] Span), + + #[diagnostic(severity(warning))] + #[error("eslint(radix): Missing radix parameter.")] + MissingRadix(#[label] Span), + + #[diagnostic(severity(warning))] + #[error("eslint(radix): Redundant radix parameter.")] + RedundantRadix(#[label] Span), + + #[diagnostic(severity(warning))] + #[error("eslint(radix): Invalid radix parameter, must be an integer between 2 and 36.")] + InvalidRadix(#[label] Span), +} + +#[derive(Debug, Default, Clone)] +pub struct Radix { + radix_type: RadixType, +} + +// doc: https://github.com/eslint/eslint/blob/main/docs/src/rules/radix.md +// code: https://github.com/eslint/eslint/blob/main/lib/rules/radix.js +// test: https://github.com/eslint/eslint/blob/main/tests/lib/rules/radix.js + +declare_oxc_lint!( + /// ### What it does + /// Enforce the consistent use of the radix argument when using `parseInt()`. + /// + /// ### Why is this bad? + /// Using the `parseInt()` function without specifying the radix can lead to unexpected results. + /// + /// ### Example + /// ```javascript + /// // error + /// var num = parseInt("071"); // 57 + /// + /// // success + /// var num = parseInt("071", 10); // 71 + /// ``` + Radix, + pedantic +); + +impl Rule for Radix { + fn from_configuration(value: serde_json::Value) -> Self { + let obj = value.get(0); + + Self { + radix_type: obj + .and_then(serde_json::Value::as_str) + .map(RadixType::from) + .unwrap_or_default(), + } + } + + 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 ctx.symbols().get_symbol_id_from_name("parseInt").is_none() { + Self::check_arguments(self, call_expr, ctx); + } + } + Expression::StaticMemberExpression(member_expr) => { + if let Expression::Identifier(ident) = &member_expr.object { + if ident.name == "Number" + && member_expr.property.name == "parseInt" + && ctx.symbols().get_symbol_id_from_name("Number").is_none() + { + Self::check_arguments(self, call_expr, ctx); + } + } + } + Expression::ChainExpression(chain_expr) => { + if let Some(member_expr) = chain_expr.expression.as_member_expression() { + if let Expression::Identifier(ident) = &member_expr.object() { + if ident.name == "Number" + && member_expr.static_property_name() == Some("parseInt") + && ctx.symbols().get_symbol_id_from_name("Number").is_none() + { + Self::check_arguments(self, call_expr, ctx); + } + } + } + } + _ => {} + } + } + } +} + +impl Radix { + fn check_arguments(&self, call_expr: &CallExpression, ctx: &LintContext) { + match call_expr.arguments.len() { + 0 => ctx.diagnostic(RadixDiagnostic::MissingParameters(call_expr.span)), + 1 => { + if matches!(&self.radix_type, RadixType::Always) { + ctx.diagnostic(RadixDiagnostic::MissingRadix(call_expr.span)); + } + } + _ => { + let radix_arg = &call_expr.arguments[1]; + if matches!(&self.radix_type, RadixType::AsNeeded) && is_default_radix(radix_arg) { + ctx.diagnostic(RadixDiagnostic::RedundantRadix(radix_arg.span())); + } else if !is_valid_radix(radix_arg) { + ctx.diagnostic(RadixDiagnostic::InvalidRadix(radix_arg.span())); + } + } + } + } +} + +#[derive(Debug, Default, Clone)] +enum RadixType { + #[default] + Always, + AsNeeded, +} + +impl RadixType { + pub fn from(raw: &str) -> Self { + match raw { + "as-needed" => Self::AsNeeded, + _ => Self::Always, + } + } +} + +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(); + + if let Expression::NumericLiteral(lit) = expr { + return lit.value.fract() == 0.0 && lit.value >= 2.0 && lit.value <= 36.0; + } + + if let Expression::Identifier(_) = expr { + return !expr.is_undefined(); + } + + false +} + +#[test] +fn test() { + use crate::tester::Tester; + use serde_json::json; + + let pass = vec![ + (r#"parseInt("10", 10);"#, None), + (r#"parseInt("10", 2);"#, None), + (r#"parseInt("10", 36);"#, None), + (r#"parseInt("10", 0x10);"#, None), + (r#"parseInt("10", 1.6e1);"#, None), + (r#"parseInt("10", 10.0);"#, None), + (r#"parseInt("10", foo);"#, None), + (r#"Number.parseInt("10", foo);"#, None), + (r#"parseInt("10", 10);"#, Some(json!(["always"]))), + (r#"parseInt("10");"#, Some(json!(["as-needed"]))), + (r#"parseInt("10", 8);"#, Some(json!(["as-needed"]))), + (r#"parseInt("10", foo);"#, Some(json!(["as-needed"]))), + ("parseInt", None), + ("Number.foo();", None), + ("Number[parseInt]();", None), + ("class C { #parseInt; foo() { Number.#parseInt(); } }", None), + ("class C { #parseInt; foo() { Number.#parseInt(foo); } }", None), + ("class C { #parseInt; foo() { Number.#parseInt(foo, 'bar'); } }", None), + ("class C { #parseInt; foo() { Number.#parseInt(foo, 10); } }", Some(json!(["as-needed"]))), + ("var parseInt; parseInt();", None), + ("var parseInt; parseInt(foo);", Some(json!(["always"]))), + ("var parseInt; parseInt(foo, 10);", Some(json!(["as-needed"]))), + ("var Number; Number.parseInt();", None), + ("var Number; Number.parseInt(foo);", Some(json!(["always"]))), + ("var Number; Number.parseInt(foo, 10);", Some(json!(["as-needed"]))), + // ("/* globals parseInt:off */ parseInt(foo);", Some(json!(["always"]))), + // ("Number.parseInt(foo, 10);", Some(json!(["as-needed"]))), // { globals: { Number: "off" } } + ]; + + let fail = vec![ + ("parseInt();", Some(json!(["as-needed"]))), + ("parseInt();", None), + (r#"parseInt("10");"#, None), + (r#"parseInt("10",);"#, None), + (r#"parseInt((0, "10"));"#, None), + (r#"parseInt((0, "10"),);"#, None), + (r#"parseInt("10", null);"#, None), + (r#"parseInt("10", undefined);"#, None), + (r#"parseInt("10", true);"#, None), + (r#"parseInt("10", "foo");"#, None), + (r#"parseInt("10", "123");"#, None), + (r#"parseInt("10", 1);"#, None), + (r#"parseInt("10", 37);"#, None), + (r#"parseInt("10", 10.5);"#, None), + ("Number.parseInt();", None), + ("Number.parseInt();", Some(json!(["as-needed"]))), + (r#"Number.parseInt("10");"#, None), + (r#"Number.parseInt("10", 1);"#, None), + (r#"Number.parseInt("10", 37);"#, None), + (r#"Number.parseInt("10", 10.5);"#, None), + (r#"parseInt("10", 10);"#, Some(json!(["as-needed"]))), + (r#"parseInt?.("10");"#, None), + (r#"Number.parseInt?.("10");"#, None), + (r#"Number?.parseInt("10");"#, None), + (r#"(Number?.parseInt)("10");"#, None), + ]; + + Tester::new(Radix::NAME, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/radix.snap b/crates/oxc_linter/src/snapshots/radix.snap new file mode 100644 index 000000000..613ad0608 --- /dev/null +++ b/crates/oxc_linter/src/snapshots/radix.snap @@ -0,0 +1,153 @@ +--- +source: crates/oxc_linter/src/tester.rs +expression: radix +--- + ⚠ eslint(radix): Missing parameters. + ╭─[radix.tsx:1:1] + 1 │ parseInt(); + · ────────── + ╰──── + + ⚠ eslint(radix): Missing parameters. + ╭─[radix.tsx:1:1] + 1 │ parseInt(); + · ────────── + ╰──── + + ⚠ eslint(radix): Missing radix parameter. + ╭─[radix.tsx:1:1] + 1 │ parseInt("10"); + · ────────────── + ╰──── + + ⚠ eslint(radix): Missing radix parameter. + ╭─[radix.tsx:1:1] + 1 │ parseInt("10",); + · ─────────────── + ╰──── + + ⚠ eslint(radix): Missing radix parameter. + ╭─[radix.tsx:1:1] + 1 │ parseInt((0, "10")); + · ─────────────────── + ╰──── + + ⚠ eslint(radix): Missing radix parameter. + ╭─[radix.tsx:1:1] + 1 │ parseInt((0, "10"),); + · ──────────────────── + ╰──── + + ⚠ eslint(radix): Invalid radix parameter, must be an integer between 2 and 36. + ╭─[radix.tsx:1:16] + 1 │ parseInt("10", null); + · ──── + ╰──── + + ⚠ eslint(radix): Invalid radix parameter, must be an integer between 2 and 36. + ╭─[radix.tsx:1:16] + 1 │ parseInt("10", undefined); + · ───────── + ╰──── + + ⚠ eslint(radix): Invalid radix parameter, must be an integer between 2 and 36. + ╭─[radix.tsx:1:16] + 1 │ parseInt("10", true); + · ──── + ╰──── + + ⚠ eslint(radix): Invalid radix parameter, must be an integer between 2 and 36. + ╭─[radix.tsx:1:16] + 1 │ parseInt("10", "foo"); + · ───── + ╰──── + + ⚠ eslint(radix): Invalid radix parameter, must be an integer between 2 and 36. + ╭─[radix.tsx:1:16] + 1 │ parseInt("10", "123"); + · ───── + ╰──── + + ⚠ 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", 37); + · ── + ╰──── + + ⚠ eslint(radix): Invalid radix parameter, must be an integer between 2 and 36. + ╭─[radix.tsx:1:16] + 1 │ parseInt("10", 10.5); + · ──── + ╰──── + + ⚠ eslint(radix): Missing parameters. + ╭─[radix.tsx:1:1] + 1 │ Number.parseInt(); + · ───────────────── + ╰──── + + ⚠ eslint(radix): Missing parameters. + ╭─[radix.tsx:1:1] + 1 │ Number.parseInt(); + · ───────────────── + ╰──── + + ⚠ eslint(radix): Missing radix parameter. + ╭─[radix.tsx:1:1] + 1 │ Number.parseInt("10"); + · ───────────────────── + ╰──── + + ⚠ eslint(radix): Invalid radix parameter, must be an integer between 2 and 36. + ╭─[radix.tsx:1:23] + 1 │ Number.parseInt("10", 1); + · ─ + ╰──── + + ⚠ eslint(radix): Invalid radix parameter, must be an integer between 2 and 36. + ╭─[radix.tsx:1:23] + 1 │ Number.parseInt("10", 37); + · ── + ╰──── + + ⚠ eslint(radix): Invalid radix parameter, must be an integer between 2 and 36. + ╭─[radix.tsx:1:23] + 1 │ Number.parseInt("10", 10.5); + · ──── + ╰──── + + ⚠ 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"); + · ──────────────── + ╰──── + + ⚠ eslint(radix): Missing radix parameter. + ╭─[radix.tsx:1:1] + 1 │ Number.parseInt?.("10"); + · ─────────────────────── + ╰──── + + ⚠ eslint(radix): Missing radix parameter. + ╭─[radix.tsx:1:1] + 1 │ Number?.parseInt("10"); + · ────────────────────── + ╰──── + + ⚠ eslint(radix): Missing radix parameter. + ╭─[radix.tsx:1:1] + 1 │ (Number?.parseInt)("10"); + · ──────────────────────── + ╰────