Skip to content

Commit 6cddbe7

Browse files
committed
fix(linter/prefer-at): wrap expressions in parentheses when needed for member access (#16643)
1 parent 678e43b commit 6cddbe7

File tree

3 files changed

+55
-31
lines changed

3 files changed

+55
-31
lines changed

crates/oxc_linter/src/rules/eslint/no_extra_boolean_cast.rs

Lines changed: 3 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
use oxc_ast::{
22
AstKind,
3-
ast::{CallExpression, Expression, NewExpression, match_member_expression},
3+
ast::{CallExpression, Expression, NewExpression},
44
};
55
use oxc_diagnostics::OxcDiagnostic;
66
use oxc_macros::declare_oxc_lint;
77
use oxc_span::{GetSpan, Span};
88
use oxc_syntax::{
99
operator::{LogicalOperator, UnaryOperator},
10-
precedence::{GetPrecedence, Precedence},
10+
precedence::Precedence,
1111
};
1212
use schemars::JsonSchema;
1313
use serde::Deserialize;
@@ -16,6 +16,7 @@ use crate::{
1616
AstNode,
1717
context::LintContext,
1818
rule::{DefaultRuleConfig, Rule},
19+
utils::get_precedence,
1920
};
2021

2122
fn no_extra_double_negation_cast_diagnostic(span: Span) -> OxcDiagnostic {
@@ -246,28 +247,6 @@ fn without_not<'a, 'b>(expr: &'b Expression<'a>) -> Option<&'b Expression<'a>> {
246247
}
247248
}
248249

249-
/// Returns the precedence of an expression if it has one.
250-
/// Returns `None` for "atomic" expressions (literals, identifiers, etc.) that have
251-
/// the highest precedence and never need parentheses.
252-
fn get_precedence(expr: &Expression) -> Option<Precedence> {
253-
match expr {
254-
Expression::SequenceExpression(e) => Some(e.precedence()),
255-
Expression::AssignmentExpression(e) => Some(e.precedence()),
256-
Expression::YieldExpression(e) => Some(e.precedence()),
257-
Expression::ConditionalExpression(e) => Some(e.precedence()),
258-
Expression::LogicalExpression(e) => Some(e.precedence()),
259-
Expression::BinaryExpression(e) => Some(e.precedence()),
260-
Expression::UnaryExpression(e) => Some(e.precedence()),
261-
Expression::UpdateExpression(e) => Some(e.precedence()),
262-
Expression::AwaitExpression(e) => Some(e.precedence()),
263-
Expression::NewExpression(e) => Some(e.precedence()),
264-
Expression::CallExpression(e) => Some(e.precedence()),
265-
match_member_expression!(Expression) => Some(expr.to_member_expression().precedence()),
266-
// Literals, identifiers, and other atomic expressions have highest precedence
267-
_ => None,
268-
}
269-
}
270-
271250
#[test]
272251
fn test() {
273252
use crate::tester::Tester;

crates/oxc_linter/src/rules/unicorn/prefer_at.rs

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
use oxc_syntax::precedence::Precedence;
2+
use schemars::JsonSchema;
3+
use serde::Deserialize;
4+
use serde_json::Value;
5+
16
use oxc_ast::{
27
AstKind,
38
ast::{
@@ -9,16 +14,13 @@ use oxc_ast::{
914
use oxc_diagnostics::OxcDiagnostic;
1015
use oxc_macros::declare_oxc_lint;
1116
use oxc_span::{GetSpan, Span};
12-
use schemars::JsonSchema;
13-
use serde::Deserialize;
14-
use serde_json::Value;
1517

1618
use crate::{
1719
AstNode,
1820
context::LintContext,
1921
fixer::{RuleFix, RuleFixer},
2022
rule::Rule,
21-
utils::is_same_expression,
23+
utils::{get_precedence, is_same_expression},
2224
};
2325

2426
fn prefer_at_diagnostic(span: Span, method: &str) -> OxcDiagnostic {
@@ -579,7 +581,14 @@ fn check_lodash_last<'a>(
579581
ctx.diagnostic_with_fix(
580582
prefer_at_diagnostic(call_expr.span, &format!("{name}.last()")),
581583
|fixer| {
582-
let new_code = format!("{}.at(-1)", fixer.source_range(arg.span()));
584+
let arg_text = fixer.source_range(arg.span());
585+
let new_code = if get_precedence(arg)
586+
.is_some_and(|precedence| precedence < Precedence::Member)
587+
{
588+
format!("({arg_text}).at(-1)")
589+
} else {
590+
format!("{arg_text}.at(-1)")
591+
};
583592
fixer.replace(call_expr.span, new_code)
584593
},
585594
);
@@ -756,6 +765,8 @@ fn test() {
756765
("lodash.last(array)", "array.at(-1)", None),
757766
// Edge cases with very large numbers
758767
("array[array.length - 9007199254740992]", "array.at(-9007199254740992)", None),
768+
("_.last([] as [])", "([] as []).at(-1)", None),
769+
("_.last([1, 2, 3] as const)", "([1, 2, 3] as const).at(-1)", None),
759770
];
760771

761772
Tester::new(PreferAt::NAME, PreferAt::PLUGIN, pass, fail).expect_fix(fix).test_and_snapshot();

crates/oxc_linter/src/utils/unicorn.rs

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@ use oxc_ast::{
22
AstKind,
33
ast::{
44
BindingPatternKind, CallExpression, Expression, FormalParameters, FunctionBody,
5-
LogicalExpression, MemberExpression, Statement,
5+
LogicalExpression, MemberExpression, Statement, match_member_expression,
66
},
77
};
88
use oxc_semantic::AstNode;
99
use oxc_span::{ContentEq, Span};
10-
use oxc_syntax::operator::LogicalOperator;
10+
use oxc_syntax::{
11+
operator::LogicalOperator,
12+
precedence::{GetPrecedence, Precedence},
13+
};
1114

1215
use crate::LintContext;
1316

@@ -375,3 +378,34 @@ where
375378

376379
false
377380
}
381+
382+
/// Returns the precedence of an expression if it has one.
383+
///
384+
/// Returns `None` for "atomic" expressions (literals, identifiers, etc.) that have
385+
/// the highest precedence and never need parentheses when used as operands.
386+
///
387+
/// This is useful for determining if parentheses are needed when transforming code.
388+
/// If `get_precedence(expr)` returns `None`, the expression never needs parentheses.
389+
/// If it returns `Some(p)`, compare `p` against the context's precedence to decide.
390+
pub fn get_precedence(expr: &Expression) -> Option<Precedence> {
391+
match expr {
392+
Expression::SequenceExpression(e) => Some(e.precedence()),
393+
Expression::AssignmentExpression(e) => Some(e.precedence()),
394+
Expression::YieldExpression(e) => Some(e.precedence()),
395+
Expression::ConditionalExpression(e) => Some(e.precedence()),
396+
Expression::LogicalExpression(e) => Some(e.precedence()),
397+
Expression::BinaryExpression(e) => Some(e.precedence()),
398+
Expression::UnaryExpression(e) => Some(e.precedence()),
399+
Expression::UpdateExpression(e) => Some(e.precedence()),
400+
Expression::AwaitExpression(e) => Some(e.precedence()),
401+
Expression::NewExpression(e) => Some(e.precedence()),
402+
Expression::CallExpression(e) => Some(e.precedence()),
403+
match_member_expression!(Expression) => Some(expr.to_member_expression().precedence()),
404+
Expression::TSAsExpression(_)
405+
| Expression::TSSatisfiesExpression(_)
406+
| Expression::TSTypeAssertion(_)
407+
| Expression::ArrowFunctionExpression(_) => Some(Precedence::Lowest),
408+
// Literals, identifiers, and other atomic expressions have highest precedence
409+
_ => None,
410+
}
411+
}

0 commit comments

Comments
 (0)