diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 1a30911e2..bf6def979 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -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, diff --git a/crates/oxc_linter/src/rules/eslint/no_useless_concat.rs b/crates/oxc_linter/src/rules/eslint/no_useless_concat.rs new file mode 100644 index 000000000..c58529111 --- /dev/null +++ b/crates/oxc_linter/src/rules/eslint/no_useless_concat.rs @@ -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? + /// + /// It’s 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(); +} diff --git a/crates/oxc_linter/src/snapshots/no_useless_concat.snap b/crates/oxc_linter/src/snapshots/no_useless_concat.snap new file mode 100644 index 000000000..62e3fc47f --- /dev/null +++ b/crates/oxc_linter/src/snapshots/no_useless_concat.snap @@ -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