-
-
Notifications
You must be signed in to change notification settings - Fork 368
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(linter) eslint plugin unicorn: prefer number properties
- Loading branch information
Showing
3 changed files
with
696 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
276 changes: 276 additions & 0 deletions
276
crates/oxc_linter/src/rules/unicorn/prefer_number_properties.rs
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,276 @@ | ||
use oxc_ast::{ast::Expression, AstKind}; | ||
use oxc_diagnostics::{ | ||
miette::{self, Diagnostic}, | ||
thiserror::{self, Error}, | ||
}; | ||
use oxc_macros::declare_oxc_lint; | ||
use oxc_span::{GetSpan, Span}; | ||
|
||
use crate::{context::LintContext, globals::GLOBAL_OBJECT_NAMES, rule::Rule, AstNode}; | ||
|
||
#[derive(Debug, Error, Diagnostic)] | ||
#[error( | ||
"eslint-plugin-unicorn(prefer-number-properties): Use `Number.{1}` instead of the global `{1}`" | ||
)] | ||
#[diagnostic(severity(warning), help("Replace it with `Number.{1}`"))] | ||
struct PreferNumberPropertiesDiagnostic(#[label] pub Span, pub String); | ||
|
||
#[derive(Debug, Default, Clone)] | ||
pub struct PreferNumberProperties; | ||
|
||
declare_oxc_lint!( | ||
/// ### What it does | ||
/// | ||
/// Disallows use of `parseInt()`, `parseFloat()`, `isNan()`, `isFinite()`, `Nan`, `Infinity` and `-Infinity` as global variables. | ||
/// | ||
/// ### Why is this bad? | ||
/// | ||
/// ECMAScript 2015 moved globals onto the `Number` constructor for consistency and to slightly improve them. This rule enforces their usage to limit the usage of globals: | ||
/// | ||
/// - [`Number.parseInt()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/parseInt) over [`parseInt()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseInt) | ||
/// - [`Number.parseFloat()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/parseFloat) over [`parseFloat()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseFloat) | ||
/// - [`Number.isNaN()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isNaN) over [`isNaN()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/isNaN) *(they have slightly [different behavior](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isNaN#difference_between_number.isnan_and_global_isnan))* | ||
/// - [`Number.isFinite()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isFinite) over [`isFinite()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/isFinite) *(they have slightly [different behavior](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isFinite#difference_between_number.isfinite_and_global_isfinite))* | ||
/// - [`Number.NaN`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/NaN) over [`NaN`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/NaN) | ||
/// - [`Number.POSITIVE_INFINITY`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/POSITIVE_INFINITY) over [`Infinity`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Infinity) | ||
/// - [`Number.NEGATIVE_INFINITY`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/NEGATIVE_INFINITY) over [`-Infinity`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Infinity) | ||
/// | ||
/// ### Example | ||
/// ```javascript | ||
/// // bad | ||
/// const foo = parseInt('10', 2); | ||
/// const bar = parseFloat('10.5'); | ||
/// | ||
/// // good | ||
/// const foo = Number.parseInt('10', 2); | ||
/// const bar = Number.parseFloat('10.5'); | ||
/// ``` | ||
PreferNumberProperties, | ||
restriction, | ||
); | ||
|
||
impl Rule for PreferNumberProperties { | ||
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { | ||
match node.kind() { | ||
AstKind::MemberExpression(member_expr) => { | ||
let Expression::Identifier(ident_name) = member_expr.object() else { | ||
return; | ||
}; | ||
|
||
if GLOBAL_OBJECT_NAMES.contains(ident_name.name.as_str()) { | ||
match member_expr.static_property_name() { | ||
Some("NaN") => { | ||
ctx.diagnostic(PreferNumberPropertiesDiagnostic( | ||
member_expr.span(), | ||
"NaN".to_string(), | ||
)); | ||
} | ||
Some("Infinity") => { | ||
ctx.diagnostic(PreferNumberPropertiesDiagnostic( | ||
member_expr.span(), | ||
"Infinity".to_string(), | ||
)); | ||
} | ||
_ => {} | ||
} | ||
} | ||
} | ||
AstKind::IdentifierReference(ident_ref) => match ident_ref.name.as_str() { | ||
"NaN" | "Infinity" => { | ||
ctx.diagnostic(PreferNumberPropertiesDiagnostic( | ||
ident_ref.span, | ||
ident_ref.name.to_string(), | ||
)); | ||
} | ||
_ => {} | ||
}, | ||
AstKind::IdentifierName(ident_name) => { | ||
if matches!( | ||
ctx.nodes().parent_kind(node.id()), | ||
Some(AstKind::MemberExpression(_) | AstKind::PropertyKey(_)) | ||
) { | ||
return; | ||
}; | ||
|
||
match ident_name.name.as_str() { | ||
"NaN" | "Infinity" => { | ||
ctx.diagnostic(PreferNumberPropertiesDiagnostic( | ||
ident_name.span, | ||
ident_name.name.to_string(), | ||
)); | ||
} | ||
_ => {} | ||
} | ||
} | ||
AstKind::CallExpression(call_expr) => { | ||
let Some(ident_name) = extract_ident_from_expression(&call_expr.callee) else { | ||
return; | ||
}; | ||
|
||
if matches!(ident_name, "isNaN" | "isFinite" | "parseFloat" | "parseInt") { | ||
ctx.diagnostic(PreferNumberPropertiesDiagnostic( | ||
call_expr.callee.span(), | ||
ident_name.to_string(), | ||
)); | ||
} | ||
} | ||
_ => {} | ||
} | ||
} | ||
} | ||
|
||
fn extract_ident_from_expression<'b>(expr: &'b Expression<'_>) -> Option<&'b str> { | ||
match expr { | ||
Expression::Identifier(ident_name) => Some(ident_name.name.as_str()), | ||
Expression::MemberExpression(member_expr) => { | ||
let Expression::Identifier(ident_name) = member_expr.object() else { | ||
return None; | ||
}; | ||
|
||
if GLOBAL_OBJECT_NAMES.contains(ident_name.name.as_str()) { | ||
member_expr.static_property_name() | ||
} else { | ||
None | ||
} | ||
} | ||
_ => None, | ||
} | ||
} | ||
|
||
#[test] | ||
fn test() { | ||
use crate::tester::Tester; | ||
|
||
let pass = vec![ | ||
(r#"Number.parseInt("10", 2);"#, None), | ||
(r#"Number.parseFloat("10.5");"#, None), | ||
(r"Number.isNaN(10);", None), | ||
(r"Number.isFinite(10);", None), | ||
(r"global.isFinite = Number.isFinite;", None), | ||
(r"global.isFinite ??= 1;", None), | ||
(r"isFinite ||= 1;", None), | ||
(r"[global.isFinite] = [];", None), | ||
(r"[global.isFinite = 1] = [];", None), | ||
(r"[[global.isFinite = 1]] = [];", None), | ||
(r"[isFinite] = [];", None), | ||
(r"[isFinite = 1] = [];", None), | ||
(r"[[isFinite = 1]] = [];", None), | ||
(r"({foo: global.isFinite} = {});", None), | ||
(r"({foo: global.isFinite = 1} = {});", None), | ||
(r"({foo: {bar: global.isFinite = 1}} = {});", None), | ||
(r"({foo: isFinite} = {});", None), | ||
(r"({foo: isFinite = 1} = {});", None), | ||
(r"({foo: {bar: isFinite = 1}} = {});", None), | ||
(r"delete global.isFinite;", None), | ||
(r"const foo = Number.NaN;", None), | ||
(r"const foo = window.Number.NaN;", None), | ||
(r"const foo = bar.NaN;", None), | ||
(r"const foo = nan;", None), | ||
(r#"const foo = "NaN";"#, None), | ||
(r"const {NaN} = {};", None), | ||
(r"const {a: NaN} = {};", None), | ||
(r"const {[a]: NaN} = {};", None), | ||
(r"const [NaN] = [];", None), | ||
(r"function NaN() {}", None), | ||
(r"const foo = function NaN() {}", None), | ||
(r"function foo(NaN) {}", None), | ||
(r"foo = function (NaN) {}", None), | ||
(r"foo = (NaN) => {}", None), | ||
(r"function foo({NaN}) {}", None), | ||
(r"function foo({a: NaN}) {}", None), | ||
(r"function foo({[a]: NaN}) {}", None), | ||
(r"function foo([NaN]) {}", None), | ||
(r"class NaN {}", None), | ||
(r"const Foo = class NaN {}", None), | ||
(r"class Foo {NaN(){}}", None), | ||
(r"class Foo {#NaN(){}}", None), | ||
(r"class Foo3 {NaN = 1}", None), | ||
(r"class Foo {#NaN = 1}", None), | ||
(r#"import {NaN} from "foo""#, None), | ||
(r#"import {NaN as NaN} from "foo""#, None), | ||
(r#"import NaN from "foo""#, None), | ||
(r#"import * as NaN from "foo""#, None), | ||
(r#"export {NaN} from "foo""#, None), | ||
(r#"export {NaN as NaN} from "foo""#, None), | ||
(r#"export * as NaN from "foo""#, None), | ||
(r"const foo = Number.POSITIVE_INFINITY;", None), | ||
(r"const foo = window.Number.POSITIVE_INFINITY;", None), | ||
(r"const foo = bar.POSITIVE_INFINITY;", None), | ||
(r"const foo = Number.Infinity;", None), | ||
(r"const foo = window.Number.Infinity;", None), | ||
(r"const foo = bar.Infinity;", None), | ||
(r"const foo = infinity;", None), | ||
(r#"const foo = "Infinite";"#, None), | ||
(r#"const foo = "-Infinity";"#, None), | ||
(r"const {Infinity} = {};", None), | ||
(r"function Infinity() {}", None), | ||
(r"class Infinity {}", None), | ||
(r"class Foo { Infinity(){}}", None), | ||
// (r#"const foo = Infinity;"#, Some(serde_json::json!([{"checkInfinity": false}]))), | ||
// (r#"const foo = -Infinity;"#, Some(serde_json::json!([{"checkInfinity": false}]))), | ||
(r"class Foo2 {NaN = 1}", None), | ||
(r"declare var NaN: number;", None), | ||
(r"declare function NaN(s: string, radix?: number): number;", None), | ||
(r"class Foo {NaN = 1}", None), | ||
]; | ||
|
||
let fail = vec![ | ||
(r"const foo = NaN;", None), | ||
(r"if (Number.isNaN(NaN)) {}", None), | ||
(r"if (Object.is(foo, NaN)) {}", None), | ||
(r"const foo = bar[NaN];", None), | ||
(r"const foo = {NaN};", None), | ||
(r"const foo = {NaN: NaN};", None), | ||
(r"const {foo = NaN} = {};", None), | ||
(r"const foo = NaN.toString();", None), | ||
(r"class Foo3 {[NaN] = 1}", None), | ||
(r"class Foo2 {[NaN] = 1}", None), | ||
(r"class Foo {[NaN] = 1}", None), | ||
(r"const foo = {[NaN]: 1}", None), | ||
(r"const foo = {[NaN]() {}}", None), | ||
(r"foo[NaN] = 1;", None), | ||
(r"class A {[NaN](){}}", None), | ||
(r"foo = {[NaN]: 1}", None), | ||
(r"const foo = Infinity;", None), | ||
(r"if (Number.isNaN(Infinity)) {}", None), | ||
(r"if (Object.is(foo, Infinity)) {}", None), | ||
(r"const foo = bar[Infinity];", None), | ||
(r"const foo = {Infinity};", None), | ||
(r"const foo = {Infinity: Infinity};", None), | ||
(r"const foo = {[Infinity]: -Infinity};", None), | ||
(r"const foo = {[-Infinity]: Infinity};", None), | ||
(r"const foo = {Infinity: -Infinity};", None), | ||
(r"const {foo = Infinity} = {};", None), | ||
(r"const {foo = -Infinity} = {};", None), | ||
(r"const foo = Infinity.toString();", None), | ||
(r"const foo = -Infinity.toString();", None), | ||
(r"const foo = (-Infinity).toString();", None), | ||
(r"const foo = +Infinity;", None), | ||
(r"const foo = +-Infinity;", None), | ||
(r"const foo = -Infinity;", None), | ||
(r"const foo = -(-Infinity);", None), | ||
(r"const foo = 1 - Infinity;", None), | ||
(r"const foo = 1 - -Infinity;", None), | ||
(r"const isPositiveZero = value => value === 0 && 1 / value === Infinity;", None), | ||
(r"const isNegativeZero = value => value === 0 && 1 / value === -Infinity;", None), | ||
(r"const {a = NaN} = {};", None), | ||
(r"const {[NaN]: a = NaN} = {};", None), | ||
(r"const [a = NaN] = [];", None), | ||
(r"function foo({a = NaN}) {}", None), | ||
(r"function foo({[NaN]: a = NaN}) {}", None), | ||
(r"function foo([a = NaN]) {}", None), | ||
(r"function foo() {return-Infinity}", None), | ||
(r"globalThis.isNaN(foo);", None), | ||
(r"global.isNaN(foo);", None), | ||
(r"window.isNaN(foo);", None), | ||
(r"self.isNaN(foo);", None), | ||
(r"globalThis.parseFloat(foo);", None), | ||
(r"global.parseFloat(foo);", None), | ||
(r"window.parseFloat(foo);", None), | ||
(r"self.parseFloat(foo);", None), | ||
(r"globalThis.NaN", None), | ||
(r"-globalThis.Infinity", None), | ||
]; | ||
|
||
Tester::new(PreferNumberProperties::NAME, pass, fail).test_and_snapshot(); | ||
} |
Oops, something went wrong.