Skip to content

Commit fcd6cf0

Browse files
fix(eslint-plugin): [no-unnecessary-template-expressions] allow template expressions used to make trailing whitespace visible (#10363)
* whitespace is useful * no-longer-needed-disable * windows stuff and template cov * remove duplicate test case * tweaks
1 parent c1b1106 commit fcd6cf0

File tree

3 files changed

+140
-15
lines changed

3 files changed

+140
-15
lines changed

packages/eslint-plugin/src/rules/no-unnecessary-template-expression.ts

+59-12
Original file line numberDiff line numberDiff line change
@@ -126,27 +126,58 @@ export default createRule<[], MessageId>({
126126
return;
127127
}
128128

129-
const fixableExpressions = node.expressions
130-
.filter(
131-
expression =>
132-
isLiteral(expression) ||
133-
isTemplateLiteral(expression) ||
129+
const fixableExpressionsReversed = node.expressions
130+
.map((expression, index) => ({
131+
expression,
132+
nextQuasi: node.quasis[index + 1],
133+
prevQuasi: node.quasis[index],
134+
}))
135+
.filter(({ expression, nextQuasi }) => {
136+
if (
134137
isUndefinedIdentifier(expression) ||
135138
isInfinityIdentifier(expression) ||
136-
isNaNIdentifier(expression),
137-
)
139+
isNaNIdentifier(expression)
140+
) {
141+
return true;
142+
}
143+
144+
if (isLiteral(expression)) {
145+
// allow trailing whitespace literal
146+
if (startsWithNewLine(nextQuasi.value.raw)) {
147+
return !(
148+
typeof expression.value === 'string' &&
149+
isWhitespace(expression.value)
150+
);
151+
}
152+
return true;
153+
}
154+
155+
if (isTemplateLiteral(expression)) {
156+
// allow trailing whitespace literal
157+
if (startsWithNewLine(nextQuasi.value.raw)) {
158+
return !(
159+
expression.quasis.length === 1 &&
160+
isWhitespace(expression.quasis[0].value.raw)
161+
);
162+
}
163+
return true;
164+
}
165+
166+
return false;
167+
})
138168
.reverse();
139169

140170
let nextCharacterIsOpeningCurlyBrace = false;
141171

142-
for (const expression of fixableExpressions) {
172+
for (const {
173+
expression,
174+
nextQuasi,
175+
prevQuasi,
176+
} of fixableExpressionsReversed) {
143177
const fixers: ((fixer: TSESLint.RuleFixer) => TSESLint.RuleFix[])[] =
144178
[];
145-
const index = node.expressions.indexOf(expression);
146-
const prevQuasi = node.quasis[index];
147-
const nextQuasi = node.quasis[index + 1];
148179

149-
if (nextQuasi.value.raw.length !== 0) {
180+
if (nextQuasi.value.raw !== '') {
150181
nextCharacterIsOpeningCurlyBrace =
151182
nextQuasi.value.raw.startsWith('{');
152183
}
@@ -271,3 +302,19 @@ export default createRule<[], MessageId>({
271302
};
272303
},
273304
});
305+
306+
function isWhitespace(x: string): boolean {
307+
// allow empty string too since we went to allow
308+
// ` ${''}
309+
// `;
310+
//
311+
// in addition to
312+
// `${' '}
313+
// `;
314+
//
315+
return /^\s*$/.test(x);
316+
}
317+
318+
function startsWithNewLine(x: string): boolean {
319+
return x.startsWith('\n') || x.startsWith('\r\n');
320+
}

packages/eslint-plugin/tests/rules/no-unnecessary-template-expression.test.ts

+81
Original file line numberDiff line numberDiff line change
@@ -885,6 +885,75 @@ const invalidCases: readonly InvalidTestCase<
885885
errors: [{ messageId: 'noUnnecessaryTemplateExpression' }],
886886
output: '` \\ud83d\\udc68 `;',
887887
},
888+
{
889+
code: `
890+
\`
891+
this code does not have trailing whitespace: \${' '}\\n even though it might look it.\`;
892+
`,
893+
errors: [
894+
{
895+
messageId: 'noUnnecessaryTemplateExpression',
896+
},
897+
],
898+
output: `
899+
\`
900+
this code does not have trailing whitespace: \\n even though it might look it.\`;
901+
`,
902+
},
903+
{
904+
code: `
905+
\`
906+
this code has trailing position template expression \${'but it isn\\'t whitespace'}
907+
\`;
908+
`,
909+
errors: [
910+
{
911+
messageId: 'noUnnecessaryTemplateExpression',
912+
},
913+
],
914+
output: `
915+
\`
916+
this code has trailing position template expression but it isn\\'t whitespace
917+
\`;
918+
`,
919+
},
920+
{
921+
code: noFormat`
922+
\`trailing whitespace followed by escaped windows newline: \${' '}\\r\\n\`;
923+
`,
924+
errors: [
925+
{
926+
messageId: 'noUnnecessaryTemplateExpression',
927+
},
928+
],
929+
output: `
930+
\`trailing whitespace followed by escaped windows newline: \\r\\n\`;
931+
`,
932+
},
933+
{
934+
code: `
935+
\`template literal with interpolations followed by newline: \${\` \${'interpolation'} \`}
936+
\`;
937+
`,
938+
errors: [
939+
{
940+
messageId: 'noUnnecessaryTemplateExpression',
941+
},
942+
{
943+
messageId: 'noUnnecessaryTemplateExpression',
944+
},
945+
],
946+
output: [
947+
`
948+
\`template literal with interpolations followed by newline: \${'interpolation'}${' '}
949+
\`;
950+
`,
951+
`
952+
\`template literal with interpolations followed by newline: interpolation${' '}
953+
\`;
954+
`,
955+
],
956+
},
888957
];
889958

890959
describe('fixer should not change runtime value', () => {
@@ -1023,6 +1092,18 @@ ruleTester.run('no-unnecessary-template-expression', rule, {
10231092
`
10241093
\`not a useless \${String.raw\`nested interpolation \${a}\`}\`;
10251094
`,
1095+
`
1096+
\`
1097+
this code has trailing whitespace: \${' '}
1098+
\`;
1099+
`,
1100+
noFormat`
1101+
\`this code has trailing whitespace with a windows \\\r new line: \${' '}\r\n\`;
1102+
`,
1103+
`
1104+
\`trailing position interpolated empty string also makes whitespace clear \${''}
1105+
\`;
1106+
`,
10261107
],
10271108

10281109
invalid: [

packages/eslint-plugin/tests/rules/no-useless-constructor.test.ts

-3
Original file line numberDiff line numberDiff line change
@@ -209,8 +209,6 @@ class A extends Object {
209209
`,
210210
],
211211
invalid: [
212-
/* eslint-disable @typescript-eslint/no-unnecessary-template-expression -- trailing whitespace */
213-
214212
{
215213
code: `
216214
class A {
@@ -432,6 +430,5 @@ ${' '}
432430
},
433431
],
434432
},
435-
/* eslint-enable @typescript-eslint/no-unnecessary-template-expression */
436433
],
437434
});

0 commit comments

Comments
 (0)