fix(linter/eslint/no-useless-concat): see through parenthesized expressions (#27984)

Part of #27982 (the `no-useless-concat` half; the `no-unassigned-vars`
half is in a separate PR).

`get_left` and `get_right` stopped walking at a
`ParenthesizedExpression`, so `'a' + ('b')`, `('a') + 'b'`, and the
outer `+` in `(foo + 'a') + ('b' + 'c')` were never compared as two
string literals. ESTree has no parenthesis nodes, so ESLint flags all of
these. Both helpers now call `without_parentheses()` before and while
following the `+` chain.

### AI disclosure

I used an AI coding assistant to help write this change and its tests. I
reviewed the diff and the snapshot output before opening the PR.

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Rahul Kashyap <itisrmk@users.noreply.github.com>
Co-authored-by: Cameron <cameron.clark@hey.com>
This commit is contained in:
authored and GitHub committed 2026-10-21 22:48:53 +00:00
1 parent 09bdb80a45
commit f5b625f242
2 files changed
+117 -4

No files matched your search

@@ -84,12 +84,12 @@ impl Rule for NoUselessConcat {
}
fn get_left<'a>(expr: &'a BinaryExpression<'a>) -> &'a Expression<'a> {
let mut left = &expr.left;
let mut left = expr.left.without_parentheses();
loop {
if let Expression::BinaryExpression(binary_expr) = left
&& binary_expr.operator == BinaryOperator::Addition
{
left = &binary_expr.right;
left = binary_expr.right.without_parentheses();
continue;
}
break;
@@ -98,12 +98,12 @@ fn get_left<'a>(expr: &'a BinaryExpression<'a>) -> &'a Expression<'a> {
}
fn get_right<'a>(expr: &'a BinaryExpression<'a>) -> &'a Expression<'a> {
let mut right = &expr.right;
let mut right = expr.right.without_parentheses();
loop {
if let Expression::BinaryExpression(binary_expr) = right
&& binary_expr.operator == BinaryOperator::Addition
{
right = &binary_expr.left;
right = binary_expr.left.without_parentheses();
continue;
}
break;
@@ -147,6 +147,7 @@ fn test() {
"foo['a'] + foo['b']",
"`${1 + 1}`",
"`${'1' + 1}`",
"'a' +\n('b')",
];
let fail = vec![
@@ -167,6 +168,15 @@ fn test() {
"'a' + 'b' + 'c' + 'd' + 'e' + foo",
"`${'a' + 'b'}` + 'c'",
"foo['a' + 'b']",
"var a = 'a' + ('b');\nvar b = ('a') + 'b';\nvar c = (foo + 'a') + ('b' + 'c');",
"'a' + ('b')",
"('a') + 'b'",
"('a') + ('b')",
"((('a'))) + 'b'",
"(foo + 'a') + 'b'",
"'a' + ('b' + 'c')",
"foo + ('a' + 'b')",
"'a' + ('b' + foo)",
];
// TODO: Implement a suggestion for this rule.
@@ -38,6 +38,13 @@ source: crates/oxc_linter/src/tester.rs
╰────
help: Rewrite into one string literal.
⚠ eslint(no-useless-concat): Unexpected string concatenation of literals.
╭─[no_useless_concat.tsx:1:8]
1 │ (foo + 'a') + ('b' + 'c')
· ───────────
╰────
help: Rewrite into one string literal.
⚠ eslint(no-useless-concat): Unexpected string concatenation of literals.
╭─[no_useless_concat.tsx:1:16]
1 │ (foo + 'a') + ('b' + 'c')
@@ -130,3 +137,99 @@ source: crates/oxc_linter/src/tester.rs
· ─────────
╰────
help: Rewrite into one string literal.
⚠ eslint(no-useless-concat): Unexpected string concatenation of literals.
╭─[no_useless_concat.tsx:1:9]
1 │ var a = 'a' + ('b');
· ──────────
2 │ var b = ('a') + 'b';
╰────
help: Rewrite into one string literal.
⚠ eslint(no-useless-concat): Unexpected string concatenation of literals.
╭─[no_useless_concat.tsx:2:10]
1 │ var a = 'a' + ('b');
2 │ var b = ('a') + 'b';
· ──────────
3 │ var c = (foo + 'a') + ('b' + 'c');
╰────
help: Rewrite into one string literal.
⚠ eslint(no-useless-concat): Unexpected string concatenation of literals.
╭─[no_useless_concat.tsx:3:16]
2 │ var b = ('a') + 'b';
3 │ var c = (foo + 'a') + ('b' + 'c');
· ───────────
╰────
help: Rewrite into one string literal.
⚠ eslint(no-useless-concat): Unexpected string concatenation of literals.
╭─[no_useless_concat.tsx:3:24]
2 │ var b = ('a') + 'b';
3 │ var c = (foo + 'a') + ('b' + 'c');
· ─────────
╰────
help: Rewrite into one string literal.
⚠ eslint(no-useless-concat): Unexpected string concatenation of literals.
╭─[no_useless_concat.tsx:1:1]
1 │ 'a' + ('b')
· ──────────
╰────
help: Rewrite into one string literal.
⚠ eslint(no-useless-concat): Unexpected string concatenation of literals.
╭─[no_useless_concat.tsx:1:2]
1 │ ('a') + 'b'
· ──────────
╰────
help: Rewrite into one string literal.
⚠ eslint(no-useless-concat): Unexpected string concatenation of literals.
╭─[no_useless_concat.tsx:1:2]
1 │ ('a') + ('b')
· ───────────
╰────
help: Rewrite into one string literal.
⚠ eslint(no-useless-concat): Unexpected string concatenation of literals.
╭─[no_useless_concat.tsx:1:4]
1 │ ((('a'))) + 'b'
· ────────────
╰────
help: Rewrite into one string literal.
⚠ eslint(no-useless-concat): Unexpected string concatenation of literals.
╭─[no_useless_concat.tsx:1:8]
1 │ (foo + 'a') + 'b'
· ──────────
╰────
help: Rewrite into one string literal.
⚠ eslint(no-useless-concat): Unexpected string concatenation of literals.
╭─[no_useless_concat.tsx:1:1]
1 │ 'a' + ('b' + 'c')
· ──────────
╰────
help: Rewrite into one string literal.
⚠ eslint(no-useless-concat): Unexpected string concatenation of literals.
╭─[no_useless_concat.tsx:1:8]
1 │ 'a' + ('b' + 'c')
· ─────────
╰────
help: Rewrite into one string literal.
⚠ eslint(no-useless-concat): Unexpected string concatenation of literals.
╭─[no_useless_concat.tsx:1:8]
1 │ foo + ('a' + 'b')
· ─────────
╰────
help: Rewrite into one string literal.
⚠ eslint(no-useless-concat): Unexpected string concatenation of literals.
╭─[no_useless_concat.tsx:1:1]
1 │ 'a' + ('b' + foo)
· ──────────
╰────
help: Rewrite into one string literal.