Skip to content
This repository was archived by the owner on Aug 31, 2023. It is now read-only.

Commit 58a8e38

Browse files
feat(rome_js_analyze): useExponentiationOperator rule (#3848)
Co-authored-by: Micha Reiser <[email protected]>
1 parent 2bfced0 commit 58a8e38

31 files changed

+4056
-3
lines changed

crates/rome_diagnostics_categories/src/categories.rs

+1
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ define_dategories! {
100100
"lint/nursery/useDefaultSwitchClauseLast":"https://docs.rome.tools/lint/rules/useDefaultSwitchClauseLast",
101101
"lint/nursery/useEnumInitializers":"https://docs.rome.tools/lint/rules/useEnumInitializers",
102102
"lint/nursery/useExhaustiveDependencies": "https://docs.rome.tools/lint/rules/useExhaustiveDependencies",
103+
"lint/nursery/useExponentiationOperator": "https://docs.rome.tools/lint/rules/useExponentiationOperator",
103104
"lint/nursery/useFlatMap": "https://docs.rome.tools/lint/rules/useFlatMap",
104105
"lint/nursery/useNumericLiterals": "https://docs.rome.tools/lint/rules/useNumericLiterals",
105106
"lint/nursery/useValidForDirection": "https://docs.rome.tools/lint/rules/useValidForDirection",

crates/rome_js_analyze/src/analyzers/nursery.rs

+2-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
1+
use crate::semantic_services::Semantic;
2+
use crate::JsRuleAction;
3+
use rome_analyze::context::RuleContext;
4+
use rome_analyze::{declare_rule, ActionCategory, Rule, RuleDiagnostic};
5+
use rome_console::markup;
6+
use rome_diagnostics::Applicability;
7+
use rome_js_factory::{make, syntax::T};
8+
use rome_js_syntax::{
9+
AnyJsExpression, JsBinaryOperator, JsCallExpression, JsClassDeclaration, JsClassExpression,
10+
JsExtendsClause, JsInExpression, OperatorPrecedence,
11+
};
12+
use rome_rowan::{AstNode, AstSeparatedList, BatchMutationExt};
13+
14+
declare_rule! {
15+
/// Disallow the use of `Math.pow` in favor of the `**` operator.
16+
///
17+
/// > Introduced in ES2016, the infix exponentiation operator ** is an alternative for the standard Math.pow function.
18+
/// > Infix notation is considered to be more readable and thus more preferable than the function notation.
19+
///
20+
/// Source: https://eslint.org/docs/latest/rules/prefer-exponentiation-operator
21+
///
22+
/// ## Examples
23+
///
24+
/// ### Invalid
25+
///
26+
/// ```js,expect_diagnostic
27+
/// const foo = Math.pow(2, 8);
28+
/// ```
29+
///
30+
/// ```js,expect_diagnostic
31+
/// const bar = Math.pow(a, b);
32+
/// ```
33+
///
34+
/// ```js,expect_diagnostic
35+
/// let baz = Math.pow(a + b, c + d);
36+
/// ```
37+
///
38+
/// ```js,expect_diagnostic
39+
/// let quux = Math.pow(-1, n);
40+
/// ```
41+
///
42+
/// ### Valid
43+
///
44+
/// ```js
45+
/// const foo = 2 ** 8;
46+
///
47+
/// const bar = a ** b;
48+
///
49+
/// let baz = (a + b) ** (c + d);
50+
///
51+
/// let quux = (-1) ** n;
52+
/// ```
53+
///
54+
pub(crate) UseExponentiationOperator {
55+
version: "11.0.0",
56+
name: "useExponentiationOperator",
57+
recommended: false,
58+
}
59+
}
60+
61+
pub struct MathPowCall {
62+
base: AnyJsExpression,
63+
exponent: AnyJsExpression,
64+
}
65+
66+
impl MathPowCall {
67+
fn make_base(&self) -> Option<AnyJsExpression> {
68+
Some(if self.does_base_need_parens()? {
69+
parenthesize_any_js_expression(&self.base)
70+
} else {
71+
self.base.clone()
72+
})
73+
}
74+
75+
fn make_exponent(&self) -> Option<AnyJsExpression> {
76+
Some(if self.does_exponent_need_parens()? {
77+
parenthesize_any_js_expression(&self.exponent)
78+
} else {
79+
self.exponent.clone()
80+
})
81+
}
82+
83+
/// Determines whether the base expression needs parens in an exponentiation binary expression.
84+
fn does_base_need_parens(&self) -> Option<bool> {
85+
Some(
86+
// '**' is right-associative, parens are needed when Math.pow(a ** b, c) is converted to (a ** b) ** c
87+
self.base.precedence().ok()? <= OperatorPrecedence::Exponential
88+
// An unary operator cannot be used immediately before an exponentiation expression
89+
|| self.base.as_js_unary_expression().is_some()
90+
|| self.base.as_js_await_expression().is_some(),
91+
)
92+
}
93+
94+
/// Determines whether the exponent expression needs parens in an exponentiation binary expression.
95+
fn does_exponent_need_parens(&self) -> Option<bool> {
96+
Some(self.exponent.precedence().ok()? < OperatorPrecedence::Exponential)
97+
}
98+
}
99+
100+
impl Rule for UseExponentiationOperator {
101+
type Query = Semantic<JsCallExpression>;
102+
type State = ();
103+
type Signals = Option<Self::State>;
104+
type Options = ();
105+
106+
fn run(ctx: &RuleContext<Self>) -> Self::Signals {
107+
let node = ctx.query();
108+
let model = ctx.model();
109+
110+
let object = match node.callee().ok()?.omit_parentheses() {
111+
AnyJsExpression::JsStaticMemberExpression(static_member_expr) => {
112+
if static_member_expr
113+
.member()
114+
.ok()?
115+
.as_js_name()?
116+
.value_token()
117+
.ok()?
118+
.token_text_trimmed()
119+
!= "pow"
120+
{
121+
return None;
122+
}
123+
124+
static_member_expr.object()
125+
}
126+
AnyJsExpression::JsComputedMemberExpression(computed_member_expr) => {
127+
if !computed_member_expr
128+
.member()
129+
.ok()?
130+
.is_string_constant("pow")
131+
{
132+
return None;
133+
}
134+
135+
computed_member_expr.object()
136+
}
137+
_ => return None,
138+
};
139+
140+
let reference = object.ok()?.omit_parentheses().as_reference_identifier()?;
141+
142+
// verifies that the Math reference is not a local variable
143+
let has_math_pow = reference.has_name("Math") && model.binding(&reference).is_none();
144+
has_math_pow.then_some(())
145+
}
146+
147+
fn diagnostic(ctx: &RuleContext<Self>, _: &Self::State) -> Option<RuleDiagnostic> {
148+
let diagnostic = RuleDiagnostic::new(
149+
rule_category!(),
150+
ctx.query().range(),
151+
"Use the '**' operator instead of 'Math.pow'.",
152+
);
153+
154+
Some(diagnostic)
155+
}
156+
157+
fn action(ctx: &RuleContext<Self>, _: &Self::State) -> Option<JsRuleAction> {
158+
let node = ctx.query();
159+
160+
if !should_suggest_fix(node)? {
161+
return None;
162+
}
163+
164+
let mut mutation = ctx.root().begin();
165+
let [base, exponent] = node.get_arguments_by_index([0, 1]);
166+
167+
let math_pow_call = MathPowCall {
168+
base: base?.as_any_js_expression()?.clone().omit_parentheses(),
169+
exponent: exponent?.as_any_js_expression()?.clone().omit_parentheses(),
170+
};
171+
172+
let new_node = make::js_binary_expression(
173+
math_pow_call.make_base()?,
174+
make::token(T![**]),
175+
math_pow_call.make_exponent()?,
176+
);
177+
178+
if let Some((needs_parens, parent)) = does_exponentiation_expression_need_parens(node) {
179+
if needs_parens && parent.is_some() {
180+
mutation.replace_node(parent.clone()?, parenthesize_any_js_expression(&parent?));
181+
}
182+
183+
mutation.replace_node(
184+
AnyJsExpression::from(node.clone()),
185+
parenthesize_any_js_expression(&AnyJsExpression::from(new_node)),
186+
);
187+
} else {
188+
mutation.replace_node(
189+
AnyJsExpression::from(node.clone()),
190+
AnyJsExpression::from(new_node),
191+
);
192+
}
193+
194+
Some(JsRuleAction {
195+
category: ActionCategory::QuickFix,
196+
applicability: Applicability::MaybeIncorrect,
197+
message: markup! { "Use the '**' operator instead of 'Math.pow'." }.to_owned(),
198+
mutation,
199+
})
200+
}
201+
}
202+
203+
/// Verify if the autofix is safe to be applied and won't remove comments.
204+
/// Argument list is considered valid if there's no spread arg and leading/trailing comments.
205+
fn should_suggest_fix(node: &JsCallExpression) -> Option<bool> {
206+
let arguments = node.arguments().ok()?;
207+
let args_count = arguments.args().len();
208+
209+
Some(
210+
args_count == 2
211+
&& !arguments.l_paren_token().ok()?.has_leading_comments()
212+
&& !arguments.l_paren_token().ok()?.has_trailing_comments()
213+
&& !arguments.r_paren_token().ok()?.has_leading_comments()
214+
&& !arguments.r_paren_token().ok()?.has_trailing_comments()
215+
&& arguments.args().into_iter().flatten().all(|arg| {
216+
!arg.syntax().has_leading_comments()
217+
&& !arg.syntax().has_trailing_comments()
218+
&& arg.as_js_spread().is_none()
219+
}),
220+
)
221+
}
222+
223+
/// Wraps a [AnyJsExpression] in paretheses
224+
fn parenthesize_any_js_expression(expr: &AnyJsExpression) -> AnyJsExpression {
225+
AnyJsExpression::from(make::js_parenthesized_expression(
226+
make::token(T!['(']),
227+
expr.clone(),
228+
make::token(T![')']),
229+
))
230+
}
231+
232+
/// Determines whether the given parent node needs parens if used as the exponent in an exponentiation binary expression.
233+
fn does_exponentiation_expression_need_parens(
234+
node: &JsCallExpression,
235+
) -> Option<(bool, Option<AnyJsExpression>)> {
236+
if let Some(parent) = node.parent::<AnyJsExpression>() {
237+
if does_expression_need_parens(node, &parent)? {
238+
return Some((true, Some(parent)));
239+
}
240+
} else if let Some(extends_clause) = node.parent::<JsExtendsClause>() {
241+
if extends_clause.parent::<JsClassDeclaration>().is_some() {
242+
return Some((true, None));
243+
}
244+
245+
if let Some(class_expr) = extends_clause.parent::<JsClassExpression>() {
246+
let class_expr = AnyJsExpression::from(class_expr);
247+
if does_expression_need_parens(node, &class_expr)? {
248+
return Some((true, Some(class_expr)));
249+
}
250+
}
251+
}
252+
253+
None
254+
}
255+
256+
/// Determines whether the given expression needs parens when used in an exponentiation binary expression.
257+
fn does_expression_need_parens(
258+
node: &JsCallExpression,
259+
expression: &AnyJsExpression,
260+
) -> Option<bool> {
261+
let needs_parentheses = match &expression {
262+
// Skips already parenthesized expressions
263+
AnyJsExpression::JsParenthesizedExpression(_) => return None,
264+
AnyJsExpression::JsBinaryExpression(bin_expr) => {
265+
if bin_expr.parent::<JsInExpression>().is_some() {
266+
return Some(true);
267+
}
268+
269+
let binding = bin_expr.right().ok()?;
270+
let call_expr = binding.as_js_call_expression();
271+
272+
bin_expr.operator().ok()? != JsBinaryOperator::Exponent
273+
|| call_expr.is_none()
274+
|| call_expr? != node
275+
}
276+
AnyJsExpression::JsCallExpression(call_expr) => !call_expr
277+
.arguments()
278+
.ok()?
279+
.args()
280+
.iter()
281+
.filter_map(|arg| {
282+
let binding = arg.ok()?;
283+
return binding
284+
.as_any_js_expression()?
285+
.as_js_call_expression()
286+
.cloned();
287+
})
288+
.any(|arg| &arg == node),
289+
AnyJsExpression::JsNewExpression(new_expr) => !new_expr
290+
.arguments()?
291+
.args()
292+
.iter()
293+
.filter_map(|arg| {
294+
let binding = arg.ok()?;
295+
return binding
296+
.as_any_js_expression()?
297+
.as_js_call_expression()
298+
.cloned();
299+
})
300+
.any(|arg| &arg == node),
301+
AnyJsExpression::JsComputedMemberExpression(member_expr) => {
302+
let binding = member_expr.member().ok()?;
303+
let call_expr = binding.as_js_call_expression();
304+
305+
call_expr.is_none() || call_expr? != node
306+
}
307+
AnyJsExpression::JsInExpression(_) => return Some(true),
308+
AnyJsExpression::JsClassExpression(_)
309+
| AnyJsExpression::JsStaticMemberExpression(_)
310+
| AnyJsExpression::JsUnaryExpression(_)
311+
| AnyJsExpression::JsTemplateExpression(_) => true,
312+
_ => false,
313+
};
314+
315+
Some(needs_parentheses && expression.precedence().ok()? >= OperatorPrecedence::Exponential)
316+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
Math.pow(a, b);
2+
(Math).pow(a, b);
3+
4+
// able to catch some workarounds
5+
Math[`pow`](a, b);
6+
(Math)['pow'](a, b);
7+
(Math)["pow"](a, b);
8+
(Math)[`pow`](a, b);
9+
10+
// non-expression parents that don't require parens
11+
var x = Math.pow(a, b);
12+
if(Math.pow(a, b)){}
13+
for(;Math.pow(a, b);){}
14+
switch(foo){ case Math.pow(a, b): break; }
15+
{ foo: Math.pow(a, b) }
16+
function foo(bar, baz = Math.pow(a, b), quux){}
17+
`${Math.pow(a, b)}`
18+
19+
// non-expression parents that do require parens
20+
class C extends Math.pow(a, b) {}
21+
22+
// already parenthesised, shouldn't insert extra parens
23+
+(Math.pow(a, b))
24+
(Math.pow(a, b)).toString()
25+
(class extends (Math.pow(a, b)) {})
26+
class C extends (Math.pow(a, b)) {}
27+
28+
// '**' is right-associative, that applies to both parent and child nodes
29+
a ** Math.pow(b, c);
30+
Math.pow(a, b) ** c;
31+
Math.pow(a, b ** c);
32+
Math.pow(a ** b, c);
33+
a ** Math.pow(b ** c, d ** e) ** f;
34+
35+
// doesn't remove already existing unnecessary parens around the whole expression
36+
(Math.pow(a, b));
37+
foo + (Math.pow(a, b));
38+
(Math.pow(a, b)) + foo;
39+
`${(Math.pow(a, b))}`;
40+
41+
// doesn't preserve unnecessary parens around base and exponent
42+
Math.pow((a), (b))
43+
Math.pow(((a)), ((b)))
44+
Math.pow((a.foo), b)
45+
Math.pow(a, (b.foo))
46+
Math.pow((a()), b)
47+
Math.pow(a, (b()))
48+
49+
// Optional chaining
50+
Math.pow?.(a, b)
51+
Math?.pow(a, b)
52+
Math?.pow?.(a, b)
53+
;(Math?.pow)(a, b)
54+
;(Math?.pow)?.(a, b)
55+
56+
// doesn't put extra parens
57+
Math.pow((a + b), (c + d))
58+
59+
// tokens that can be adjacent
60+
a+Math.pow(b, c)+d

0 commit comments

Comments
 (0)