This repository has been archived by the owner on Aug 31, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 656
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(rome_js_analyze):
useSimpleNumberKeys
(#4447)
- Loading branch information
Showing
15 changed files
with
790 additions
and
4 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
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
360 changes: 360 additions & 0 deletions
360
crates/rome_js_analyze/src/analyzers/nursery/use_simple_number_keys.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,360 @@ | ||
use crate::JsRuleAction; | ||
use rome_analyze::{context::RuleContext, declare_rule, ActionCategory, Ast, Rule, RuleDiagnostic}; | ||
use rome_console::markup; | ||
use rome_diagnostics::Applicability; | ||
use rome_js_factory::make; | ||
use rome_js_syntax::{ | ||
AnyJsObjectMember, JsLiteralMemberName, JsObjectExpression, JsSyntaxKind, TextRange, | ||
}; | ||
use rome_rowan::{AstNode, BatchMutationExt}; | ||
use std::str::FromStr; | ||
|
||
declare_rule! { | ||
/// Disallow number literal object member names which are not base10 or uses underscore as separator | ||
/// | ||
/// ## Examples | ||
/// | ||
/// ### Invalid | ||
/// | ||
/// ```js,expect_diagnostic | ||
/// ({ 0x1: 1 }); | ||
/// ``` | ||
/// ```js,expect_diagnostic | ||
/// ({ 11_1.11: "ee" }); | ||
/// ``` | ||
/// ```js,expect_diagnostic | ||
/// ({ 0o1: 1 }); | ||
/// ``` | ||
/// ```js,expect_diagnostic | ||
/// ({ 1n: 1 }); | ||
/// ``` | ||
/// ```js,expect_diagnostic | ||
/// ({ 11_1.11: "ee" }); | ||
/// ``` | ||
/// | ||
/// ## Valid | ||
/// | ||
/// ```js | ||
/// ({ 0: "zero" }); | ||
/// ({ 122: "integer" }); | ||
/// ({ 1.22: "floating point" }); | ||
/// ({ 3.1e12: "floating point with e" }); | ||
/// ``` | ||
/// | ||
pub(crate) UseSimpleNumberKeys { | ||
version: "12.1.0", | ||
name: "useSimpleNumberKeys", | ||
recommended: false, | ||
} | ||
} | ||
|
||
#[derive(Clone)] | ||
pub enum NumberLiteral { | ||
Binary { | ||
node: JsLiteralMemberName, | ||
value: String, | ||
big_int: bool, | ||
}, | ||
Decimal { | ||
node: JsLiteralMemberName, | ||
value: String, | ||
big_int: bool, | ||
underscore: bool, | ||
}, | ||
Octal { | ||
node: JsLiteralMemberName, | ||
value: String, | ||
big_int: bool, | ||
}, | ||
Hexadecimal { | ||
node: JsLiteralMemberName, | ||
value: String, | ||
big_int: bool, | ||
}, | ||
FloatingPoint { | ||
node: JsLiteralMemberName, | ||
value: String, | ||
exponent: bool, | ||
underscore: bool, | ||
}, | ||
} | ||
|
||
pub struct NumberLiteralError; | ||
|
||
impl TryFrom<AnyJsObjectMember> for NumberLiteral { | ||
type Error = NumberLiteralError; | ||
|
||
fn try_from(any_member: AnyJsObjectMember) -> Result<Self, Self::Error> { | ||
let literal_member_name_syntax = any_member | ||
.syntax() | ||
.children() | ||
.find(|x| JsLiteralMemberName::can_cast(x.kind())) | ||
.unwrap(); | ||
let literal_member_name = JsLiteralMemberName::cast(literal_member_name_syntax).unwrap(); | ||
|
||
let token = literal_member_name.value().unwrap(); | ||
match token.kind() { | ||
JsSyntaxKind::JS_NUMBER_LITERAL | JsSyntaxKind::JS_BIGINT_LITERAL => { | ||
let chars: Vec<char> = token.to_string().chars().collect(); | ||
let mut value = String::new(); | ||
|
||
let mut is_first_char_zero: bool = false; | ||
let mut is_second_char_a_letter: Option<char> = None; | ||
let mut contains_dot: bool = false; | ||
let mut exponent: bool = false; | ||
let mut largest_digit: char = '0'; | ||
let mut underscore: bool = false; | ||
let mut big_int: bool = false; | ||
|
||
for i in 0..chars.len() { | ||
if i == 0 && chars[i] == '0' && chars.len() > 1 { | ||
is_first_char_zero = true; | ||
continue; | ||
} | ||
|
||
if chars[i] == 'n' { | ||
big_int = true; | ||
break; | ||
} | ||
|
||
if chars[i] == 'e' || chars[i] == 'E' { | ||
exponent = true; | ||
} | ||
|
||
if i == 1 && chars[i].is_alphabetic() && !exponent { | ||
is_second_char_a_letter = Some(chars[i]); | ||
continue; | ||
} | ||
|
||
if chars[i] == '_' { | ||
underscore = true; | ||
continue; | ||
} | ||
|
||
if chars[i] == '.' { | ||
contains_dot = true; | ||
} | ||
|
||
if largest_digit < chars[i] { | ||
largest_digit = chars[i]; | ||
} | ||
|
||
value.push(chars[i]) | ||
} | ||
|
||
if contains_dot { | ||
return Ok(Self::FloatingPoint { | ||
node: literal_member_name, | ||
value, | ||
exponent, | ||
underscore, | ||
}); | ||
}; | ||
if !is_first_char_zero { | ||
return Ok(Self::Decimal { | ||
node: literal_member_name, | ||
value, | ||
big_int, | ||
underscore, | ||
}); | ||
}; | ||
|
||
match is_second_char_a_letter { | ||
Some('b' | 'B') => { | ||
return Ok(Self::Binary { | ||
node: literal_member_name, | ||
value, | ||
big_int, | ||
}) | ||
} | ||
Some('o' | 'O') => { | ||
return Ok(Self::Octal { | ||
node: literal_member_name, | ||
value, | ||
big_int, | ||
}) | ||
} | ||
Some('x' | 'X') => { | ||
return Ok(Self::Hexadecimal { | ||
node: literal_member_name, | ||
value, | ||
big_int, | ||
}) | ||
} | ||
_ => (), | ||
} | ||
|
||
if largest_digit < '8' { | ||
return Ok(Self::Octal { | ||
node: literal_member_name, | ||
value, | ||
big_int, | ||
}); | ||
} | ||
|
||
Ok(Self::Decimal { | ||
node: literal_member_name, | ||
value, | ||
big_int, | ||
underscore, | ||
}) | ||
} | ||
_ => Err(NumberLiteralError), | ||
} | ||
} | ||
} | ||
|
||
impl NumberLiteral { | ||
fn node(&self) -> JsLiteralMemberName { | ||
match self { | ||
Self::Decimal { node, .. } => node.clone(), | ||
Self::Binary { node, .. } => node.clone(), | ||
Self::FloatingPoint { node, .. } => node.clone(), | ||
Self::Octal { node, .. } => node.clone(), | ||
Self::Hexadecimal { node, .. } => node.clone(), | ||
} | ||
} | ||
|
||
fn range(&self) -> TextRange { | ||
match self { | ||
Self::Decimal { node, .. } => node.range(), | ||
Self::Binary { node, .. } => node.range(), | ||
Self::FloatingPoint { node, .. } => node.range(), | ||
Self::Octal { node, .. } => node.range(), | ||
Self::Hexadecimal { node, .. } => node.range(), | ||
} | ||
} | ||
|
||
fn value(&self) -> &String { | ||
match self { | ||
Self::Decimal { value, .. } => value, | ||
Self::Binary { value, .. } => value, | ||
Self::FloatingPoint { value, .. } => value, | ||
Self::Octal { value, .. } => value, | ||
Self::Hexadecimal { value, .. } => value, | ||
} | ||
} | ||
} | ||
|
||
impl NumberLiteral { | ||
fn to_base_ten(&self) -> Option<f64> { | ||
match self { | ||
Self::Binary { value, .. } => i64::from_str_radix(value, 2).map(|num| num as f64).ok(), | ||
Self::Decimal { value, .. } | Self::FloatingPoint { value, .. } => { | ||
f64::from_str(value).ok() | ||
} | ||
Self::Octal { value, .. } => i64::from_str_radix(value, 7).map(|num| num as f64).ok(), | ||
Self::Hexadecimal { value, .. } => { | ||
i64::from_str_radix(value, 16).map(|num| num as f64).ok() | ||
} | ||
} | ||
} | ||
} | ||
|
||
enum WrongNumberLiteralName { | ||
Binary, | ||
Hexadecimal, | ||
Octal, | ||
BigInt, | ||
WithUnderscore, | ||
} | ||
pub struct RuleState(WrongNumberLiteralName, NumberLiteral); | ||
|
||
impl Rule for UseSimpleNumberKeys { | ||
type Query = Ast<JsObjectExpression>; | ||
type State = RuleState; | ||
type Signals = Vec<Self::State>; | ||
type Options = (); | ||
|
||
fn run(ctx: &RuleContext<Self>) -> Self::Signals { | ||
let mut signals: Self::Signals = Vec::new(); | ||
let node = ctx.query(); | ||
|
||
for number_literal in node | ||
.members() | ||
.into_iter() | ||
.flatten() | ||
.filter_map(|member| NumberLiteral::try_from(member).ok()) | ||
{ | ||
match number_literal { | ||
NumberLiteral::Decimal { big_int: true, .. } => { | ||
signals.push(RuleState(WrongNumberLiteralName::BigInt, number_literal)) | ||
} | ||
NumberLiteral::FloatingPoint { | ||
underscore: true, .. | ||
} | ||
| NumberLiteral::Decimal { | ||
underscore: true, .. | ||
} => signals.push(RuleState( | ||
WrongNumberLiteralName::WithUnderscore, | ||
number_literal, | ||
)), | ||
NumberLiteral::Binary { .. } => { | ||
signals.push(RuleState(WrongNumberLiteralName::Binary, number_literal)) | ||
} | ||
NumberLiteral::Hexadecimal { .. } => signals.push(RuleState( | ||
WrongNumberLiteralName::Hexadecimal, | ||
number_literal, | ||
)), | ||
NumberLiteral::Octal { .. } => { | ||
signals.push(RuleState(WrongNumberLiteralName::Octal, number_literal)) | ||
} | ||
_ => (), | ||
} | ||
} | ||
|
||
signals | ||
} | ||
|
||
fn diagnostic( | ||
_ctx: &RuleContext<Self>, | ||
RuleState(reason, literal): &Self::State, | ||
) -> Option<RuleDiagnostic> { | ||
let title = match reason { | ||
WrongNumberLiteralName::BigInt => "Bigint is not allowed here.", | ||
WrongNumberLiteralName::WithUnderscore => { | ||
"Number literal with underscore is not allowed here." | ||
} | ||
WrongNumberLiteralName::Binary => "Binary number literal in is not allowed here.", | ||
WrongNumberLiteralName::Hexadecimal => { | ||
"Hexadecimal number literal is not allowed here." | ||
} | ||
WrongNumberLiteralName::Octal => "Octal number literal is not allowed here.", | ||
}; | ||
|
||
let diagnostic = RuleDiagnostic::new(rule_category!(), literal.range(), title.to_string()); | ||
|
||
Some(diagnostic) | ||
} | ||
|
||
fn action( | ||
ctx: &RuleContext<Self>, | ||
RuleState(reason, literal): &Self::State, | ||
) -> Option<JsRuleAction> { | ||
let mut mutation = ctx.root().begin(); | ||
let node = literal.node(); | ||
let token = node.value().ok()?; | ||
|
||
let message = match reason { | ||
WrongNumberLiteralName::Binary | ||
| WrongNumberLiteralName::Octal | ||
| WrongNumberLiteralName::Hexadecimal => { | ||
let text = literal.to_base_ten()?; | ||
mutation.replace_token(token, make::js_number_literal(text)); | ||
markup! ("Replace "{ node.to_string() } " with "{text.to_string()}).to_owned() | ||
} | ||
WrongNumberLiteralName::WithUnderscore | WrongNumberLiteralName::BigInt => { | ||
let text = literal.value(); | ||
mutation.replace_token(token, make::js_number_literal(text)); | ||
markup! ("Replace "{ node.to_string() } " with "{text}).to_owned() | ||
} | ||
}; | ||
|
||
Some(JsRuleAction { | ||
category: ActionCategory::QuickFix, | ||
applicability: Applicability::Always, | ||
message, | ||
mutation, | ||
}) | ||
} | ||
} |
8 changes: 8 additions & 0 deletions
8
crates/rome_js_analyze/tests/specs/nursery/useSimpleNumberKeys/invalid.js
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,8 @@ | ||
({ 1n: 1 }); | ||
({ 0x1: 1 }); | ||
({ 0o12: 1 }); | ||
({ 0b1: 1 }); | ||
({ 0o1: 1 }); | ||
({ 1_0: 1 }); | ||
({ 0.1e1_2: "ed" }); | ||
({ 11_1.11: "ee" }); |
Oops, something went wrong.