mirror of
https://github.com/danbulant/oxc
synced 2026-05-19 04:08:41 +00:00
feat(linter): add radix rule (#3167)
This commit is contained in:
parent
9590eb0cf4
commit
7113e850b4
3 changed files with 383 additions and 0 deletions
|
|
@ -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,
|
||||
|
|
|
|||
228
crates/oxc_linter/src/rules/eslint/radix.rs
Normal file
228
crates/oxc_linter/src/rules/eslint/radix.rs
Normal 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();
|
||||
}
|
||||
153
crates/oxc_linter/src/snapshots/radix.snap
Normal file
153
crates/oxc_linter/src/snapshots/radix.snap
Normal 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");
|
||||
· ────────────────────────
|
||||
╰────
|
||||
Loading…
Reference in a new issue