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):
useExponentiationOperator
rule (#3848)
Co-authored-by: Micha Reiser <[email protected]>
- Loading branch information
1 parent
2bfced0
commit 58a8e38
Showing
31 changed files
with
4,056 additions
and
3 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
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
316 changes: 316 additions & 0 deletions
316
crates/rome_js_analyze/src/analyzers/nursery/use_exponentiation_operator.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,316 @@ | ||
use crate::semantic_services::Semantic; | ||
use crate::JsRuleAction; | ||
use rome_analyze::context::RuleContext; | ||
use rome_analyze::{declare_rule, ActionCategory, Rule, RuleDiagnostic}; | ||
use rome_console::markup; | ||
use rome_diagnostics::Applicability; | ||
use rome_js_factory::{make, syntax::T}; | ||
use rome_js_syntax::{ | ||
AnyJsExpression, JsBinaryOperator, JsCallExpression, JsClassDeclaration, JsClassExpression, | ||
JsExtendsClause, JsInExpression, OperatorPrecedence, | ||
}; | ||
use rome_rowan::{AstNode, AstSeparatedList, BatchMutationExt}; | ||
|
||
declare_rule! { | ||
/// Disallow the use of `Math.pow` in favor of the `**` operator. | ||
/// | ||
/// > Introduced in ES2016, the infix exponentiation operator ** is an alternative for the standard Math.pow function. | ||
/// > Infix notation is considered to be more readable and thus more preferable than the function notation. | ||
/// | ||
/// Source: https://eslint.org/docs/latest/rules/prefer-exponentiation-operator | ||
/// | ||
/// ## Examples | ||
/// | ||
/// ### Invalid | ||
/// | ||
/// ```js,expect_diagnostic | ||
/// const foo = Math.pow(2, 8); | ||
/// ``` | ||
/// | ||
/// ```js,expect_diagnostic | ||
/// const bar = Math.pow(a, b); | ||
/// ``` | ||
/// | ||
/// ```js,expect_diagnostic | ||
/// let baz = Math.pow(a + b, c + d); | ||
/// ``` | ||
/// | ||
/// ```js,expect_diagnostic | ||
/// let quux = Math.pow(-1, n); | ||
/// ``` | ||
/// | ||
/// ### Valid | ||
/// | ||
/// ```js | ||
/// const foo = 2 ** 8; | ||
/// | ||
/// const bar = a ** b; | ||
/// | ||
/// let baz = (a + b) ** (c + d); | ||
/// | ||
/// let quux = (-1) ** n; | ||
/// ``` | ||
/// | ||
pub(crate) UseExponentiationOperator { | ||
version: "11.0.0", | ||
name: "useExponentiationOperator", | ||
recommended: false, | ||
} | ||
} | ||
|
||
pub struct MathPowCall { | ||
base: AnyJsExpression, | ||
exponent: AnyJsExpression, | ||
} | ||
|
||
impl MathPowCall { | ||
fn make_base(&self) -> Option<AnyJsExpression> { | ||
Some(if self.does_base_need_parens()? { | ||
parenthesize_any_js_expression(&self.base) | ||
} else { | ||
self.base.clone() | ||
}) | ||
} | ||
|
||
fn make_exponent(&self) -> Option<AnyJsExpression> { | ||
Some(if self.does_exponent_need_parens()? { | ||
parenthesize_any_js_expression(&self.exponent) | ||
} else { | ||
self.exponent.clone() | ||
}) | ||
} | ||
|
||
/// Determines whether the base expression needs parens in an exponentiation binary expression. | ||
fn does_base_need_parens(&self) -> Option<bool> { | ||
Some( | ||
// '**' is right-associative, parens are needed when Math.pow(a ** b, c) is converted to (a ** b) ** c | ||
self.base.precedence().ok()? <= OperatorPrecedence::Exponential | ||
// An unary operator cannot be used immediately before an exponentiation expression | ||
|| self.base.as_js_unary_expression().is_some() | ||
|| self.base.as_js_await_expression().is_some(), | ||
) | ||
} | ||
|
||
/// Determines whether the exponent expression needs parens in an exponentiation binary expression. | ||
fn does_exponent_need_parens(&self) -> Option<bool> { | ||
Some(self.exponent.precedence().ok()? < OperatorPrecedence::Exponential) | ||
} | ||
} | ||
|
||
impl Rule for UseExponentiationOperator { | ||
type Query = Semantic<JsCallExpression>; | ||
type State = (); | ||
type Signals = Option<Self::State>; | ||
type Options = (); | ||
|
||
fn run(ctx: &RuleContext<Self>) -> Self::Signals { | ||
let node = ctx.query(); | ||
let model = ctx.model(); | ||
|
||
let object = match node.callee().ok()?.omit_parentheses() { | ||
AnyJsExpression::JsStaticMemberExpression(static_member_expr) => { | ||
if static_member_expr | ||
.member() | ||
.ok()? | ||
.as_js_name()? | ||
.value_token() | ||
.ok()? | ||
.token_text_trimmed() | ||
!= "pow" | ||
{ | ||
return None; | ||
} | ||
|
||
static_member_expr.object() | ||
} | ||
AnyJsExpression::JsComputedMemberExpression(computed_member_expr) => { | ||
if !computed_member_expr | ||
.member() | ||
.ok()? | ||
.is_string_constant("pow") | ||
{ | ||
return None; | ||
} | ||
|
||
computed_member_expr.object() | ||
} | ||
_ => return None, | ||
}; | ||
|
||
let reference = object.ok()?.omit_parentheses().as_reference_identifier()?; | ||
|
||
// verifies that the Math reference is not a local variable | ||
let has_math_pow = reference.has_name("Math") && model.binding(&reference).is_none(); | ||
has_math_pow.then_some(()) | ||
} | ||
|
||
fn diagnostic(ctx: &RuleContext<Self>, _: &Self::State) -> Option<RuleDiagnostic> { | ||
let diagnostic = RuleDiagnostic::new( | ||
rule_category!(), | ||
ctx.query().range(), | ||
"Use the '**' operator instead of 'Math.pow'.", | ||
); | ||
|
||
Some(diagnostic) | ||
} | ||
|
||
fn action(ctx: &RuleContext<Self>, _: &Self::State) -> Option<JsRuleAction> { | ||
let node = ctx.query(); | ||
|
||
if !should_suggest_fix(node)? { | ||
return None; | ||
} | ||
|
||
let mut mutation = ctx.root().begin(); | ||
let [base, exponent] = node.get_arguments_by_index([0, 1]); | ||
|
||
let math_pow_call = MathPowCall { | ||
base: base?.as_any_js_expression()?.clone().omit_parentheses(), | ||
exponent: exponent?.as_any_js_expression()?.clone().omit_parentheses(), | ||
}; | ||
|
||
let new_node = make::js_binary_expression( | ||
math_pow_call.make_base()?, | ||
make::token(T![**]), | ||
math_pow_call.make_exponent()?, | ||
); | ||
|
||
if let Some((needs_parens, parent)) = does_exponentiation_expression_need_parens(node) { | ||
if needs_parens && parent.is_some() { | ||
mutation.replace_node(parent.clone()?, parenthesize_any_js_expression(&parent?)); | ||
} | ||
|
||
mutation.replace_node( | ||
AnyJsExpression::from(node.clone()), | ||
parenthesize_any_js_expression(&AnyJsExpression::from(new_node)), | ||
); | ||
} else { | ||
mutation.replace_node( | ||
AnyJsExpression::from(node.clone()), | ||
AnyJsExpression::from(new_node), | ||
); | ||
} | ||
|
||
Some(JsRuleAction { | ||
category: ActionCategory::QuickFix, | ||
applicability: Applicability::MaybeIncorrect, | ||
message: markup! { "Use the '**' operator instead of 'Math.pow'." }.to_owned(), | ||
mutation, | ||
}) | ||
} | ||
} | ||
|
||
/// Verify if the autofix is safe to be applied and won't remove comments. | ||
/// Argument list is considered valid if there's no spread arg and leading/trailing comments. | ||
fn should_suggest_fix(node: &JsCallExpression) -> Option<bool> { | ||
let arguments = node.arguments().ok()?; | ||
let args_count = arguments.args().len(); | ||
|
||
Some( | ||
args_count == 2 | ||
&& !arguments.l_paren_token().ok()?.has_leading_comments() | ||
&& !arguments.l_paren_token().ok()?.has_trailing_comments() | ||
&& !arguments.r_paren_token().ok()?.has_leading_comments() | ||
&& !arguments.r_paren_token().ok()?.has_trailing_comments() | ||
&& arguments.args().into_iter().flatten().all(|arg| { | ||
!arg.syntax().has_leading_comments() | ||
&& !arg.syntax().has_trailing_comments() | ||
&& arg.as_js_spread().is_none() | ||
}), | ||
) | ||
} | ||
|
||
/// Wraps a [AnyJsExpression] in paretheses | ||
fn parenthesize_any_js_expression(expr: &AnyJsExpression) -> AnyJsExpression { | ||
AnyJsExpression::from(make::js_parenthesized_expression( | ||
make::token(T!['(']), | ||
expr.clone(), | ||
make::token(T![')']), | ||
)) | ||
} | ||
|
||
/// Determines whether the given parent node needs parens if used as the exponent in an exponentiation binary expression. | ||
fn does_exponentiation_expression_need_parens( | ||
node: &JsCallExpression, | ||
) -> Option<(bool, Option<AnyJsExpression>)> { | ||
if let Some(parent) = node.parent::<AnyJsExpression>() { | ||
if does_expression_need_parens(node, &parent)? { | ||
return Some((true, Some(parent))); | ||
} | ||
} else if let Some(extends_clause) = node.parent::<JsExtendsClause>() { | ||
if extends_clause.parent::<JsClassDeclaration>().is_some() { | ||
return Some((true, None)); | ||
} | ||
|
||
if let Some(class_expr) = extends_clause.parent::<JsClassExpression>() { | ||
let class_expr = AnyJsExpression::from(class_expr); | ||
if does_expression_need_parens(node, &class_expr)? { | ||
return Some((true, Some(class_expr))); | ||
} | ||
} | ||
} | ||
|
||
None | ||
} | ||
|
||
/// Determines whether the given expression needs parens when used in an exponentiation binary expression. | ||
fn does_expression_need_parens( | ||
node: &JsCallExpression, | ||
expression: &AnyJsExpression, | ||
) -> Option<bool> { | ||
let needs_parentheses = match &expression { | ||
// Skips already parenthesized expressions | ||
AnyJsExpression::JsParenthesizedExpression(_) => return None, | ||
AnyJsExpression::JsBinaryExpression(bin_expr) => { | ||
if bin_expr.parent::<JsInExpression>().is_some() { | ||
return Some(true); | ||
} | ||
|
||
let binding = bin_expr.right().ok()?; | ||
let call_expr = binding.as_js_call_expression(); | ||
|
||
bin_expr.operator().ok()? != JsBinaryOperator::Exponent | ||
|| call_expr.is_none() | ||
|| call_expr? != node | ||
} | ||
AnyJsExpression::JsCallExpression(call_expr) => !call_expr | ||
.arguments() | ||
.ok()? | ||
.args() | ||
.iter() | ||
.filter_map(|arg| { | ||
let binding = arg.ok()?; | ||
return binding | ||
.as_any_js_expression()? | ||
.as_js_call_expression() | ||
.cloned(); | ||
}) | ||
.any(|arg| &arg == node), | ||
AnyJsExpression::JsNewExpression(new_expr) => !new_expr | ||
.arguments()? | ||
.args() | ||
.iter() | ||
.filter_map(|arg| { | ||
let binding = arg.ok()?; | ||
return binding | ||
.as_any_js_expression()? | ||
.as_js_call_expression() | ||
.cloned(); | ||
}) | ||
.any(|arg| &arg == node), | ||
AnyJsExpression::JsComputedMemberExpression(member_expr) => { | ||
let binding = member_expr.member().ok()?; | ||
let call_expr = binding.as_js_call_expression(); | ||
|
||
call_expr.is_none() || call_expr? != node | ||
} | ||
AnyJsExpression::JsInExpression(_) => return Some(true), | ||
AnyJsExpression::JsClassExpression(_) | ||
| AnyJsExpression::JsStaticMemberExpression(_) | ||
| AnyJsExpression::JsUnaryExpression(_) | ||
| AnyJsExpression::JsTemplateExpression(_) => true, | ||
_ => false, | ||
}; | ||
|
||
Some(needs_parentheses && expression.precedence().ok()? >= OperatorPrecedence::Exponential) | ||
} |
60 changes: 60 additions & 0 deletions
60
crates/rome_js_analyze/tests/specs/nursery/useExponentiationOperator/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,60 @@ | ||
Math.pow(a, b); | ||
(Math).pow(a, b); | ||
|
||
// able to catch some workarounds | ||
Math[`pow`](a, b); | ||
(Math)['pow'](a, b); | ||
(Math)["pow"](a, b); | ||
(Math)[`pow`](a, b); | ||
|
||
// non-expression parents that don't require parens | ||
var x = Math.pow(a, b); | ||
if(Math.pow(a, b)){} | ||
for(;Math.pow(a, b);){} | ||
switch(foo){ case Math.pow(a, b): break; } | ||
{ foo: Math.pow(a, b) } | ||
function foo(bar, baz = Math.pow(a, b), quux){} | ||
`${Math.pow(a, b)}` | ||
|
||
// non-expression parents that do require parens | ||
class C extends Math.pow(a, b) {} | ||
|
||
// already parenthesised, shouldn't insert extra parens | ||
+(Math.pow(a, b)) | ||
(Math.pow(a, b)).toString() | ||
(class extends (Math.pow(a, b)) {}) | ||
class C extends (Math.pow(a, b)) {} | ||
|
||
// '**' is right-associative, that applies to both parent and child nodes | ||
a ** Math.pow(b, c); | ||
Math.pow(a, b) ** c; | ||
Math.pow(a, b ** c); | ||
Math.pow(a ** b, c); | ||
a ** Math.pow(b ** c, d ** e) ** f; | ||
|
||
// doesn't remove already existing unnecessary parens around the whole expression | ||
(Math.pow(a, b)); | ||
foo + (Math.pow(a, b)); | ||
(Math.pow(a, b)) + foo; | ||
`${(Math.pow(a, b))}`; | ||
|
||
// doesn't preserve unnecessary parens around base and exponent | ||
Math.pow((a), (b)) | ||
Math.pow(((a)), ((b))) | ||
Math.pow((a.foo), b) | ||
Math.pow(a, (b.foo)) | ||
Math.pow((a()), b) | ||
Math.pow(a, (b())) | ||
|
||
// Optional chaining | ||
Math.pow?.(a, b) | ||
Math?.pow(a, b) | ||
Math?.pow?.(a, b) | ||
;(Math?.pow)(a, b) | ||
;(Math?.pow)?.(a, b) | ||
|
||
// doesn't put extra parens | ||
Math.pow((a + b), (c + d)) | ||
|
||
// tokens that can be adjacent | ||
a+Math.pow(b, c)+d |
Oops, something went wrong.