Skip to content

Commit c661bac

Browse files
feat(linter): add eslint/prefer-template rule (#13117)
Relates to #11947
1 parent cc2a85b commit c661bac

File tree

3 files changed

+757
-0
lines changed

3 files changed

+757
-0
lines changed

crates/oxc_linter/src/rules.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ mod eslint {
175175
pub mod prefer_promise_reject_errors;
176176
pub mod prefer_rest_params;
177177
pub mod prefer_spread;
178+
pub mod prefer_template;
178179
pub mod radix;
179180
pub mod require_await;
180181
pub mod require_yield;
@@ -747,6 +748,7 @@ oxc_macros::declare_all_lint_rules! {
747748
eslint::no_void,
748749
eslint::no_with,
749750
eslint::operator_assignment,
751+
eslint::prefer_template,
750752
eslint::prefer_destructuring,
751753
eslint::prefer_promise_reject_errors,
752754
eslint::prefer_exponentiation_operator,
Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
use oxc_ast::{
2+
AstKind,
3+
ast::{BinaryExpression, BinaryOperator, Expression},
4+
};
5+
use oxc_diagnostics::OxcDiagnostic;
6+
use oxc_macros::declare_oxc_lint;
7+
use oxc_span::Span;
8+
9+
use crate::{AstNode, context::LintContext, rule::Rule};
10+
11+
fn prefer_template_diagnostic(span: Span) -> OxcDiagnostic {
12+
OxcDiagnostic::warn("Unexpected string concatenation.")
13+
.with_help("Use template literals instead of string concatenation.")
14+
.with_label(span)
15+
}
16+
17+
#[derive(Debug, Default, Clone)]
18+
pub struct PreferTemplate;
19+
20+
declare_oxc_lint!(
21+
/// ### What it does
22+
///
23+
/// Require template literals instead of string concatenation.
24+
///
25+
/// ### Why is this bad?
26+
///
27+
/// In ES2015 (ES6), we can use template literals instead of string concatenation.
28+
///
29+
/// ### Examples
30+
///
31+
/// Examples of **incorrect** code for this rule:
32+
/// ```js
33+
/// const str = "Hello, " + name + "!";
34+
/// const str1 = "Time: " + (12 * 60 * 60 * 1000);
35+
/// ```
36+
///
37+
/// Examples of **correct** code for this rule:
38+
/// ```js
39+
/// const str = "Hello World!";
40+
/// const str2 = `Time: ${12 * 60 * 60 * 1000}`;
41+
/// const str4 = "Hello, " + "World!";
42+
/// ```
43+
PreferTemplate,
44+
eslint,
45+
style,
46+
pending
47+
);
48+
49+
impl Rule for PreferTemplate {
50+
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
51+
let AstKind::BinaryExpression(expr) = node.kind() else {
52+
return;
53+
};
54+
if !matches!(expr.operator, BinaryOperator::Addition) {
55+
return;
56+
}
57+
// check is the outermost binary expression
58+
let not_outermost = ctx.nodes().ancestor_kinds(node.id()).any(
59+
|v| matches!(v, AstKind::BinaryExpression(e) if e.operator == BinaryOperator::Addition),
60+
);
61+
if not_outermost {
62+
return;
63+
}
64+
if check_should_report(expr) {
65+
ctx.diagnostic(prefer_template_diagnostic(expr.span));
66+
}
67+
}
68+
}
69+
70+
fn check_should_report(expr: &BinaryExpression) -> bool {
71+
if expr.operator != BinaryOperator::Addition {
72+
return false;
73+
}
74+
75+
let left = expr.left.get_inner_expression();
76+
let right = expr.right.get_inner_expression();
77+
78+
let left_is_string =
79+
matches!(left, Expression::StringLiteral(_) | Expression::TemplateLiteral(_));
80+
let right_is_string =
81+
matches!(right, Expression::StringLiteral(_) | Expression::TemplateLiteral(_));
82+
83+
match (left_is_string, right_is_string) {
84+
// 'a' + 'v'
85+
(true, true) => false,
86+
// 'a' + (v + '3')
87+
(true, false) => any_none_string_literal(right),
88+
// (v + 'a') + 'c'
89+
(false, true) => any_none_string_literal(left),
90+
// a + b
91+
(false, false) => !all_none_string_literal(left) || !all_none_string_literal(right),
92+
}
93+
}
94+
95+
fn all_none_string_literal(expr: &Expression) -> bool {
96+
match expr {
97+
Expression::BinaryExpression(binary) if binary.operator == BinaryOperator::Addition => {
98+
all_none_string_literal(binary.left.get_inner_expression())
99+
&& all_none_string_literal(binary.right.get_inner_expression())
100+
}
101+
Expression::StringLiteral(_) | Expression::TemplateLiteral(_) => false,
102+
_ => true,
103+
}
104+
}
105+
106+
fn any_none_string_literal(expr: &Expression) -> bool {
107+
match expr {
108+
Expression::BinaryExpression(binary) if binary.operator == BinaryOperator::Addition => {
109+
any_none_string_literal(binary.left.get_inner_expression())
110+
|| any_none_string_literal(binary.right.get_inner_expression())
111+
}
112+
Expression::StringLiteral(_) | Expression::TemplateLiteral(_) => false,
113+
_ => true,
114+
}
115+
}
116+
117+
#[test]
118+
fn test() {
119+
use crate::tester::Tester;
120+
121+
let pass = vec![
122+
"'use strict';",
123+
"var foo = 'foo' + '\0';",
124+
"var foo = 'bar';",
125+
"var foo = 'bar' + 'baz';",
126+
"var foo = foo + +'100';",
127+
"var foo = `bar`;",
128+
"var foo = `hello, ${name}!`;",
129+
r#"var foo = `foo` + `bar` + "hoge";"#,
130+
r#"var foo = `foo` +
131+
`bar` +
132+
"hoge";"#,
133+
];
134+
135+
let fail = vec![
136+
"var foo = 'hello, ' + name + '!';",
137+
"var foo = bar + 'baz';",
138+
"var foo = bar + `baz`;",
139+
"var foo = +100 + 'yen';",
140+
"var foo = 'bar' + baz;",
141+
"var foo = '¥' + (n * 1000) + '-'",
142+
"var foo = 'aaa' + aaa; var bar = 'bbb' + bbb;",
143+
"var string = (number + 1) + 'px';",
144+
"var foo = 'bar' + baz + 'qux';",
145+
"var foo = '0 backslashes: ${bar}' + baz;",
146+
r"var foo = '1 backslash: \${bar}' + baz;",
147+
"var foo = '2 backslashes: \\${bar}' + baz;",
148+
r"var foo = '3 backslashes: \\\${bar}' + baz;",
149+
"var foo = bar + 'this is a backtick: `' + baz;",
150+
r"var foo = bar + 'this is a backtick preceded by a backslash: \`' + baz;",
151+
"var foo = bar + 'this is a backtick preceded by two backslashes: \\`' + baz;",
152+
"var foo = bar + `${baz}foo`;",
153+
"var foo = bar + baz + 'qux';",
154+
"var foo = /* a */ 'bar' /* b */ + /* c */ baz /* d */ + 'qux' /* e */ ;",
155+
"var foo = bar + ('baz') + 'qux' + (boop);",
156+
r"foo + 'unescapes an escaped single quote in a single-quoted string: \''",
157+
r#"foo + "unescapes an escaped double quote in a double-quoted string: ""#,
158+
r#"foo + 'does not unescape an escaped double quote in a single-quoted string: "'"#,
159+
r#"foo + "does not unescape an escaped single quote in a double-quoted string: \'""#,
160+
r"foo + 'handles unicode escapes correctly: \x27'",
161+
r"foo + 'does not autofix octal escape sequence' + '\x1b'",
162+
r"foo + 'does not autofix non-octal decimal escape sequence' + '\8'",
163+
r"foo + '\n other text \x1b'",
164+
r"foo + '\0\1'",
165+
"foo + '\08'",
166+
"foo + '\\033'",
167+
"foo + '\0'",
168+
r#""default-src 'self' https://*.google.com;"
169+
+ "frame-ancestors 'none';"
170+
+ "report-to " + foo + ";""#,
171+
"'a' + 'b' + foo",
172+
"'a' + 'b' + foo + 'c' + 'd'",
173+
"'a' + 'b + c' + foo + 'd' + 'e'",
174+
"'a' + 'b' + foo + ('c' + 'd')",
175+
"'a' + 'b' + foo + ('a' + 'b')",
176+
"'a' + 'b' + foo + ('c' + 'd') + ('e' + 'f')",
177+
"foo + ('a' + 'b') + ('c' + 'd')",
178+
"'a' + foo + ('b' + 'c') + ('d' + bar + 'e')",
179+
"foo + ('b' + 'c') + ('d' + bar + 'e')",
180+
"'a' + 'b' + foo + ('c' + 'd' + 'e')",
181+
"'a' + 'b' + foo + ('c' + bar + 'd')",
182+
"'a' + 'b' + foo + ('c' + bar + ('d' + 'e') + 'f')",
183+
"'a' + 'b' + foo + ('c' + bar + 'e') + 'f' + test",
184+
"'a' + foo + ('b' + bar + 'c') + ('d' + test)",
185+
"'a' + foo + ('b' + 'c') + ('d' + bar)",
186+
"foo + ('a' + bar + 'b') + 'c' + test",
187+
"'a' + '`b`' + c",
188+
"'a' + '`b` + `c`' + d",
189+
"'a' + b + ('`c`' + '`d`')",
190+
"'`a`' + b + ('`c`' + '`d`')",
191+
"foo + ('`a`' + bar + '`b`') + '`c`' + test",
192+
"'a' + ('b' + 'c') + d",
193+
"'a' + ('`b`' + '`c`') + d",
194+
"a + ('b' + 'c') + d",
195+
"a + ('b' + 'c') + (d + 'e')",
196+
"a + ('`b`' + '`c`') + d",
197+
"a + ('`b` + `c`' + '`d`') + e",
198+
"'a' + ('b' + 'c' + 'd') + e",
199+
"'a' + ('b' + 'c' + 'd' + (e + 'f') + 'g' +'h' + 'i') + j",
200+
"a + (('b' + 'c') + 'd')",
201+
"(a + 'b') + ('c' + 'd') + e",
202+
r#"var foo = "Hello " + "world " + "another " + test"#,
203+
r#"'Hello ' + '"world" ' + test"#,
204+
r#""Hello " + "'world' " + test"#,
205+
];
206+
207+
// let _fix = vec![
208+
// ("var foo = 'hello, ' + name + '!';", "var foo = `hello, ${ name }!`;", None),
209+
// ("var foo = bar + 'baz';", "var foo = `${bar }baz`;", None),
210+
// ("var foo = bar + `baz`;", "var foo = `${bar }baz`;", None),
211+
// ("var foo = +100 + 'yen';", "var foo = `${+100 }yen`;", None),
212+
// ("var foo = 'bar' + baz;", "var foo = `bar${ baz}`;", None),
213+
// ("var foo = '¥' + (n * 1000) + '-'", "var foo = `¥${ n * 1000 }-`", None),
214+
// ("var foo = 'aaa' + aaa; var bar = 'bbb' + bbb;", "var foo = `aaa${ aaa}`; var bar = `bbb${ bbb}`;", None),
215+
// ("var string = (number + 1) + 'px';", "var string = `${number + 1 }px`;", None),
216+
// ("var foo = 'bar' + baz + 'qux';", "var foo = `bar${ baz }qux`;", None),
217+
// // ("var foo = '0 backslashes: ${bar}' + baz;", "var foo = `0 backslashes: \${bar}${ baz}`;", None),
218+
// // ("var foo = '1 backslash: \${bar}' + baz;", "var foo = `1 backslash: \${bar}${ baz}`;", None),
219+
// // ("var foo = '2 backslashes: \\${bar}' + baz;", "var foo = `2 backslashes: \\\${bar}${ baz}`;", None),
220+
// // ("var foo = '3 backslashes: \\\${bar}' + baz;", "var foo = `3 backslashes: \\\${bar}${ baz}`;", None),
221+
// // ("var foo = bar + 'this is a backtick: `' + baz;", "var foo = `${bar }this is a backtick: \`${ baz}`;", None),
222+
// // ("var foo = bar + 'this is a backtick preceded by a backslash: \`' + baz;", "var foo = `${bar }this is a backtick preceded by a backslash: \`${ baz}`;", None),
223+
// // ("var foo = bar + 'this is a backtick preceded by two backslashes: \\`' + baz;", "var foo = `${bar }this is a backtick preceded by two backslashes: \\\`${ baz}`;", None),
224+
// ("var foo = bar + `${baz}foo`;", "var foo = `${bar }${baz}foo`;", None),
225+
// ("var foo = bar + baz + 'qux';", "var foo = `${bar + baz }qux`;", None),
226+
// ("var foo = /* a */ 'bar' /* b */ + /* c */ baz /* d */ + 'qux' /* e */ ;", "var foo = /* a */ `bar${ /* b */ /* c */ baz /* d */ }qux` /* e */ ;", None),
227+
// ("var foo = bar + ('baz') + 'qux' + (boop);", "var foo = `${bar }baz` + `qux${ boop}`;", None),
228+
// ("foo + 'unescapes an escaped single quote in a single-quoted string: \''", "`${foo }unescapes an escaped single quote in a single-quoted string: '`", None),
229+
// (r#"foo + "unescapes an escaped double quote in a double-quoted string: """#, r#"`${foo }unescapes an escaped double quote in a double-quoted string: "`"#, None),
230+
// (r#"foo + 'does not unescape an escaped double quote in a single-quoted string: "'"#, r#"`${foo }does not unescape an escaped double quote in a single-quoted string: "`"#, None),
231+
// (r#"foo + "does not unescape an escaped single quote in a double-quoted string: \'""#, "`${foo }does not unescape an escaped single quote in a double-quoted string: \'`", None),
232+
// ("foo + 'handles unicode escapes correctly: \x27'", "`${foo }handles unicode escapes correctly: \x27`", None),
233+
// ("foo + '\\033'", "`${foo }\\033`", None),
234+
// ("foo + '\0'", "`${foo }\0`", None),
235+
// (r#""default-src 'self' https://*.google.com;"
236+
// + "frame-ancestors 'none';"
237+
// + "report-to " + foo + ";""#, "`default-src 'self' https://*.google.com;`
238+
// + `frame-ancestors 'none';`
239+
// + `report-to ${ foo };`", None),
240+
// ("'a' + 'b' + foo", "`a` + `b${ foo}`", None),
241+
// ("'a' + 'b' + foo + 'c' + 'd'", "`a` + `b${ foo }c` + `d`", None),
242+
// ("'a' + 'b + c' + foo + 'd' + 'e'", "`a` + `b + c${ foo }d` + `e`", None),
243+
// ("'a' + 'b' + foo + ('c' + 'd')", "`a` + `b${ foo }c` + `d`", None),
244+
// ("'a' + 'b' + foo + ('a' + 'b')", "`a` + `b${ foo }a` + `b`", None),
245+
// ("'a' + 'b' + foo + ('c' + 'd') + ('e' + 'f')", "`a` + `b${ foo }c` + `d` + `e` + `f`", None),
246+
// ("foo + ('a' + 'b') + ('c' + 'd')", "`${foo }a` + `b` + `c` + `d`", None),
247+
// ("'a' + foo + ('b' + 'c') + ('d' + bar + 'e')", "`a${ foo }b` + `c` + `d${ bar }e`", None),
248+
// ("foo + ('b' + 'c') + ('d' + bar + 'e')", "`${foo }b` + `c` + `d${ bar }e`", None),
249+
// ("'a' + 'b' + foo + ('c' + 'd' + 'e')", "`a` + `b${ foo }c` + `d` + `e`", None),
250+
// ("'a' + 'b' + foo + ('c' + bar + 'd')", "`a` + `b${ foo }c${ bar }d`", None),
251+
// ("'a' + 'b' + foo + ('c' + bar + ('d' + 'e') + 'f')", "`a` + `b${ foo }c${ bar }d` + `e` + `f`", None),
252+
// ("'a' + 'b' + foo + ('c' + bar + 'e') + 'f' + test", "`a` + `b${ foo }c${ bar }e` + `f${ test}`", None),
253+
// ("'a' + foo + ('b' + bar + 'c') + ('d' + test)", "`a${ foo }b${ bar }c` + `d${ test}`", None),
254+
// ("'a' + foo + ('b' + 'c') + ('d' + bar)", "`a${ foo }b` + `c` + `d${ bar}`", None),
255+
// ("foo + ('a' + bar + 'b') + 'c' + test", "`${foo }a${ bar }b` + `c${ test}`", None),
256+
// // ("'a' + '`b`' + c", "`a` + `\`b\`${ c}`", None),
257+
// // ("'a' + '`b` + `c`' + d", "`a` + `\`b\` + \`c\`${ d}`", None),
258+
// // ("'a' + b + ('`c`' + '`d`')", "`a${ b }\`c\`` + `\`d\``", None),
259+
// // ("'`a`' + b + ('`c`' + '`d`')", "`\`a\`${ b }\`c\`` + `\`d\``", None),
260+
// // ("foo + ('`a`' + bar + '`b`') + '`c`' + test", "`${foo }\`a\`${ bar }\`b\`` + `\`c\`${ test}`", None),
261+
// ("'a' + ('b' + 'c') + d", "`a` + `b` + `c${ d}`", None),
262+
// // ("'a' + ('`b`' + '`c`') + d", "`a` + `\`b\`` + `\`c\`${ d}`", None),
263+
// ("a + ('b' + 'c') + d", "`${a }b` + `c${ d}`", None),
264+
// ("a + ('b' + 'c') + (d + 'e')", "`${a }b` + `c${ d }e`", None),
265+
// // ("a + ('`b`' + '`c`') + d", "`${a }\`b\`` + `\`c\`${ d}`", None),
266+
// // ("a + ('`b` + `c`' + '`d`') + e", "`${a }\`b\` + \`c\`` + `\`d\`${ e}`", None),
267+
// ("'a' + ('b' + 'c' + 'd') + e", "`a` + `b` + `c` + `d${ e}`", None),
268+
// ("'a' + ('b' + 'c' + 'd' + (e + 'f') + 'g' +'h' + 'i') + j", "`a` + `b` + `c` + `d${ e }fg` +`h` + `i${ j}`", None),
269+
// ("a + (('b' + 'c') + 'd')", "`${a }b` + `c` + `d`", None),
270+
// ("(a + 'b') + ('c' + 'd') + e", "`${a }b` + `c` + `d${ e}`", None),
271+
// (r#"var foo = "Hello " + "world " + "another " + test"#, "var foo = `Hello ` + `world ` + `another ${ test}`", None),
272+
// (r#"'Hello ' + '"world" ' + test"#, r#"`Hello ` + `"world" ${ test}`"#, None),
273+
// (r#""Hello " + "'world' " + test"#, "`Hello ` + `'world' ${ test}`", None)
274+
// ];
275+
Tester::new(PreferTemplate::NAME, PreferTemplate::PLUGIN, pass, fail).test_and_snapshot();
276+
}

0 commit comments

Comments
 (0)