Skip to content

Commit

Permalink
feat(eslint-plugin): [no-useless-template-literals] add new rule
Browse files Browse the repository at this point in the history
  • Loading branch information
StyleShit committed Nov 19, 2023
1 parent 4a079d7 commit d56e6f2
Show file tree
Hide file tree
Showing 3 changed files with 348 additions and 0 deletions.
64 changes: 64 additions & 0 deletions packages/eslint-plugin/docs/rules/no-useless-template-literals.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
---
description: 'Disallow unnecessary template literals.'
---

> 🛑 This file is source code, not the primary documentation location! 🛑
>
> See **https://typescript-eslint.io/rules/no-useless-template-literals** for documentation.
This rule reports template literals that can be simplified to a normal string literal.

## Examples

<!--tabs-->

### ❌ Incorrect

```ts
const ab1 = `${'a'}${'b'}`;
const ab2 = `a${'b'}`;

const stringWithNumber = `1 + 1 = ${2}`;

const stringWithBoolean = `${'true is '}${true}`;

const string = 'a';
const wrappedString = `${string}`;

declare const intersectionWithString: string & { _brand: 'test-brand' };
const wrappedIntersection = `${intersectionWithString}`;
```

### ✅ Correct

```ts
const string = 'a';
const concatenatedString = `${string}-b`;

const number = 1;
const concatenatedNumber = `${number}-2`;

const boolean = true;
const concatenatedBoolean = `${boolean}-false`;

const nullish = null;
const concatenatedNullish = `${nullish}-undefined`;

const left = 'left';
const right = 'right';
const concatenatedVariables = `${left}-${right}`;

const concatenatedExpressions = `${1 + 2}-${3 + 4}`;

const taggedTemplate = tag`${'a'}-${'b'}`;

const wrappedNumber = `${number}`;
const wrappedBoolean = `${boolean}`;
const wrappedNull = `${nullish}`;
```

<!--/tabs-->

## Related To

- [`restrict-template-expressions`](./restrict-template-expressions.md)
90 changes: 90 additions & 0 deletions packages/eslint-plugin/src/rules/no-useless-template-literals.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import type { TSESTree } from '@typescript-eslint/utils';
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
import * as ts from 'typescript';

import {
createRule,
getConstrainedTypeAtLocation,
getParserServices,
isTypeFlagSet,
} from '../util';

type MessageId = 'noUselessTemplateLiteral';

export default createRule<[], MessageId>({
name: 'no-useless-template-literals',
meta: {
type: 'problem',
docs: {
description: 'Disallow unnecessary template literals',
recommended: 'recommended',
requiresTypeChecking: true,
},
messages: {
noUselessTemplateLiteral:
'Template literal expression is unnecessary and can be simplified.',
},
schema: [],
},
defaultOptions: [],
create(context) {
const services = getParserServices(context);

function isUnderlyingTypeString(expression: TSESTree.Expression): boolean {
const type = getConstrainedTypeAtLocation(services, expression);

const isString = (t: ts.Type): boolean => {
return isTypeFlagSet(t, ts.TypeFlags.StringLike);
};

if (type.isUnion()) {
return type.types.every(isString);
}

if (type.isIntersection()) {
return type.types.some(isString);
}

return isString(type);
}

return {
TemplateLiteral(node: TSESTree.TemplateLiteral): void {
// don't check tagged template literals
if (node.parent.type === AST_NODE_TYPES.TaggedTemplateExpression) {
return;
}

// don't allow a single variable in a template literal
const hasSingleStringVariable =
node.quasis.length === 2 &&
node.quasis[0].value.raw === '' &&
node.quasis[1].value.raw === '' &&
node.expressions.length === 1 &&
node.expressions[0].type === AST_NODE_TYPES.Identifier &&
isUnderlyingTypeString(node.expressions[0]);

if (hasSingleStringVariable) {
context.report({
node,
messageId: 'noUselessTemplateLiteral',
});

return;
}

// don't allow concatenating only literals in a template literal
const allAreLiterals = node.expressions.every(expression => {
return expression.type === AST_NODE_TYPES.Literal;
});

if (allAreLiterals) {
context.report({
node,
messageId: 'noUselessTemplateLiteral',
});
}
},
};
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import { RuleTester } from '@typescript-eslint/rule-tester';

import rule from '../../src/rules/no-useless-template-literals';
import { getFixturesRootDir } from '../RuleTester';

const rootPath = getFixturesRootDir();

const ruleTester = new RuleTester({
parser: '@typescript-eslint/parser',
parserOptions: {
tsconfigRootDir: rootPath,
project: './tsconfig.json',
},
});

ruleTester.run('no-useless-template-literals', rule, {
valid: [
"const string = 'a';",

// allow variables & literals concatenation
`
const string = 'a';
const concatenated = \`\${string}b\`;
`,

`
const number = 1;
const concatenated = \`\${number}b\`;
`,

`
const boolean = true;
const concatenated = \`\${boolean}b\`;
`,

`
const nullish = nullish;
const concatenated = \`\${nullish}-undefined\`;
`,

`
const left = 'a';
const right = 'b';
const concatenated = \`\${left}\${right}\`;
`,

`
const left = 'a';
const right = 'c';
const concatenated = \`\${left}b\${right}\`;
`,

`
const left = 'a';
const center = 'b';
const right = 'c';
const concatenated = \`\${left}\${center}\${right}\`;
`,

// allow expressions
`
const concatenated = \`1 + 1 = \${1 + 1}\`;
`,

`
const concatenated = \`true && false = \${true && false}\`;
`,

// allow tagged template literals
`
tag\`\${'a'}\${'b'}\`;
`,

// allow wrapping numbers and booleans since it converts them to strings
`
const number = 1;
const wrapped = \`\${number}\`;
`,

`
const boolean = true;
const wrapped = \`\${boolean}\`;
`,

`
const nullish = null;
const wrapped = \`\${nullish}\`;
`,

// allow union types that include string
`
declare const union: string | number;
const wrapped = \`\${union}\`;
`,
],

invalid: [
// don't allow concatenating only literals in a template literal
{
code: `
const concatenated = \`\${'a'}\${'b'}\`;
`,
errors: [
{
messageId: 'noUselessTemplateLiteral',
line: 2,
column: 30,
},
],
},

{
code: `
const concatenated = \`a\${'b'}\`;
`,
errors: [
{
messageId: 'noUselessTemplateLiteral',
line: 2,
column: 30,
},
],
},

{
code: `
const concatenated = \`\${'1 + 1 = '}\${2}\`;
`,
errors: [
{
messageId: 'noUselessTemplateLiteral',
line: 2,
column: 30,
},
],
},

{
code: `
const concatenated = \`1 + 1 = \${2}\`;
`,
errors: [
{
messageId: 'noUselessTemplateLiteral',
line: 2,
column: 30,
},
],
},

{
code: `
const concatenated = \`\${'a'}\${true}\`;
`,
errors: [
{
messageId: 'noUselessTemplateLiteral',
line: 2,
column: 30,
},
],
},

// don't allow a single string variable in a template literal
{
code: `
const string = 'a';
const wrapped = \`\${string}\`;
`,
errors: [
{
messageId: 'noUselessTemplateLiteral',
line: 3,
column: 25,
},
],
},

// don't allow intersection types that include string
{
code: `
declare const intersection: string & { _brand: 'test-brand' };
const wrapped = \`\${intersection}\`;
`,
errors: [
{
messageId: 'noUselessTemplateLiteral',
line: 3,
column: 25,
},
],
},
],
});

0 comments on commit d56e6f2

Please sign in to comment.