fix(linter/eslint/no-implicit-coercion): preserve template coercion whitespace (#25470)

## Summary

- only treat one-expression template literals as shorthand coercion when both cooked quasis are empty
- preserve leading, trailing, and escaped whitespace when disallowTemplateShorthand is enabled
- add regression coverage for meaningful whitespace and empty line continuations

Fixes #25454
This commit is contained in:
camc314 committed 2026-08-10 09:08:03 +00:00
1 parent 2e35238d65
commit b746e00fd1
2 files changed
+29 -31

No files matched your search

@@ -315,25 +315,20 @@ impl Rule for NoImplicitCoercion {
let first_quasi = &template.quasis[0];
let last_quasi = &template.quasis[1];
// Check if both quasi parts contain no meaningful content.
// We use the cooked value to handle escape sequences properly.
// ESLint considers templates like `\n${foo}` as coercion because
// escape sequences that become whitespace don't add meaningful content.
// We also allow backslash characters in the cooked value because they
// typically come from line continuations (e.g., `\\\n${foo}`) which
// ESLint also treats as implicit coercion.
let first_is_empty_or_whitespace = first_quasi
// Only an empty cooked value is shorthand coercion. Whitespace is part of the
// resulting string and must not be discarded by the fixer.
let first_is_empty = first_quasi
.value
.cooked
.as_ref()
.is_some_and(|s| s.chars().all(|c| c.is_whitespace() || c == '\\'));
let last_is_empty_or_whitespace = last_quasi
.is_some_and(|s| s.is_empty());
let last_is_empty = last_quasi
.value
.cooked
.as_ref()
.is_some_and(|s| s.chars().all(|c| c.is_whitespace() || c == '\\'));
.is_some_and(|s| s.is_empty());
if first_is_empty_or_whitespace && last_is_empty_or_whitespace {
if first_is_empty && last_is_empty {
let expr = &template.expressions[0];
// Don't warn if the expression is already a string literal or String() call
if !is_string_literal_or_string_call(expr) {
@@ -554,6 +549,20 @@ fn test() {
("foo += `${bar}`", None),
("`a${foo}`", Some(serde_json::json!([{ "disallowTemplateShorthand": true }]))),
("`${foo}b`", Some(serde_json::json!([{ "disallowTemplateShorthand": true }]))),
("` ${foo}`", Some(serde_json::json!([{ "disallowTemplateShorthand": true }]))),
("`${foo} `", Some(serde_json::json!([{ "disallowTemplateShorthand": true }]))),
(r"`\n${foo}`", Some(serde_json::json!([{ "disallowTemplateShorthand": true }]))),
(r"`${foo}\t`", Some(serde_json::json!([{ "disallowTemplateShorthand": true }]))),
(
r"`\\
${foo}`",
Some(serde_json::json!([{ "disallowTemplateShorthand": true }])),
),
(
r"`${foo}\\
`",
Some(serde_json::json!([{ "disallowTemplateShorthand": true }])),
),
("`${foo}${bar}`", Some(serde_json::json!([{ "disallowTemplateShorthand": true }]))),
("tag`${foo}`", Some(serde_json::json!([{ "disallowTemplateShorthand": true }]))),
("`${foo}`", None),
@@ -597,16 +606,7 @@ fn test() {
(r#"foo.bar+"""#, None),
("foo.bar+``", None),
("`${foo}`", Some(serde_json::json!([{ "disallowTemplateShorthand": true }]))),
(
r"`\\
${foo}`",
Some(serde_json::json!([{ "disallowTemplateShorthand": true }])),
),
(
r"`${foo}\\
`",
Some(serde_json::json!([{ "disallowTemplateShorthand": true }])),
),
("`\\\n${foo}`", Some(serde_json::json!([{ "disallowTemplateShorthand": true }]))),
(r#"foo += """#, None),
("foo += ``", None),
("var a = !!foo", Some(serde_json::json!([{ "boolean": true, "allow": ["~"] }]))),
@@ -637,6 +637,11 @@ fn test() {
"var a = Boolean(foo)",
Some(serde_json::json!([{ "boolean": true, "allow": ["~"] }])),
),
(
"`${foo}`",
"String(foo)",
Some(serde_json::json!([{ "disallowTemplateShorthand": true }])),
),
];
Tester::new(NoImplicitCoercion::NAME, NoImplicitCoercion::PLUGIN, pass, fail)
.expect_fix(fix)
@@ -172,15 +172,8 @@ source: crates/oxc_linter/src/tester.rs
⚠ eslint(no-implicit-coercion): Unexpected implicit coercion to string
╭─[no_implicit_coercion.tsx:1:1]
1 │ ╭─▶ `\\
2 │ ╰─▶ ${foo}`
╰────
help: Use `String(value)` instead
⚠ eslint(no-implicit-coercion): Unexpected implicit coercion to string
╭─[no_implicit_coercion.tsx:1:1]
1 │ ╭─▶ `${foo}\\
2 │ ╰─▶ `
1 │ ╭─▶ `\
2 │ ╰─▶ ${foo}`
╰────
help: Use `String(value)` instead