Skip to content

Commit

Permalink
feat(linter): add radix rule (#3167)
Browse files Browse the repository at this point in the history
  • Loading branch information
KubaJastrz committed May 8, 2024
1 parent 9590eb0 commit 7113e85
Show file tree
Hide file tree
Showing 3 changed files with 383 additions and 0 deletions.
2 changes: 2 additions & 0 deletions crates/oxc_linter/src/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
228 changes: 228 additions & 0 deletions crates/oxc_linter/src/rules/eslint/radix.rs
Original file line number Diff line number Diff line change
@@ -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();
}
153 changes: 153 additions & 0 deletions crates/oxc_linter/src/snapshots/radix.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
---
source: crates/oxc_linter/src/tester.rs
expression: radix
---
eslint(radix): Missing parameters.
╭─[radix.tsx:1:1]
1parseInt();
· ──────────
╰────

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

eslint(radix): Missing radix parameter.
╭─[radix.tsx:1:1]
1parseInt("10");
· ──────────────
╰────

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

eslint(radix): Missing radix parameter.
╭─[radix.tsx:1:1]
1parseInt((0, "10"));
· ───────────────────
╰────

eslint(radix): Missing radix parameter.
╭─[radix.tsx:1:1]
1parseInt((0, "10"),);
· ────────────────────
╰────

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

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

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

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

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

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

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

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

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

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

eslint(radix): Missing radix parameter.
╭─[radix.tsx:1:1]
1Number.parseInt("10");
· ─────────────────────
╰────

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

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

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

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

eslint(radix): Missing radix parameter.
╭─[radix.tsx:1:1]
1parseInt?.("10");
· ────────────────
╰────

eslint(radix): Missing radix parameter.
╭─[radix.tsx:1:1]
1Number.parseInt?.("10");
· ───────────────────────
╰────

eslint(radix): Missing radix parameter.
╭─[radix.tsx:1:1]
1Number?.parseInt("10");
· ──────────────────────
╰────

eslint(radix): Missing radix parameter.
╭─[radix.tsx:1:1]
1 │ (Number?.parseInt)("10");
· ────────────────────────
╰────

0 comments on commit 7113e85

Please sign in to comment.