feat(linter/eslint): Implement no-useless-concat (#3363)

Rule Detail:
[link](https://eslint.org/docs/latest/rules/no-useless-concat)

---

I haven't implemented one condition with the multiline string test case
and honestly I don't really understand why it is allowed? And also not
sure how I would implement that in oxlint.

Another issue is that maybe the output isn't great, the underlining
matches the whole BinaryExpression, for example:
```
+   ╭─[no_useless_concat.tsx:1:1]
+ 1 │ foo + `a` + `b`
+   · ───────────────
```

So maybe instead the diagnostic should get two spans passed, for each
Expression::StringLiteral or Expression::TemplateLiteral, that would
also allow the help text to show that it can be written as `"ab"`.

But an automatic fixxer would be more helpful I reckon :)

---------

Co-authored-by: Boshen <boshenc@gmail.com>
This commit is contained in:
Jelle van der Waa 2024-05-25 19:52:33 +02:00 committed by GitHub
parent 7d73fb33e8
commit 147864cfeb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 248 additions and 0 deletions

View file

@ -99,6 +99,7 @@ mod eslint {
pub mod no_unused_labels;
pub mod no_unused_private_class_members;
pub mod no_useless_catch;
pub mod no_useless_concat;
pub mod no_useless_escape;
pub mod no_useless_rename;
pub mod no_var;
@ -468,6 +469,7 @@ oxc_macros::declare_all_lint_rules! {
eslint::no_useless_catch,
eslint::no_useless_escape,
eslint::no_useless_rename,
eslint::no_useless_concat,
eslint::no_var,
eslint::no_void,
eslint::no_with,

View file

@ -0,0 +1,135 @@
use oxc_ast::ast::{BinaryExpression, Expression};
use oxc_ast::AstKind;
use oxc_diagnostics::OxcDiagnostic;
use oxc_macros::declare_oxc_lint;
use oxc_span::{GetSpan, Span};
use oxc_syntax::{identifier::is_line_terminator, operator::BinaryOperator};
use crate::{context::LintContext, rule::Rule, AstNode};
#[derive(Debug, Default, Clone)]
pub struct NoUselessConcat;
fn no_useless_concat_diagnostic(span0: Span) -> OxcDiagnostic {
OxcDiagnostic::warn("eslint(no-useless-concat): Unexpected string concatenation of literals.")
.with_help("Rewrite into one string literal")
.with_labels([span0.into()])
}
declare_oxc_lint!(
/// ### What it does
///
/// Disallow unnecessary concatenation of literals or template literals
///
/// ### Why is this bad?
///
/// Its unnecessary to concatenate two strings together.
///
/// ### Example
/// ```javascript
/// var foo = "a" + "b";
/// ```
NoUselessConcat,
suspicious
);
impl Rule for NoUselessConcat {
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
let AstKind::BinaryExpression(binary_expr) = node.kind() else {
return;
};
if binary_expr.operator != BinaryOperator::Addition {
return;
}
let left = get_left(binary_expr);
let right = get_right(binary_expr);
if left.is_string_literal() && right.is_string_literal() {
let left_span = left.span();
let right_span = right.span();
let span = Span::new(left_span.end, right_span.start);
let source_text = span.source_text(ctx.source_text());
if source_text.chars().any(is_line_terminator) {
return;
}
let span = Span::new(left_span.start, right_span.end);
ctx.diagnostic(no_useless_concat_diagnostic(span));
}
}
}
fn get_left<'a>(expr: &'a BinaryExpression<'a>) -> &'a Expression<'a> {
let mut left = &expr.left;
loop {
if let Expression::BinaryExpression(binary_expr) = left {
if binary_expr.operator == BinaryOperator::Addition {
left = &binary_expr.right;
continue;
}
}
break;
}
left
}
fn get_right<'a>(expr: &'a BinaryExpression<'a>) -> &'a Expression<'a> {
let mut right = &expr.right;
loop {
if let Expression::BinaryExpression(binary_expr) = right {
if binary_expr.operator == BinaryOperator::Addition {
right = &binary_expr.left;
continue;
}
}
break;
}
right
}
#[test]
fn test() {
use crate::tester::Tester;
let pass = vec![
"var a = 1 + 1;",
"var a = 1 * '2';",
"var a = 1 - 2;",
"var a = foo + bar;",
"var a = 'foo' + bar;",
"var foo = 'foo' +
'bar';",
"var string = (number + 1) + 'px';",
"'a' + 1",
"1 + '1'",
"1 + `1`",
"`1` + 1",
"(1 + +2) + `b`",
"
'a'
+ 'b'
+ 'c'
",
];
let fail = vec![
"'a' + 'b'",
"'a' +
'b' + 'c'",
"foo + 'a' + 'b'",
"'a' + 'b' + 'c'",
"(foo + 'a') + ('b' + 'c')",
"`a` + 'b'",
"`a` + `b`",
"foo + `a` + `b`",
"foo + 'a' + 'b'",
"'a' +
'b' + 'c'
+ 'd'
",
"'a' + 'b' + 'c' + 'd' + 'e' + foo",
];
Tester::new(NoUselessConcat::NAME, pass, fail).test_and_snapshot();
}

View file

@ -0,0 +1,111 @@
---
source: crates/oxc_linter/src/tester.rs
expression: no_useless_concat
---
⚠ 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:2:9]
1 │ 'a' +
2 │ 'b' + 'c'
· ─────────
╰────
help: Rewrite into one string literal
⚠ eslint(no-useless-concat): Unexpected string concatenation of literals.
╭─[no_useless_concat.tsx:1:7]
1 │ foo + 'a' + 'b'
· ─────────
╰────
help: Rewrite into one string literal
⚠ eslint(no-useless-concat): Unexpected string concatenation of literals.
╭─[no_useless_concat.tsx:1:7]
1 │ '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' + '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')
· ─────────
╰────
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:1]
1 │ `a` + `b`
· ─────────
╰────
help: Rewrite into one string literal
⚠ eslint(no-useless-concat): Unexpected string concatenation of literals.
╭─[no_useless_concat.tsx:1:7]
1 │ foo + `a` + `b`
· ─────────
╰────
help: Rewrite into one string literal
⚠ eslint(no-useless-concat): Unexpected string concatenation of literals.
╭─[no_useless_concat.tsx:1:7]
1 │ foo + 'a' + 'b'
· ─────────
╰────
help: Rewrite into one string literal
⚠ eslint(no-useless-concat): Unexpected string concatenation of literals.
╭─[no_useless_concat.tsx:2:9]
1 │ 'a' +
2 │ 'b' + 'c'
· ─────────
3 │ + 'd'
╰────
help: Rewrite into one string literal
⚠ eslint(no-useless-concat): Unexpected string concatenation of literals.
╭─[no_useless_concat.tsx:1:19]
1 │ 'a' + 'b' + 'c' + 'd' + 'e' + foo
· ─────────
╰────
help: Rewrite into one string literal
⚠ eslint(no-useless-concat): Unexpected string concatenation of literals.
╭─[no_useless_concat.tsx:1:13]
1 │ 'a' + 'b' + 'c' + 'd' + 'e' + foo
· ─────────
╰────
help: Rewrite into one string literal
⚠ eslint(no-useless-concat): Unexpected string concatenation of literals.
╭─[no_useless_concat.tsx:1:7]
1 │ 'a' + 'b' + 'c' + 'd' + 'e' + foo
· ─────────
╰────
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' + 'd' + 'e' + foo
· ─────────
╰────
help: Rewrite into one string literal