-
-
Notifications
You must be signed in to change notification settings - Fork 339
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(linter): add
radix
rule (#3167)
- Loading branch information
1 parent
9590eb0
commit 7113e85
Showing
3 changed files
with
383 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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] | ||
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"); | ||
· ──────────────────────── | ||
╰──── |