feat(linter): add radix rule (#3167)

This commit is contained in:
Kuba Jastrzębski 2024-05-08 17:44:12 +02:00 committed by GitHub
parent 9590eb0cf4
commit 7113e850b4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 383 additions and 0 deletions

View file

@ -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,

View file

@ -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();
}

View file

@ -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");
· ────────────────────────
╰────