From 1d794d72d2165a1cd5b1e373716984e3bf45a527 Mon Sep 17 00:00:00 2001 From: Guillaume Drouard Date: Sat, 8 Mar 2025 16:21:27 +0100 Subject: [PATCH 1/8] feat(eslint-plugin-template): init prefer-template rule --- .../src/configs/template-all.ts | 1 + packages/eslint-plugin-template/README.md | 1 + .../docs/rules/prefer-template-literal.md | 872 ++++++++++++++++++ .../src/configs/all.json | 1 + packages/eslint-plugin-template/src/index.ts | 4 + .../src/rules/eqeqeq.ts | 16 +- .../src/rules/prefer-template-literal.ts | 129 +++ .../src/utils/literal-primitive.ts | 14 + .../rules/prefer-template-literal/cases.ts | 300 ++++++ .../rules/prefer-template-literal/spec.ts | 14 + 10 files changed, 1341 insertions(+), 11 deletions(-) create mode 100644 packages/eslint-plugin-template/docs/rules/prefer-template-literal.md create mode 100644 packages/eslint-plugin-template/src/rules/prefer-template-literal.ts create mode 100644 packages/eslint-plugin-template/src/utils/literal-primitive.ts create mode 100644 packages/eslint-plugin-template/tests/rules/prefer-template-literal/cases.ts create mode 100644 packages/eslint-plugin-template/tests/rules/prefer-template-literal/spec.ts diff --git a/packages/angular-eslint/src/configs/template-all.ts b/packages/angular-eslint/src/configs/template-all.ts index 5368f3fb7..89c04da1d 100644 --- a/packages/angular-eslint/src/configs/template-all.ts +++ b/packages/angular-eslint/src/configs/template-all.ts @@ -42,6 +42,7 @@ export default ( '@angular-eslint/template/prefer-ngsrc': 'error', '@angular-eslint/template/prefer-self-closing-tags': 'error', '@angular-eslint/template/prefer-static-string-properties': 'error', + '@angular-eslint/template/prefer-template-literal': 'error', '@angular-eslint/template/role-has-required-aria': 'error', '@angular-eslint/template/table-scope': 'error', '@angular-eslint/template/use-track-by-function': 'error', diff --git a/packages/eslint-plugin-template/README.md b/packages/eslint-plugin-template/README.md index a39858eb6..fa1c71815 100644 --- a/packages/eslint-plugin-template/README.md +++ b/packages/eslint-plugin-template/README.md @@ -63,6 +63,7 @@ Please see https://github.com/angular-eslint/angular-eslint for full usage instr | [`no-positive-tabindex`](https://github.com/angular-eslint/angular-eslint/blob/main/packages/eslint-plugin-template/docs/rules/no-positive-tabindex.md) | Ensures that the `tabindex` attribute is not positive | | | :bulb: | | | [`prefer-control-flow`](https://github.com/angular-eslint/angular-eslint/blob/main/packages/eslint-plugin-template/docs/rules/prefer-control-flow.md) | Ensures that the built-in control flow is used. | | | | | | [`prefer-ngsrc`](https://github.com/angular-eslint/angular-eslint/blob/main/packages/eslint-plugin-template/docs/rules/prefer-ngsrc.md) | Ensures ngSrc is used instead of src for img elements | | | | | +| [`prefer-template-literal`](https://github.com/angular-eslint/angular-eslint/blob/main/packages/eslint-plugin-template/docs/rules/prefer-template-literal.md) | Ensure that template literals are used instead of concatenating strings or expressions | | :wrench: | | | | [`role-has-required-aria`](https://github.com/angular-eslint/angular-eslint/blob/main/packages/eslint-plugin-template/docs/rules/role-has-required-aria.md) | [Accessibility] Ensures elements with ARIA roles have all required properties for that role. | | | :bulb: | :accessibility: | | [`table-scope`](https://github.com/angular-eslint/angular-eslint/blob/main/packages/eslint-plugin-template/docs/rules/table-scope.md) | [Accessibility] Ensures that the `scope` attribute is only used on the `` element | | :wrench: | | :accessibility: | | [`use-track-by-function`](https://github.com/angular-eslint/angular-eslint/blob/main/packages/eslint-plugin-template/docs/rules/use-track-by-function.md) | Ensures trackBy function is used | | | | | diff --git a/packages/eslint-plugin-template/docs/rules/prefer-template-literal.md b/packages/eslint-plugin-template/docs/rules/prefer-template-literal.md new file mode 100644 index 000000000..ba4b45f86 --- /dev/null +++ b/packages/eslint-plugin-template/docs/rules/prefer-template-literal.md @@ -0,0 +1,872 @@ + + +
+ +# `@angular-eslint/template/prefer-template-literal` + +Ensure that template literals are used instead of concatenating strings or expressions + +- Type: suggestion +- 🔧 Supports autofix (`--fix`) + +
+ +## Rule Options + +The rule does not have any configuration options. + +
+ +## Usage Examples + +> The following examples are generated automatically from the actual unit tests within the plugin, so you can be assured that their behavior is accurate based on the current commit. + +
+ +
+❌ - Toggle examples of incorrect code for this rule + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```html +{{ 'prefix-' + '-suffix' }} + ~~~~~~~~~~~~~~~~~~~~~ +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```html +{{ `prefix-${value}-suffix` + `-prefix2-${value2}-suffix2` }} + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```html + + ~~~~~~~~~~~~~~~~~~~ +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```html +@if (value() + '-suffix' | pipe) {} + ~~~~~~~~~~~~~~~~~~~ +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```html +@defer (when value() + '-suffix' | pipe) {} + ~~~~~~~~~~~~~~~~~~~ +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```html +{{ 'prefix-' + 42 }} + ~~~~~~~~~~~~~~ +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```html +{{ 'prefix-' + null }} + ~~~~~~~~~~~~~~~~ +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```html +{{ 'prefix-' + undefined }} + ~~~~~~~~~~~~~~~~~~~~~ +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```html +{{ 'prefix-' + true }} + ~~~~~~~~~~~~~~~~ +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```html +{{ 'prefix-' + value }} + ~~~~~~~~~~~~~~~~~ +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```html +{{ 'prefix-' + value() }} + ~~~~~~~~~~~~~~~~~~~ +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```html +{{ 'prefix-' + [42] }} + ~~~~~~~~~~~~~~~~ +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```html +{{ 'prefix-' + `prefix-${value}-suffix` }} + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```html +{{ 42 + '-suffix' }} + ~~~~~~~~~~~~~~ +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```html +{{ null + '-suffix' }} + ~~~~~~~~~~~~~~~~ +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```html +{{ undefined + '-suffix' }} + ~~~~~~~~~~~~~~~~~~~~~ +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```html +{{ true + '-suffix' }} + ~~~~~~~~~~~~~~~~ +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```html +{{ value + '-suffix' }} + ~~~~~~~~~~~~~~~~~ +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```html +{{ value() + '-suffix' }} + ~~~~~~~~~~~~~~~~~~~ +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```html +{{ [42] + '-suffix' }} + ~~~~~~~~~~~~~~~~ +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```html +{{ `prefix-${value}-suffix` + '-suffix' }} + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``` + +
+ +
+ +--- + +
+ +
+✅ - Toggle examples of correct code for this rule + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ✅ Valid Code + +```html +{{ `prefix-${value}-suffix` }} +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ✅ Valid Code + +```html +{{ 42 + 42 }} +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ✅ Valid Code + +```html +{{ value + value2 }} +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ✅ Valid Code + +```html +{{ value() + value2() }} +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ✅ Valid Code + +```html +{{ 'text' | pipe }} +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ✅ Valid Code + +```html +@if (`prefix-${value}-suffix`) {} +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ✅ Valid Code + +```html +@defer (when `prefix-${value}-suffix`) {} +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ✅ Valid Code + +```html +

{{ `prefix-${value}-suffix` }}

+``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ✅ Valid Code + +```html + +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ✅ Valid Code + +```html + +``` + +
+ +
diff --git a/packages/eslint-plugin-template/src/configs/all.json b/packages/eslint-plugin-template/src/configs/all.json index 5ff375c0b..063ab4ad3 100644 --- a/packages/eslint-plugin-template/src/configs/all.json +++ b/packages/eslint-plugin-template/src/configs/all.json @@ -28,6 +28,7 @@ "@angular-eslint/template/prefer-ngsrc": "error", "@angular-eslint/template/prefer-self-closing-tags": "error", "@angular-eslint/template/prefer-static-string-properties": "error", + "@angular-eslint/template/prefer-template-literal": "error", "@angular-eslint/template/role-has-required-aria": "error", "@angular-eslint/template/table-scope": "error", "@angular-eslint/template/use-track-by-function": "error", diff --git a/packages/eslint-plugin-template/src/index.ts b/packages/eslint-plugin-template/src/index.ts index c7fa19293..a925d65f4 100644 --- a/packages/eslint-plugin-template/src/index.ts +++ b/packages/eslint-plugin-template/src/index.ts @@ -73,6 +73,9 @@ import preferSelfClosingTags, { import preferStaticStringProperties, { RULE_NAME as preferStaticStringPropertiesRuleName, } from './rules/prefer-static-string-properties'; +import preferTemplateLiteral, { + RULE_NAME as preferTemplateLiteralRuleName, +} from './rules/prefer-template-literal'; import roleHasRequiredAria, { RULE_NAME as roleHasRequiredAriaRuleName, } from './rules/role-has-required-aria'; @@ -119,6 +122,7 @@ export = { [preferSelfClosingTagsRuleName]: preferSelfClosingTags, [preferStaticStringPropertiesRuleName]: preferStaticStringProperties, [preferNgsrcRuleName]: preferNgsrc, + [preferTemplateLiteralRuleName]: preferTemplateLiteral, [roleHasRequiredAriaRuleName]: roleHasRequiredAria, [tableScopeRuleName]: tableScope, [useTrackByFunctionRuleName]: useTrackByFunction, diff --git a/packages/eslint-plugin-template/src/rules/eqeqeq.ts b/packages/eslint-plugin-template/src/rules/eqeqeq.ts index 60779fcc1..060b8ed2d 100644 --- a/packages/eslint-plugin-template/src/rules/eqeqeq.ts +++ b/packages/eslint-plugin-template/src/rules/eqeqeq.ts @@ -8,6 +8,10 @@ import { ensureTemplateParser } from '@angular-eslint/utils'; import type { TSESLint } from '@typescript-eslint/utils'; import { createESLintRule } from '../utils/create-eslint-rule'; import { getNearestNodeFrom } from '../utils/get-nearest-node-from'; +import { + isLiteralPrimitive, + isStringLiteralPrimitive, +} from '../utils/literal-primitive'; export type Options = [{ readonly allowNullOrUndefined?: boolean }]; export type MessageIds = 'eqeqeq' | 'suggestStrictEquality'; @@ -142,10 +146,6 @@ function isASTWithSource(node: unknown): node is ASTWithSource { return node instanceof ASTWithSource; } -function isLiteralPrimitive(node: unknown): node is LiteralPrimitive { - return node instanceof LiteralPrimitive; -} - function isInterpolation(node: unknown): node is Interpolation { return node instanceof Interpolation; } @@ -157,16 +157,10 @@ function isNumeric(value: unknown): value is number | string { ); } -function isString(value: unknown): value is string { - return typeof value === 'string'; -} - function isStringNonNumericValue( ast: AST, ): ast is LiteralPrimitive & { value: string } { - return ( - isLiteralPrimitive(ast) && isString(ast.value) && !isNumeric(ast.value) - ); + return isStringLiteralPrimitive(ast) && !isNumeric(ast.value); } function isNilValue( diff --git a/packages/eslint-plugin-template/src/rules/prefer-template-literal.ts b/packages/eslint-plugin-template/src/rules/prefer-template-literal.ts new file mode 100644 index 000000000..08261f5e2 --- /dev/null +++ b/packages/eslint-plugin-template/src/rules/prefer-template-literal.ts @@ -0,0 +1,129 @@ +import { + AST, + LiteralPrimitive, + TemplateLiteral, + type Binary, +} from '@angular-eslint/bundled-angular-compiler'; +import { ensureTemplateParser } from '@angular-eslint/utils'; +import { createESLintRule } from '../utils/create-eslint-rule'; +import { isStringLiteralPrimitive } from '../utils/literal-primitive'; +import { RuleFix, RuleFixer } from '@typescript-eslint/utils/ts-eslint'; + +const messageId = 'preferTemplateLiteral'; + +export type Options = []; +export type MessageIds = typeof messageId; +export const RULE_NAME = 'prefer-template-literal'; + +export default createESLintRule({ + name: RULE_NAME, + meta: { + type: 'suggestion', + docs: { + description: + 'Ensure that template literals are used instead of concatenating strings or expressions', + }, + fixable: 'code', + schema: [], + messages: { + preferTemplateLiteral: + 'Prefer using template literal instead of concatenating strings or expressions', + }, + }, + defaultOptions: [], + create(context) { + ensureTemplateParser(context); + const { sourceCode } = context; + + return { + 'Binary[operation="+"]'(node: Binary) { + const { left, right } = node; + + const isLeftString = + isStringLiteralPrimitive(left) || left instanceof TemplateLiteral; + const isRightString = + isStringLiteralPrimitive(right) || right instanceof TemplateLiteral; + + // If both sides are not strings, we don't report anything + if (!isLeftString && !isRightString) { + return; + } + + const { + sourceSpan: { start, end }, + } = node; + + context.report({ + loc: { + start: sourceCode.getLocFromIndex(start), + end: sourceCode.getLocFromIndex(end), + }, + messageId, + fix: (fixer) => { + // If both sides are literals, we can just remove the `+` sign and concatenate them + if ( + left instanceof LiteralPrimitive && + right instanceof LiteralPrimitive + ) { + return fixer.replaceTextRange( + [start, end], + `'${left.value}${right.value}'`, + ); + } + + const fixes = new Array(); + + // Fix the left side + fixes.push(...getLeftSideFixs(fixer, left)); + + // Remove the `+` sign + fixes.push( + fixer.removeRange([left.sourceSpan.end, right.sourceSpan.start]), + ); + + // Fix the right side + fixes.push(...getRightSideFixs(fixer, right)); + + return fixes; + }, + }); + }, + }; + }, +}); + +function getLeftSideFixs(fixer: RuleFixer, left: AST): readonly RuleFix[] { + const { start, end } = left.sourceSpan; + + if (left instanceof TemplateLiteral) { + // Remove the end ` sign from the left side + return [fixer.removeRange([end - 1, end])]; + } else if (isStringLiteralPrimitive(left)) { + // Transform left side to template literal + return [fixer.replaceTextRange([start, end], `\`${left.value}`)]; + } else { + // Transform left side to template literal + return [ + fixer.insertTextBeforeRange([start, end], '`${'), + fixer.insertTextAfterRange([start, end], '}'), + ]; + } +} + +function getRightSideFixs(fixer: RuleFixer, right: AST): readonly RuleFix[] { + const { start, end } = right.sourceSpan; + + if (right instanceof TemplateLiteral) { + // Remove the start ` sign from the right side + return [fixer.removeRange([start, start + 1])]; + } else if (isStringLiteralPrimitive(right)) { + // Transform right side to template literal + return [fixer.replaceTextRange([start, end], `${right.value}\``)]; + } else { + // Transform right side to template literal + return [ + fixer.insertTextBeforeRange([start, end], '${'), + fixer.insertTextAfterRange([start, end], '}`'), + ]; + } +} diff --git a/packages/eslint-plugin-template/src/utils/literal-primitive.ts b/packages/eslint-plugin-template/src/utils/literal-primitive.ts new file mode 100644 index 000000000..694ac13ce --- /dev/null +++ b/packages/eslint-plugin-template/src/utils/literal-primitive.ts @@ -0,0 +1,14 @@ +import { + AST, + LiteralPrimitive, +} from '@angular-eslint/bundled-angular-compiler'; + +export function isLiteralPrimitive(node: AST): node is LiteralPrimitive { + return node instanceof LiteralPrimitive; +} + +export function isStringLiteralPrimitive( + node: AST, +): node is Omit & { value: string } { + return isLiteralPrimitive(node) && typeof node.value === 'string'; +} diff --git a/packages/eslint-plugin-template/tests/rules/prefer-template-literal/cases.ts b/packages/eslint-plugin-template/tests/rules/prefer-template-literal/cases.ts new file mode 100644 index 000000000..485df0ecf --- /dev/null +++ b/packages/eslint-plugin-template/tests/rules/prefer-template-literal/cases.ts @@ -0,0 +1,300 @@ +import type { + InvalidTestCase, + ValidTestCase, +} from '@typescript-eslint/rule-tester'; +import type { + MessageIds, + Options, +} from '../../../src/rules/prefer-template-literal'; +import { convertAnnotatedSourceToFailureCase } from '@angular-eslint/test-utils'; + +const messageId: MessageIds = 'preferTemplateLiteral'; + +export const valid: readonly (string | ValidTestCase)[] = [ + '{{ `prefix-${value}-suffix` }}', + '{{ 42 + 42 }}', + '{{ value + value2 }}', + '{{ value() + value2() }}', + "{{ 'text' | pipe }}", + '@if (`prefix-${value}-suffix`) {}', + '@defer (when `prefix-${value}-suffix`) {}', + '

{{ `prefix-${value}-suffix` }}

', + '', + '', +]; + +export const invalid: readonly InvalidTestCase[] = [ + convertAnnotatedSourceToFailureCase({ + messageId, + description: + 'should fail concatenation (left: static string, right: static string)', + annotatedSource: ` + {{ 'prefix-' + '-suffix' }} + ~~~~~~~~~~~~~~~~~~~~~ + `, + annotatedOutput: ` + {{ 'prefix--suffix' }} + + `, + }), + + convertAnnotatedSourceToFailureCase({ + messageId, + description: 'should fail concatenation (left: template, right: template)', + annotatedSource: ` + {{ \`prefix-\${value}-suffix\` + \`-prefix2-\${value2}-suffix2\` }} + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + `, + annotatedOutput: ` + {{ \`prefix-\${value}-suffix-prefix2-\${value2}-suffix2\` }} + + `, + }), + + convertAnnotatedSourceToFailureCase({ + messageId, + description: 'should fail concatenation with binding attribute', + annotatedSource: ` + + ~~~~~~~~~~~~~~~~~~~ + `, + annotatedOutput: ` + + + `, + }), + + convertAnnotatedSourceToFailureCase({ + messageId, + description: 'should fail concatenation with if and pipe', + annotatedSource: ` + @if (value() + '-suffix' | pipe) {} + ~~~~~~~~~~~~~~~~~~~ + `, + annotatedOutput: ` + @if (\`\${value()}-suffix\` | pipe) {} + + `, + }), + + convertAnnotatedSourceToFailureCase({ + messageId, + description: 'should fail concatenation with defer', + annotatedSource: ` + @defer (when value() + '-suffix' | pipe) {} + ~~~~~~~~~~~~~~~~~~~ + `, + annotatedOutput: ` + @defer (when \`\${value()}-suffix\` | pipe) {} + + `, + }), + + // Left : static string + convertAnnotatedSourceToFailureCase({ + messageId, + description: + 'should fail concatenation (left: static string, right: number)', + annotatedSource: ` + {{ 'prefix-' + 42 }} + ~~~~~~~~~~~~~~ + `, + annotatedOutput: ` + {{ 'prefix-42' }} + + `, + }), + convertAnnotatedSourceToFailureCase({ + messageId, + description: 'should fail concatenation (left: static string, right: null)', + annotatedSource: ` + {{ 'prefix-' + null }} + ~~~~~~~~~~~~~~~~ + `, + annotatedOutput: ` + {{ 'prefix-null' }} + + `, + }), + convertAnnotatedSourceToFailureCase({ + messageId, + description: + 'should fail concatenation (left: static string, right: undefined)', + annotatedSource: ` + {{ 'prefix-' + undefined }} + ~~~~~~~~~~~~~~~~~~~~~ + `, + annotatedOutput: ` + {{ 'prefix-undefined' }} + + `, + }), + convertAnnotatedSourceToFailureCase({ + messageId, + description: + 'should fail concatenation (left: static string, right: boolean)', + annotatedSource: ` + {{ 'prefix-' + true }} + ~~~~~~~~~~~~~~~~ + `, + annotatedOutput: ` + {{ 'prefix-true' }} + + `, + }), + convertAnnotatedSourceToFailureCase({ + messageId, + description: + 'should fail concatenation (left: static string, right: property read)', + annotatedSource: ` + {{ 'prefix-' + value }} + ~~~~~~~~~~~~~~~~~ + `, + annotatedOutput: ` + {{ \`prefix-\${value}\` }} + + `, + }), + convertAnnotatedSourceToFailureCase({ + messageId, + description: 'should fail concatenation (left: static string, right: call)', + annotatedSource: ` + {{ 'prefix-' + value() }} + ~~~~~~~~~~~~~~~~~~~ + `, + annotatedOutput: ` + {{ \`prefix-\${value()}\` }} + + `, + }), + convertAnnotatedSourceToFailureCase({ + messageId, + description: + 'should fail concatenation (left: static string, right: array)', + annotatedSource: ` + {{ 'prefix-' + [42] }} + ~~~~~~~~~~~~~~~~ + `, + annotatedOutput: ` + {{ \`prefix-\${[42]}\` }} + + `, + }), + convertAnnotatedSourceToFailureCase({ + messageId, + description: + 'should fail concatenation (left: static string, right: template)', + annotatedSource: ` + {{ 'prefix-' + \`prefix-\${value}-suffix\` }} + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + `, + annotatedOutput: ` + {{ \`prefix-prefix-\${value}-suffix\` }} + + `, + }), + + // Right : static string + convertAnnotatedSourceToFailureCase({ + messageId, + description: + 'should fail concatenation (left: number, right: static string)', + annotatedSource: ` + {{ 42 + '-suffix' }} + ~~~~~~~~~~~~~~ + `, + annotatedOutput: ` + {{ '42-suffix' }} + + `, + }), + convertAnnotatedSourceToFailureCase({ + messageId, + description: 'should fail concatenation (left: null, right: static string)', + annotatedSource: ` + {{ null + '-suffix' }} + ~~~~~~~~~~~~~~~~ + `, + annotatedOutput: ` + {{ 'null-suffix' }} + + `, + }), + convertAnnotatedSourceToFailureCase({ + messageId, + description: + 'should fail concatenation (left: undefined, right: static string)', + annotatedSource: ` + {{ undefined + '-suffix' }} + ~~~~~~~~~~~~~~~~~~~~~ + `, + annotatedOutput: ` + {{ 'undefined-suffix' }} + + `, + }), + convertAnnotatedSourceToFailureCase({ + messageId, + description: + 'should fail concatenation (left: boolean, right: static string)', + annotatedSource: ` + {{ true + '-suffix' }} + ~~~~~~~~~~~~~~~~ + `, + annotatedOutput: ` + {{ 'true-suffix' }} + + `, + }), + convertAnnotatedSourceToFailureCase({ + messageId, + description: + 'should fail concatenation (left: property read, right: static string)', + annotatedSource: ` + {{ value + '-suffix' }} + ~~~~~~~~~~~~~~~~~ + `, + annotatedOutput: ` + {{ \`\${value}-suffix\` }} + + `, + }), + convertAnnotatedSourceToFailureCase({ + messageId, + description: 'should fail concatenation (left: call, right: static string)', + annotatedSource: ` + {{ value() + '-suffix' }} + ~~~~~~~~~~~~~~~~~~~ + `, + annotatedOutput: ` + {{ \`\${value()}-suffix\` }} + + `, + }), + convertAnnotatedSourceToFailureCase({ + messageId, + description: + 'should fail concatenation (left: array, right: static string)', + annotatedSource: ` + {{ [42] + '-suffix' }} + ~~~~~~~~~~~~~~~~ + `, + annotatedOutput: ` + {{ \`\${[42]}-suffix\` }} + + `, + }), + convertAnnotatedSourceToFailureCase({ + messageId, + description: + 'should fail concatenation (left: template, right: static string)', + annotatedSource: ` + {{ \`prefix-\${value}-suffix\` + '-suffix' }} + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + `, + annotatedOutput: ` + {{ \`prefix-\${value}-suffix-suffix\` }} + + `, + }), +]; diff --git a/packages/eslint-plugin-template/tests/rules/prefer-template-literal/spec.ts b/packages/eslint-plugin-template/tests/rules/prefer-template-literal/spec.ts new file mode 100644 index 000000000..3cddbd990 --- /dev/null +++ b/packages/eslint-plugin-template/tests/rules/prefer-template-literal/spec.ts @@ -0,0 +1,14 @@ +import { RuleTester } from '@angular-eslint/test-utils'; +import rule, { RULE_NAME } from '../../../src/rules/prefer-template-literal'; +import { invalid, valid } from './cases'; + +const ruleTester = new RuleTester({ + languageOptions: { + parser: require('@angular-eslint/template-parser'), + }, +}); + +ruleTester.run(RULE_NAME, rule, { + valid, + invalid, +}); From 1ef1f39682b73a72b9fb66b0a5b764ca67f6f2a7 Mon Sep 17 00:00:00 2001 From: Guillaume Drouard Date: Mon, 10 Mar 2025 15:25:35 +0100 Subject: [PATCH 2/8] fix(prefer-template-literals): typo --- .../src/rules/prefer-template-literal.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/eslint-plugin-template/src/rules/prefer-template-literal.ts b/packages/eslint-plugin-template/src/rules/prefer-template-literal.ts index 08261f5e2..88a5473b9 100644 --- a/packages/eslint-plugin-template/src/rules/prefer-template-literal.ts +++ b/packages/eslint-plugin-template/src/rules/prefer-template-literal.ts @@ -74,7 +74,7 @@ export default createESLintRule({ const fixes = new Array(); // Fix the left side - fixes.push(...getLeftSideFixs(fixer, left)); + fixes.push(...getLeftSideFixes(fixer, left)); // Remove the `+` sign fixes.push( @@ -82,7 +82,7 @@ export default createESLintRule({ ); // Fix the right side - fixes.push(...getRightSideFixs(fixer, right)); + fixes.push(...getRightSideFixes(fixer, right)); return fixes; }, @@ -92,7 +92,7 @@ export default createESLintRule({ }, }); -function getLeftSideFixs(fixer: RuleFixer, left: AST): readonly RuleFix[] { +function getLeftSideFixes(fixer: RuleFixer, left: AST): readonly RuleFix[] { const { start, end } = left.sourceSpan; if (left instanceof TemplateLiteral) { @@ -110,7 +110,7 @@ function getLeftSideFixs(fixer: RuleFixer, left: AST): readonly RuleFix[] { } } -function getRightSideFixs(fixer: RuleFixer, right: AST): readonly RuleFix[] { +function getRightSideFixes(fixer: RuleFixer, right: AST): readonly RuleFix[] { const { start, end } = right.sourceSpan; if (right instanceof TemplateLiteral) { From ea21d6f5a0dfccb5118a581746541bf9e83b78d3 Mon Sep 17 00:00:00 2001 From: Guillaume Drouard Date: Sun, 16 Mar 2025 21:55:48 +0100 Subject: [PATCH 3/8] fix(prefer-template-literal): add escape backquote when necessary --- .../src/rules/prefer-template-literal.ts | 4 ++-- .../rules/prefer-template-literal/cases.ts | 17 ++++++++++------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/packages/eslint-plugin-template/src/rules/prefer-template-literal.ts b/packages/eslint-plugin-template/src/rules/prefer-template-literal.ts index 88a5473b9..6ea3e1ea2 100644 --- a/packages/eslint-plugin-template/src/rules/prefer-template-literal.ts +++ b/packages/eslint-plugin-template/src/rules/prefer-template-literal.ts @@ -100,7 +100,7 @@ function getLeftSideFixes(fixer: RuleFixer, left: AST): readonly RuleFix[] { return [fixer.removeRange([end - 1, end])]; } else if (isStringLiteralPrimitive(left)) { // Transform left side to template literal - return [fixer.replaceTextRange([start, end], `\`${left.value}`)]; + return [fixer.replaceTextRange([start, end], `\`${left.value.replaceAll('`', '\\`')}`)]; } else { // Transform left side to template literal return [ @@ -118,7 +118,7 @@ function getRightSideFixes(fixer: RuleFixer, right: AST): readonly RuleFix[] { return [fixer.removeRange([start, start + 1])]; } else if (isStringLiteralPrimitive(right)) { // Transform right side to template literal - return [fixer.replaceTextRange([start, end], `${right.value}\``)]; + return [fixer.replaceTextRange([start, end], `${right.value.replaceAll('`', '\\`')}\``)]; } else { // Transform right side to template literal return [ diff --git a/packages/eslint-plugin-template/tests/rules/prefer-template-literal/cases.ts b/packages/eslint-plugin-template/tests/rules/prefer-template-literal/cases.ts index 485df0ecf..ddc26cd4b 100644 --- a/packages/eslint-plugin-template/tests/rules/prefer-template-literal/cases.ts +++ b/packages/eslint-plugin-template/tests/rules/prefer-template-literal/cases.ts @@ -15,10 +15,13 @@ export const valid: readonly (string | ValidTestCase)[] = [ '{{ 42 + 42 }}', '{{ value + value2 }}', '{{ value() + value2() }}', - "{{ 'text' | pipe }}", + "{{ 'simple-quote' | pipe }}", + '{{ "double-quote" }}"', + '{{ `backquote` }}', '@if (`prefix-${value}-suffix`) {}', '@defer (when `prefix-${value}-suffix`) {}', '

{{ `prefix-${value}-suffix` }}

', + '', '', '', ]; @@ -185,11 +188,11 @@ export const invalid: readonly InvalidTestCase[] = [ description: 'should fail concatenation (left: static string, right: template)', annotatedSource: ` - {{ 'prefix-' + \`prefix-\${value}-suffix\` }} - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + {{ 'pre\`fix-' + \`'pre\\\`fix"-\${value}-"suf\\\`fix'\` }} + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ `, annotatedOutput: ` - {{ \`prefix-prefix-\${value}-suffix\` }} + {{ \`pre\\\`fix-'pre\\\`fix"-\${value}-"suf\\\`fix'\` }} `, }), @@ -289,11 +292,11 @@ export const invalid: readonly InvalidTestCase[] = [ description: 'should fail concatenation (left: template, right: static string)', annotatedSource: ` - {{ \`prefix-\${value}-suffix\` + '-suffix' }} - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + {{ \`'pre\\\`fix"-\${value}-"suf\\\`fix'\` + '-suf\`fix' }} + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ `, annotatedOutput: ` - {{ \`prefix-\${value}-suffix-suffix\` }} + {{ \`'pre\\\`fix"-\${value}-"suf\\\`fix'-suf\\\`fix\` }} `, }), From 60f80a5529bf24552cc47434840fe7329d3afb0a Mon Sep 17 00:00:00 2001 From: Guillaume Drouard Date: Tue, 18 Mar 2025 00:44:31 +0100 Subject: [PATCH 4/8] fix(prefer-template-literal): fix double quote and fix template concat with literal primitive --- .../src/rules/prefer-template-literal.ts | 30 +- .../src/utils/literal-primitive.ts | 4 + .../rules/prefer-template-literal/cases.ts | 490 +++++++++++++++++- 3 files changed, 490 insertions(+), 34 deletions(-) diff --git a/packages/eslint-plugin-template/src/rules/prefer-template-literal.ts b/packages/eslint-plugin-template/src/rules/prefer-template-literal.ts index 6ea3e1ea2..e55367b65 100644 --- a/packages/eslint-plugin-template/src/rules/prefer-template-literal.ts +++ b/packages/eslint-plugin-template/src/rules/prefer-template-literal.ts @@ -6,7 +6,7 @@ import { } from '@angular-eslint/bundled-angular-compiler'; import { ensureTemplateParser } from '@angular-eslint/utils'; import { createESLintRule } from '../utils/create-eslint-rule'; -import { isStringLiteralPrimitive } from '../utils/literal-primitive'; +import { getLiteralPrimitiveStringValue, isLiteralPrimitive, isStringLiteralPrimitive } from '../utils/literal-primitive'; import { RuleFix, RuleFixer } from '@typescript-eslint/utils/ts-eslint'; const messageId = 'preferTemplateLiteral'; @@ -53,6 +53,18 @@ export default createESLintRule({ sourceSpan: { start, end }, } = node; + function getQuote(): '"' | "'" | '`' { + const leftValue = sourceCode.text.at(left.sourceSpan.start); + if (leftValue === "'" || leftValue === '"') { + return leftValue; + } + const rightValue = sourceCode.text.at(right.sourceSpan.start); + if (rightValue === "'" || rightValue === '"') { + return rightValue; + } + return '`'; + } + context.report({ loc: { start: sourceCode.getLocFromIndex(start), @@ -60,14 +72,16 @@ export default createESLintRule({ }, messageId, fix: (fixer) => { - // If both sides are literals, we can just remove the `+` sign and concatenate them + + // If both sides are literals, we remove the `+` sign, escape if necessary and concatenate them if ( left instanceof LiteralPrimitive && right instanceof LiteralPrimitive ) { + const quote = getQuote(); return fixer.replaceTextRange( [start, end], - `'${left.value}${right.value}'`, + `${quote}${getLiteralPrimitiveStringValue(left, quote)}${getLiteralPrimitiveStringValue(right, quote)}${quote}`, ); } @@ -98,9 +112,9 @@ function getLeftSideFixes(fixer: RuleFixer, left: AST): readonly RuleFix[] { if (left instanceof TemplateLiteral) { // Remove the end ` sign from the left side return [fixer.removeRange([end - 1, end])]; - } else if (isStringLiteralPrimitive(left)) { + } else if (isLiteralPrimitive(left)) { // Transform left side to template literal - return [fixer.replaceTextRange([start, end], `\`${left.value.replaceAll('`', '\\`')}`)]; + return [fixer.replaceTextRange([start, end], `\`${getLiteralPrimitiveStringValue(left, '`')}`)]; } else { // Transform left side to template literal return [ @@ -116,9 +130,9 @@ function getRightSideFixes(fixer: RuleFixer, right: AST): readonly RuleFix[] { if (right instanceof TemplateLiteral) { // Remove the start ` sign from the right side return [fixer.removeRange([start, start + 1])]; - } else if (isStringLiteralPrimitive(right)) { - // Transform right side to template literal - return [fixer.replaceTextRange([start, end], `${right.value.replaceAll('`', '\\`')}\``)]; + } else if (isLiteralPrimitive(right)) { + // Transform right side to template literal if it's a string + return [fixer.replaceTextRange([start, end], `${getLiteralPrimitiveStringValue(right, '`')}\``)]; } else { // Transform right side to template literal return [ diff --git a/packages/eslint-plugin-template/src/utils/literal-primitive.ts b/packages/eslint-plugin-template/src/utils/literal-primitive.ts index 694ac13ce..057edff1b 100644 --- a/packages/eslint-plugin-template/src/utils/literal-primitive.ts +++ b/packages/eslint-plugin-template/src/utils/literal-primitive.ts @@ -12,3 +12,7 @@ export function isStringLiteralPrimitive( ): node is Omit & { value: string } { return isLiteralPrimitive(node) && typeof node.value === 'string'; } + +export function getLiteralPrimitiveStringValue(node: LiteralPrimitive, quote: "'" | '"' | '`'): string { + return typeof node.value === 'string' ? `${node.value.replaceAll(quote, `\\${quote}`)}` : String(node.value); +} \ No newline at end of file diff --git a/packages/eslint-plugin-template/tests/rules/prefer-template-literal/cases.ts b/packages/eslint-plugin-template/tests/rules/prefer-template-literal/cases.ts index ddc26cd4b..118a925f1 100644 --- a/packages/eslint-plugin-template/tests/rules/prefer-template-literal/cases.ts +++ b/packages/eslint-plugin-template/tests/rules/prefer-template-literal/cases.ts @@ -30,13 +30,27 @@ export const invalid: readonly InvalidTestCase[] = [ convertAnnotatedSourceToFailureCase({ messageId, description: - 'should fail concatenation (left: static string, right: static string)', + 'should fail concatenation (left: simple quote, right: simple quote)', annotatedSource: ` - {{ 'prefix-' + '-suffix' }} - ~~~~~~~~~~~~~~~~~~~~~ + {{ 'pre"fix-' + '-suf\\'fix' }} + ~~~~~~~~~~~~~~~~~~~~~~~~ `, annotatedOutput: ` - {{ 'prefix--suffix' }} + {{ 'pre"fix--suf\\'fix' }} + + `, + }), + + convertAnnotatedSourceToFailureCase({ + messageId, + description: + 'should fail concatenation (left: double quote, right: double quote)', + annotatedSource: ` + {{ "pre'fix-" + "-suf\\"fix" }} + ~~~~~~~~~~~~~~~~~~~~~~~~ + `, + annotatedOutput: ` + {{ "pre'fix--suf\\"fix" }} `, }), @@ -54,6 +68,62 @@ export const invalid: readonly InvalidTestCase[] = [ `, }), + convertAnnotatedSourceToFailureCase({ + messageId, + description: + 'should fail concatenation (left: simple quote, right: double quote)', + annotatedSource: ` + {{ 'pre"fix-' + "-suf'fix" }} + ~~~~~~~~~~~~~~~~~~~~~~~ + `, + annotatedOutput: ` + {{ 'pre"fix--suf\\'fix' }} + + `, + }), + + convertAnnotatedSourceToFailureCase({ + messageId, + description: + 'should fail concatenation (left: simple quote, right: template)', + annotatedSource: ` + {{ 'pre\`fix-' + \`'pre\\\`fix"-\${value}-"suf\\\`fix'\` }} + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + `, + annotatedOutput: ` + {{ \`pre\\\`fix-'pre\\\`fix"-\${value}-"suf\\\`fix'\` }} + + `, + }), + + convertAnnotatedSourceToFailureCase({ + messageId, + description: + 'should fail concatenation (left: double quote, right: simple quote)', + annotatedSource: ` + {{ "pre'fix-" + '-suf"fix' }} + ~~~~~~~~~~~~~~~~~~~~~~~ + `, + annotatedOutput: ` + {{ "pre'fix--suf\\"fix" }} + + `, + }), + + convertAnnotatedSourceToFailureCase({ + messageId, + description: + 'should fail concatenation (left: double quote, right: template)', + annotatedSource: ` + {{ "pre\`fix-" + \`'pre\\\`fix"-\${value}-"suf\\\`fix'\` }} + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + `, + annotatedOutput: ` + {{ \`pre\\\`fix-'pre\\\`fix"-\${value}-"suf\\\`fix'\` }} + + `, + }), + convertAnnotatedSourceToFailureCase({ messageId, description: 'should fail concatenation with binding attribute', @@ -71,7 +141,7 @@ export const invalid: readonly InvalidTestCase[] = [ messageId, description: 'should fail concatenation with if and pipe', annotatedSource: ` - @if (value() + '-suffix' | pipe) {} + @if (value() + "-suffix" | pipe) {} ~~~~~~~~~~~~~~~~~~~ `, annotatedOutput: ` @@ -93,11 +163,11 @@ export const invalid: readonly InvalidTestCase[] = [ `, }), - // Left : static string + // Left : simple quote convertAnnotatedSourceToFailureCase({ messageId, description: - 'should fail concatenation (left: static string, right: number)', + 'should fail concatenation (left: simple quote, right: number)', annotatedSource: ` {{ 'prefix-' + 42 }} ~~~~~~~~~~~~~~ @@ -109,7 +179,7 @@ export const invalid: readonly InvalidTestCase[] = [ }), convertAnnotatedSourceToFailureCase({ messageId, - description: 'should fail concatenation (left: static string, right: null)', + description: 'should fail concatenation (left: simple quote, right: null)', annotatedSource: ` {{ 'prefix-' + null }} ~~~~~~~~~~~~~~~~ @@ -122,7 +192,7 @@ export const invalid: readonly InvalidTestCase[] = [ convertAnnotatedSourceToFailureCase({ messageId, description: - 'should fail concatenation (left: static string, right: undefined)', + 'should fail concatenation (left: simple quote, right: undefined)', annotatedSource: ` {{ 'prefix-' + undefined }} ~~~~~~~~~~~~~~~~~~~~~ @@ -135,7 +205,7 @@ export const invalid: readonly InvalidTestCase[] = [ convertAnnotatedSourceToFailureCase({ messageId, description: - 'should fail concatenation (left: static string, right: boolean)', + 'should fail concatenation (left: simple quote, right: boolean)', annotatedSource: ` {{ 'prefix-' + true }} ~~~~~~~~~~~~~~~~ @@ -148,7 +218,7 @@ export const invalid: readonly InvalidTestCase[] = [ convertAnnotatedSourceToFailureCase({ messageId, description: - 'should fail concatenation (left: static string, right: property read)', + 'should fail concatenation (left: simple quote, right: property read)', annotatedSource: ` {{ 'prefix-' + value }} ~~~~~~~~~~~~~~~~~ @@ -160,7 +230,7 @@ export const invalid: readonly InvalidTestCase[] = [ }), convertAnnotatedSourceToFailureCase({ messageId, - description: 'should fail concatenation (left: static string, right: call)', + description: 'should fail concatenation (left: simple quote, right: call)', annotatedSource: ` {{ 'prefix-' + value() }} ~~~~~~~~~~~~~~~~~~~ @@ -173,7 +243,7 @@ export const invalid: readonly InvalidTestCase[] = [ convertAnnotatedSourceToFailureCase({ messageId, description: - 'should fail concatenation (left: static string, right: array)', + 'should fail concatenation (left: simple quote, right: array)', annotatedSource: ` {{ 'prefix-' + [42] }} ~~~~~~~~~~~~~~~~ @@ -183,25 +253,196 @@ export const invalid: readonly InvalidTestCase[] = [ `, }), + + // Left : double quote convertAnnotatedSourceToFailureCase({ messageId, description: - 'should fail concatenation (left: static string, right: template)', + 'should fail concatenation (left: double quote, right: number)', annotatedSource: ` - {{ 'pre\`fix-' + \`'pre\\\`fix"-\${value}-"suf\\\`fix'\` }} - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + {{ "prefix-" + 42 }} + ~~~~~~~~~~~~~~ `, annotatedOutput: ` - {{ \`pre\\\`fix-'pre\\\`fix"-\${value}-"suf\\\`fix'\` }} + {{ "prefix-42" }} + + `, + }), + convertAnnotatedSourceToFailureCase({ + messageId, + description: 'should fail concatenation (left: double quote, right: null)', + annotatedSource: ` + {{ "prefix-" + null }} + ~~~~~~~~~~~~~~~~ + `, + annotatedOutput: ` + {{ "prefix-null" }} + + `, + }), + convertAnnotatedSourceToFailureCase({ + messageId, + description: + 'should fail concatenation (left: double quote, right: undefined)', + annotatedSource: ` + {{ "prefix-" + undefined }} + ~~~~~~~~~~~~~~~~~~~~~ + `, + annotatedOutput: ` + {{ "prefix-undefined" }} + + `, + }), + convertAnnotatedSourceToFailureCase({ + messageId, + description: + 'should fail concatenation (left: double quote, right: boolean)', + annotatedSource: ` + {{ "prefix-" + true }} + ~~~~~~~~~~~~~~~~ + `, + annotatedOutput: ` + {{ "prefix-true" }} + + `, + }), + convertAnnotatedSourceToFailureCase({ + messageId, + description: + 'should fail concatenation (left: double quote, right: property read)', + annotatedSource: ` + {{ "prefix-" + value }} + ~~~~~~~~~~~~~~~~~ + `, + annotatedOutput: ` + {{ \`prefix-\${value}\` }} + + `, + }), + convertAnnotatedSourceToFailureCase({ + messageId, + description: 'should fail concatenation (left: double quote, right: call)', + annotatedSource: ` + {{ "prefix-" + value() }} + ~~~~~~~~~~~~~~~~~~~ + `, + annotatedOutput: ` + {{ \`prefix-\${value()}\` }} + + `, + }), + convertAnnotatedSourceToFailureCase({ + messageId, + description: + 'should fail concatenation (left: double quote, right: array)', + annotatedSource: ` + {{ "prefix-" + [42] }} + ~~~~~~~~~~~~~~~~ + `, + annotatedOutput: ` + {{ \`prefix-\${[42]}\` }} `, }), - // Right : static string + // Left : template convertAnnotatedSourceToFailureCase({ messageId, description: - 'should fail concatenation (left: number, right: static string)', + 'should fail concatenation (left: template, right: number)', + annotatedSource: ` + {{ \`prefix-\${value}-suffix\` + 42 }} + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + `, + annotatedOutput: ` + {{ \`prefix-\${value}-suffix42\` }} + + `, + }), + convertAnnotatedSourceToFailureCase({ + messageId, + description: + 'should fail concatenation (left: template, right: null)', + annotatedSource: ` + {{ \`prefix-\${value}-suffix\` + null }} + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + `, + annotatedOutput: ` + {{ \`prefix-\${value}-suffixnull\` }} + + `, + }), + convertAnnotatedSourceToFailureCase({ + messageId, + description: + 'should fail concatenation (left: template, right: undefined)', + annotatedSource: ` + {{ \`prefix-\${value}-suffix\` + undefined }} + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + `, + annotatedOutput: ` + {{ \`prefix-\${value}-suffixundefined\` }} + + `, + }), + convertAnnotatedSourceToFailureCase({ + messageId, + description: + 'should fail concatenation (left: template, right: boolean)', + annotatedSource: ` + {{ \`prefix-\${value}-suffix\` + false }} + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + `, + annotatedOutput: ` + {{ \`prefix-\${value}-suffixfalse\` }} + + `, + }), + convertAnnotatedSourceToFailureCase({ + messageId, + description: + 'should fail concatenation (left: template, right: property read)', + annotatedSource: ` + {{ \`prefix-\${value}-suffix\` + value2 }} + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + `, + annotatedOutput: ` + {{ \`prefix-\${value}-suffix\${value2}\` }} + + `, + }), + convertAnnotatedSourceToFailureCase({ + messageId, + description: + 'should fail concatenation (left: template, right: call)', + annotatedSource: ` + {{ \`prefix-\${value}-suffix\` + value2() }} + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + `, + annotatedOutput: ` + {{ \`prefix-\${value}-suffix\${value2()}\` }} + + `, + }), + convertAnnotatedSourceToFailureCase({ + messageId, + description: + 'should fail concatenation (left: template, right: array)', + annotatedSource: ` + {{ \`prefix-\${value}-suffix\` + [42] }} + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + `, + annotatedOutput: ` + {{ \`prefix-\${value}-suffix\${[42]}\` }} + + `, + }), + + // Right : simple quote + convertAnnotatedSourceToFailureCase({ + messageId, + description: + 'should fail concatenation (left: number, right: simple quote)', annotatedSource: ` {{ 42 + '-suffix' }} ~~~~~~~~~~~~~~ @@ -213,7 +454,7 @@ export const invalid: readonly InvalidTestCase[] = [ }), convertAnnotatedSourceToFailureCase({ messageId, - description: 'should fail concatenation (left: null, right: static string)', + description: 'should fail concatenation (left: null, right: simple quote)', annotatedSource: ` {{ null + '-suffix' }} ~~~~~~~~~~~~~~~~ @@ -226,7 +467,7 @@ export const invalid: readonly InvalidTestCase[] = [ convertAnnotatedSourceToFailureCase({ messageId, description: - 'should fail concatenation (left: undefined, right: static string)', + 'should fail concatenation (left: undefined, right: simple quote)', annotatedSource: ` {{ undefined + '-suffix' }} ~~~~~~~~~~~~~~~~~~~~~ @@ -239,7 +480,7 @@ export const invalid: readonly InvalidTestCase[] = [ convertAnnotatedSourceToFailureCase({ messageId, description: - 'should fail concatenation (left: boolean, right: static string)', + 'should fail concatenation (left: boolean, right: simple quote)', annotatedSource: ` {{ true + '-suffix' }} ~~~~~~~~~~~~~~~~ @@ -252,7 +493,7 @@ export const invalid: readonly InvalidTestCase[] = [ convertAnnotatedSourceToFailureCase({ messageId, description: - 'should fail concatenation (left: property read, right: static string)', + 'should fail concatenation (left: property read, right: simple quote)', annotatedSource: ` {{ value + '-suffix' }} ~~~~~~~~~~~~~~~~~ @@ -264,7 +505,7 @@ export const invalid: readonly InvalidTestCase[] = [ }), convertAnnotatedSourceToFailureCase({ messageId, - description: 'should fail concatenation (left: call, right: static string)', + description: 'should fail concatenation (left: call, right: simple quote)', annotatedSource: ` {{ value() + '-suffix' }} ~~~~~~~~~~~~~~~~~~~ @@ -277,7 +518,7 @@ export const invalid: readonly InvalidTestCase[] = [ convertAnnotatedSourceToFailureCase({ messageId, description: - 'should fail concatenation (left: array, right: static string)', + 'should fail concatenation (left: array, right: simple quote)', annotatedSource: ` {{ [42] + '-suffix' }} ~~~~~~~~~~~~~~~~ @@ -290,7 +531,7 @@ export const invalid: readonly InvalidTestCase[] = [ convertAnnotatedSourceToFailureCase({ messageId, description: - 'should fail concatenation (left: template, right: static string)', + 'should fail concatenation (left: template, right: simple quote)', annotatedSource: ` {{ \`'pre\\\`fix"-\${value}-"suf\\\`fix'\` + '-suf\`fix' }} ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -300,4 +541,201 @@ export const invalid: readonly InvalidTestCase[] = [ `, }), + + // Right : double quote + convertAnnotatedSourceToFailureCase({ + messageId, + description: + 'should fail concatenation (left: number, right: double quote)', + annotatedSource: ` + {{ 42 + "-suffix" }} + ~~~~~~~~~~~~~~ + `, + annotatedOutput: ` + {{ "42-suffix" }} + + `, + }), + convertAnnotatedSourceToFailureCase({ + messageId, + description: 'should fail concatenation (left: null, right: double quote)', + annotatedSource: ` + {{ null + "-suffix" }} + ~~~~~~~~~~~~~~~~ + `, + annotatedOutput: ` + {{ "null-suffix" }} + + `, + }), + convertAnnotatedSourceToFailureCase({ + messageId, + description: + 'should fail concatenation (left: undefined, right: double quote)', + annotatedSource: ` + {{ undefined + "-suffix" }} + ~~~~~~~~~~~~~~~~~~~~~ + `, + annotatedOutput: ` + {{ "undefined-suffix" }} + + `, + }), + convertAnnotatedSourceToFailureCase({ + messageId, + description: + 'should fail concatenation (left: boolean, right: double quote)', + annotatedSource: ` + {{ true + "-suffix" }} + ~~~~~~~~~~~~~~~~ + `, + annotatedOutput: ` + {{ "true-suffix" }} + + `, + }), + convertAnnotatedSourceToFailureCase({ + messageId, + description: + 'should fail concatenation (left: property read, right: double quote)', + annotatedSource: ` + {{ value + "-suffix" }} + ~~~~~~~~~~~~~~~~~ + `, + annotatedOutput: ` + {{ \`\${value}-suffix\` }} + + `, + }), + convertAnnotatedSourceToFailureCase({ + messageId, + description: 'should fail concatenation (left: call, right: double quote)', + annotatedSource: ` + {{ value() + "-suffix" }} + ~~~~~~~~~~~~~~~~~~~ + `, + annotatedOutput: ` + {{ \`\${value()}-suffix\` }} + + `, + }), + convertAnnotatedSourceToFailureCase({ + messageId, + description: + 'should fail concatenation (left: array, right: double quote)', + annotatedSource: ` + {{ [42] + "-suffix" }} + ~~~~~~~~~~~~~~~~ + `, + annotatedOutput: ` + {{ \`\${[42]}-suffix\` }} + + `, + }), + convertAnnotatedSourceToFailureCase({ + messageId, + description: + 'should fail concatenation (left: template, right: double quote)', + annotatedSource: ` + {{ \`'pre\\\`fix"-\${value}-"suf\\\`fix'\` + "-suf\`fix" }} + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + `, + annotatedOutput: ` + {{ \`'pre\\\`fix"-\${value}-"suf\\\`fix'-suf\\\`fix\` }} + + `, + }), + + // Right : template + convertAnnotatedSourceToFailureCase({ + messageId, + description: + 'should fail concatenation (left: number, right: template)', + annotatedSource: ` + {{ 42 + \`prefix-\${value}-suffix\` }} + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + `, + annotatedOutput: ` + {{ \`42prefix-\${value}-suffix\` }} + + `, + }), + convertAnnotatedSourceToFailureCase({ + messageId, + description: + 'should fail concatenation (left: null, right: template)', + annotatedSource: ` + {{ null + \`prefix-\${value}-suffix\` }} + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + `, + annotatedOutput: ` + {{ \`nullprefix-\${value}-suffix\` }} + + `, + }), + convertAnnotatedSourceToFailureCase({ + messageId, + description: + 'should fail concatenation (left: undefined, right: template)', + annotatedSource: ` + {{ undefined + \`prefix-\${value}-suffix\` }} + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + `, + annotatedOutput: ` + {{ \`undefinedprefix-\${value}-suffix\` }} + + `, + }), + convertAnnotatedSourceToFailureCase({ + messageId, + description: + 'should fail concatenation (left: boolean, right: template)', + annotatedSource: ` + {{ false + \`prefix-\${value}-suffix\` }} + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + `, + annotatedOutput: ` + {{ \`falseprefix-\${value}-suffix\` }} + + `, + }), + convertAnnotatedSourceToFailureCase({ + messageId, + description: + 'should fail concatenation (left: property read, right: template)', + annotatedSource: ` + {{ value2 + \`prefix-\${value}-suffix\` }} + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + `, + annotatedOutput: ` + {{ \`\${value2}prefix-\${value}-suffix\` }} + + `, + }), + convertAnnotatedSourceToFailureCase({ + messageId, + description: + 'should fail concatenation (left: call, right: template)', + annotatedSource: ` + {{ value2() + \`prefix-\${value}-suffix\` }} + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + `, + annotatedOutput: ` + {{ \`\${value2()}prefix-\${value}-suffix\` }} + + `, + }), + convertAnnotatedSourceToFailureCase({ + messageId, + description: + 'should fail concatenation (left: array, right: template)', + annotatedSource: ` + {{ [42] + \`prefix-\${value}-suffix\` }} + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + `, + annotatedOutput: ` + {{ \`\${[42]}prefix-\${value}-suffix\` }} + + `, + }), ]; From 9391c14543b487919a114aaf6f9109fe9c735e65 Mon Sep 17 00:00:00 2001 From: Guillaume Drouard Date: Fri, 21 Mar 2025 22:19:41 +0100 Subject: [PATCH 5/8] fix: format --- .../src/rules/prefer-template-literal.ts | 21 ++++++-- .../src/utils/literal-primitive.ts | 11 ++-- .../rules/prefer-template-literal/cases.ts | 52 +++++++------------ 3 files changed, 43 insertions(+), 41 deletions(-) diff --git a/packages/eslint-plugin-template/src/rules/prefer-template-literal.ts b/packages/eslint-plugin-template/src/rules/prefer-template-literal.ts index e55367b65..e07b959eb 100644 --- a/packages/eslint-plugin-template/src/rules/prefer-template-literal.ts +++ b/packages/eslint-plugin-template/src/rules/prefer-template-literal.ts @@ -6,7 +6,11 @@ import { } from '@angular-eslint/bundled-angular-compiler'; import { ensureTemplateParser } from '@angular-eslint/utils'; import { createESLintRule } from '../utils/create-eslint-rule'; -import { getLiteralPrimitiveStringValue, isLiteralPrimitive, isStringLiteralPrimitive } from '../utils/literal-primitive'; +import { + getLiteralPrimitiveStringValue, + isLiteralPrimitive, + isStringLiteralPrimitive, +} from '../utils/literal-primitive'; import { RuleFix, RuleFixer } from '@typescript-eslint/utils/ts-eslint'; const messageId = 'preferTemplateLiteral'; @@ -72,7 +76,6 @@ export default createESLintRule({ }, messageId, fix: (fixer) => { - // If both sides are literals, we remove the `+` sign, escape if necessary and concatenate them if ( left instanceof LiteralPrimitive && @@ -114,7 +117,12 @@ function getLeftSideFixes(fixer: RuleFixer, left: AST): readonly RuleFix[] { return [fixer.removeRange([end - 1, end])]; } else if (isLiteralPrimitive(left)) { // Transform left side to template literal - return [fixer.replaceTextRange([start, end], `\`${getLiteralPrimitiveStringValue(left, '`')}`)]; + return [ + fixer.replaceTextRange( + [start, end], + `\`${getLiteralPrimitiveStringValue(left, '`')}`, + ), + ]; } else { // Transform left side to template literal return [ @@ -132,7 +140,12 @@ function getRightSideFixes(fixer: RuleFixer, right: AST): readonly RuleFix[] { return [fixer.removeRange([start, start + 1])]; } else if (isLiteralPrimitive(right)) { // Transform right side to template literal if it's a string - return [fixer.replaceTextRange([start, end], `${getLiteralPrimitiveStringValue(right, '`')}\``)]; + return [ + fixer.replaceTextRange( + [start, end], + `${getLiteralPrimitiveStringValue(right, '`')}\``, + ), + ]; } else { // Transform right side to template literal return [ diff --git a/packages/eslint-plugin-template/src/utils/literal-primitive.ts b/packages/eslint-plugin-template/src/utils/literal-primitive.ts index 057edff1b..337be06ca 100644 --- a/packages/eslint-plugin-template/src/utils/literal-primitive.ts +++ b/packages/eslint-plugin-template/src/utils/literal-primitive.ts @@ -13,6 +13,11 @@ export function isStringLiteralPrimitive( return isLiteralPrimitive(node) && typeof node.value === 'string'; } -export function getLiteralPrimitiveStringValue(node: LiteralPrimitive, quote: "'" | '"' | '`'): string { - return typeof node.value === 'string' ? `${node.value.replaceAll(quote, `\\${quote}`)}` : String(node.value); -} \ No newline at end of file +export function getLiteralPrimitiveStringValue( + node: LiteralPrimitive, + quote: "'" | '"' | '`', +): string { + return typeof node.value === 'string' + ? `${node.value.replaceAll(quote, `\\${quote}`)}` + : String(node.value); +} diff --git a/packages/eslint-plugin-template/tests/rules/prefer-template-literal/cases.ts b/packages/eslint-plugin-template/tests/rules/prefer-template-literal/cases.ts index 118a925f1..de107ae41 100644 --- a/packages/eslint-plugin-template/tests/rules/prefer-template-literal/cases.ts +++ b/packages/eslint-plugin-template/tests/rules/prefer-template-literal/cases.ts @@ -40,7 +40,7 @@ export const invalid: readonly InvalidTestCase[] = [ `, }), - + convertAnnotatedSourceToFailureCase({ messageId, description: @@ -109,7 +109,7 @@ export const invalid: readonly InvalidTestCase[] = [ `, }), - + convertAnnotatedSourceToFailureCase({ messageId, description: @@ -242,8 +242,7 @@ export const invalid: readonly InvalidTestCase[] = [ }), convertAnnotatedSourceToFailureCase({ messageId, - description: - 'should fail concatenation (left: simple quote, right: array)', + description: 'should fail concatenation (left: simple quote, right: array)', annotatedSource: ` {{ 'prefix-' + [42] }} ~~~~~~~~~~~~~~~~ @@ -333,8 +332,7 @@ export const invalid: readonly InvalidTestCase[] = [ }), convertAnnotatedSourceToFailureCase({ messageId, - description: - 'should fail concatenation (left: double quote, right: array)', + description: 'should fail concatenation (left: double quote, right: array)', annotatedSource: ` {{ "prefix-" + [42] }} ~~~~~~~~~~~~~~~~ @@ -348,8 +346,7 @@ export const invalid: readonly InvalidTestCase[] = [ // Left : template convertAnnotatedSourceToFailureCase({ messageId, - description: - 'should fail concatenation (left: template, right: number)', + description: 'should fail concatenation (left: template, right: number)', annotatedSource: ` {{ \`prefix-\${value}-suffix\` + 42 }} ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -361,8 +358,7 @@ export const invalid: readonly InvalidTestCase[] = [ }), convertAnnotatedSourceToFailureCase({ messageId, - description: - 'should fail concatenation (left: template, right: null)', + description: 'should fail concatenation (left: template, right: null)', annotatedSource: ` {{ \`prefix-\${value}-suffix\` + null }} ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -374,8 +370,7 @@ export const invalid: readonly InvalidTestCase[] = [ }), convertAnnotatedSourceToFailureCase({ messageId, - description: - 'should fail concatenation (left: template, right: undefined)', + description: 'should fail concatenation (left: template, right: undefined)', annotatedSource: ` {{ \`prefix-\${value}-suffix\` + undefined }} ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -387,8 +382,7 @@ export const invalid: readonly InvalidTestCase[] = [ }), convertAnnotatedSourceToFailureCase({ messageId, - description: - 'should fail concatenation (left: template, right: boolean)', + description: 'should fail concatenation (left: template, right: boolean)', annotatedSource: ` {{ \`prefix-\${value}-suffix\` + false }} ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -413,8 +407,7 @@ export const invalid: readonly InvalidTestCase[] = [ }), convertAnnotatedSourceToFailureCase({ messageId, - description: - 'should fail concatenation (left: template, right: call)', + description: 'should fail concatenation (left: template, right: call)', annotatedSource: ` {{ \`prefix-\${value}-suffix\` + value2() }} ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -426,8 +419,7 @@ export const invalid: readonly InvalidTestCase[] = [ }), convertAnnotatedSourceToFailureCase({ messageId, - description: - 'should fail concatenation (left: template, right: array)', + description: 'should fail concatenation (left: template, right: array)', annotatedSource: ` {{ \`prefix-\${value}-suffix\` + [42] }} ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -517,8 +509,7 @@ export const invalid: readonly InvalidTestCase[] = [ }), convertAnnotatedSourceToFailureCase({ messageId, - description: - 'should fail concatenation (left: array, right: simple quote)', + description: 'should fail concatenation (left: array, right: simple quote)', annotatedSource: ` {{ [42] + '-suffix' }} ~~~~~~~~~~~~~~~~ @@ -621,8 +612,7 @@ export const invalid: readonly InvalidTestCase[] = [ }), convertAnnotatedSourceToFailureCase({ messageId, - description: - 'should fail concatenation (left: array, right: double quote)', + description: 'should fail concatenation (left: array, right: double quote)', annotatedSource: ` {{ [42] + "-suffix" }} ~~~~~~~~~~~~~~~~ @@ -649,8 +639,7 @@ export const invalid: readonly InvalidTestCase[] = [ // Right : template convertAnnotatedSourceToFailureCase({ messageId, - description: - 'should fail concatenation (left: number, right: template)', + description: 'should fail concatenation (left: number, right: template)', annotatedSource: ` {{ 42 + \`prefix-\${value}-suffix\` }} ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -662,8 +651,7 @@ export const invalid: readonly InvalidTestCase[] = [ }), convertAnnotatedSourceToFailureCase({ messageId, - description: - 'should fail concatenation (left: null, right: template)', + description: 'should fail concatenation (left: null, right: template)', annotatedSource: ` {{ null + \`prefix-\${value}-suffix\` }} ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -675,8 +663,7 @@ export const invalid: readonly InvalidTestCase[] = [ }), convertAnnotatedSourceToFailureCase({ messageId, - description: - 'should fail concatenation (left: undefined, right: template)', + description: 'should fail concatenation (left: undefined, right: template)', annotatedSource: ` {{ undefined + \`prefix-\${value}-suffix\` }} ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -688,8 +675,7 @@ export const invalid: readonly InvalidTestCase[] = [ }), convertAnnotatedSourceToFailureCase({ messageId, - description: - 'should fail concatenation (left: boolean, right: template)', + description: 'should fail concatenation (left: boolean, right: template)', annotatedSource: ` {{ false + \`prefix-\${value}-suffix\` }} ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -714,8 +700,7 @@ export const invalid: readonly InvalidTestCase[] = [ }), convertAnnotatedSourceToFailureCase({ messageId, - description: - 'should fail concatenation (left: call, right: template)', + description: 'should fail concatenation (left: call, right: template)', annotatedSource: ` {{ value2() + \`prefix-\${value}-suffix\` }} ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -727,8 +712,7 @@ export const invalid: readonly InvalidTestCase[] = [ }), convertAnnotatedSourceToFailureCase({ messageId, - description: - 'should fail concatenation (left: array, right: template)', + description: 'should fail concatenation (left: array, right: template)', annotatedSource: ` {{ [42] + \`prefix-\${value}-suffix\` }} ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From 87a9485176427699f5c31c1cd0c1f2f0209e3d56 Mon Sep 17 00:00:00 2001 From: Guillaume Drouard Date: Sat, 22 Mar 2025 00:00:08 +0100 Subject: [PATCH 6/8] doc: run script update-rule-docs --- .../docs/rules/prefer-template-literal.md | 1043 ++++++++++++++++- 1 file changed, 1006 insertions(+), 37 deletions(-) diff --git a/packages/eslint-plugin-template/docs/rules/prefer-template-literal.md b/packages/eslint-plugin-template/docs/rules/prefer-template-literal.md index ba4b45f86..510d24ce8 100644 --- a/packages/eslint-plugin-template/docs/rules/prefer-template-literal.md +++ b/packages/eslint-plugin-template/docs/rules/prefer-template-literal.md @@ -56,7 +56,520 @@ The rule does not have any configuration options. #### ❌ Invalid Code ```html -{{ 'prefix-' + '-suffix' }} +{{ 'pre"fix-' + '-suf\'fix' }} + ~~~~~~~~~~~~~~~~~~~~~~~~ +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```html +{{ "pre'fix-" + "-suf\"fix" }} + ~~~~~~~~~~~~~~~~~~~~~~~~ +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```html +{{ `prefix-${value}-suffix` + `-prefix2-${value2}-suffix2` }} + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```html +{{ 'pre"fix-' + "-suf'fix" }} + ~~~~~~~~~~~~~~~~~~~~~~~ +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```html +{{ 'pre`fix-' + `'pre\`fix"-${value}-"suf\`fix'` }} + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```html +{{ "pre'fix-" + '-suf"fix' }} + ~~~~~~~~~~~~~~~~~~~~~~~ +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```html +{{ "pre`fix-" + `'pre\`fix"-${value}-"suf\`fix'` }} + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```html + + ~~~~~~~~~~~~~~~~~~~ +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```html +@if (value() + "-suffix" | pipe) {} + ~~~~~~~~~~~~~~~~~~~ +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```html +@defer (when value() + '-suffix' | pipe) {} + ~~~~~~~~~~~~~~~~~~~ +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```html +{{ 'prefix-' + 42 }} + ~~~~~~~~~~~~~~ +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```html +{{ 'prefix-' + null }} + ~~~~~~~~~~~~~~~~ +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```html +{{ 'prefix-' + undefined }} + ~~~~~~~~~~~~~~~~~~~~~ +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```html +{{ 'prefix-' + true }} + ~~~~~~~~~~~~~~~~ +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```html +{{ 'prefix-' + value }} + ~~~~~~~~~~~~~~~~~ +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```html +{{ 'prefix-' + value() }} + ~~~~~~~~~~~~~~~~~~~ +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```html +{{ 'prefix-' + [42] }} + ~~~~~~~~~~~~~~~~ +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```html +{{ "prefix-" + 42 }} + ~~~~~~~~~~~~~~ +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```html +{{ "prefix-" + null }} + ~~~~~~~~~~~~~~~~ +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```html +{{ "prefix-" + undefined }} ~~~~~~~~~~~~~~~~~~~~~ ``` @@ -83,8 +596,251 @@ The rule does not have any configuration options. #### ❌ Invalid Code ```html -{{ `prefix-${value}-suffix` + `-prefix2-${value2}-suffix2` }} - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +{{ "prefix-" + true }} + ~~~~~~~~~~~~~~~~ +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```html +{{ "prefix-" + value }} + ~~~~~~~~~~~~~~~~~ +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```html +{{ "prefix-" + value() }} + ~~~~~~~~~~~~~~~~~~~ +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```html +{{ "prefix-" + [42] }} + ~~~~~~~~~~~~~~~~ +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```html +{{ `prefix-${value}-suffix` + 42 }} + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```html +{{ `prefix-${value}-suffix` + null }} + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```html +{{ `prefix-${value}-suffix` + undefined }} + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```html +{{ `prefix-${value}-suffix` + false }} + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```html +{{ `prefix-${value}-suffix` + value2 }} + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```html +{{ `prefix-${value}-suffix` + value2() }} + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ```
@@ -110,8 +866,8 @@ The rule does not have any configuration options. #### ❌ Invalid Code ```html - - ~~~~~~~~~~~~~~~~~~~ +{{ `prefix-${value}-suffix` + [42] }} + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ```
@@ -137,8 +893,8 @@ The rule does not have any configuration options. #### ❌ Invalid Code ```html -@if (value() + '-suffix' | pipe) {} - ~~~~~~~~~~~~~~~~~~~ +{{ 42 + '-suffix' }} + ~~~~~~~~~~~~~~ ```
@@ -164,8 +920,8 @@ The rule does not have any configuration options. #### ❌ Invalid Code ```html -@defer (when value() + '-suffix' | pipe) {} - ~~~~~~~~~~~~~~~~~~~ +{{ null + '-suffix' }} + ~~~~~~~~~~~~~~~~ ```
@@ -191,8 +947,8 @@ The rule does not have any configuration options. #### ❌ Invalid Code ```html -{{ 'prefix-' + 42 }} - ~~~~~~~~~~~~~~ +{{ undefined + '-suffix' }} + ~~~~~~~~~~~~~~~~~~~~~ ```
@@ -218,7 +974,7 @@ The rule does not have any configuration options. #### ❌ Invalid Code ```html -{{ 'prefix-' + null }} +{{ true + '-suffix' }} ~~~~~~~~~~~~~~~~ ``` @@ -245,8 +1001,8 @@ The rule does not have any configuration options. #### ❌ Invalid Code ```html -{{ 'prefix-' + undefined }} - ~~~~~~~~~~~~~~~~~~~~~ +{{ value + '-suffix' }} + ~~~~~~~~~~~~~~~~~ ```
@@ -272,7 +1028,34 @@ The rule does not have any configuration options. #### ❌ Invalid Code ```html -{{ 'prefix-' + true }} +{{ value() + '-suffix' }} + ~~~~~~~~~~~~~~~~~~~ +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```html +{{ [42] + '-suffix' }} ~~~~~~~~~~~~~~~~ ``` @@ -299,8 +1082,8 @@ The rule does not have any configuration options. #### ❌ Invalid Code ```html -{{ 'prefix-' + value }} - ~~~~~~~~~~~~~~~~~ +{{ `'pre\`fix"-${value}-"suf\`fix'` + '-suf`fix' }} + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ```
@@ -326,8 +1109,8 @@ The rule does not have any configuration options. #### ❌ Invalid Code ```html -{{ 'prefix-' + value() }} - ~~~~~~~~~~~~~~~~~~~ +{{ 42 + "-suffix" }} + ~~~~~~~~~~~~~~ ```
@@ -353,7 +1136,7 @@ The rule does not have any configuration options. #### ❌ Invalid Code ```html -{{ 'prefix-' + [42] }} +{{ null + "-suffix" }} ~~~~~~~~~~~~~~~~ ``` @@ -380,8 +1163,8 @@ The rule does not have any configuration options. #### ❌ Invalid Code ```html -{{ 'prefix-' + `prefix-${value}-suffix` }} - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +{{ undefined + "-suffix" }} + ~~~~~~~~~~~~~~~~~~~~~ ```
@@ -407,8 +1190,8 @@ The rule does not have any configuration options. #### ❌ Invalid Code ```html -{{ 42 + '-suffix' }} - ~~~~~~~~~~~~~~ +{{ true + "-suffix" }} + ~~~~~~~~~~~~~~~~ ```
@@ -434,8 +1217,8 @@ The rule does not have any configuration options. #### ❌ Invalid Code ```html -{{ null + '-suffix' }} - ~~~~~~~~~~~~~~~~ +{{ value + "-suffix" }} + ~~~~~~~~~~~~~~~~~ ```
@@ -461,8 +1244,8 @@ The rule does not have any configuration options. #### ❌ Invalid Code ```html -{{ undefined + '-suffix' }} - ~~~~~~~~~~~~~~~~~~~~~ +{{ value() + "-suffix" }} + ~~~~~~~~~~~~~~~~~~~ ```
@@ -488,7 +1271,7 @@ The rule does not have any configuration options. #### ❌ Invalid Code ```html -{{ true + '-suffix' }} +{{ [42] + "-suffix" }} ~~~~~~~~~~~~~~~~ ``` @@ -515,8 +1298,8 @@ The rule does not have any configuration options. #### ❌ Invalid Code ```html -{{ value + '-suffix' }} - ~~~~~~~~~~~~~~~~~ +{{ `'pre\`fix"-${value}-"suf\`fix'` + "-suf`fix" }} + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ```
@@ -542,8 +1325,8 @@ The rule does not have any configuration options. #### ❌ Invalid Code ```html -{{ value() + '-suffix' }} - ~~~~~~~~~~~~~~~~~~~ +{{ 42 + `prefix-${value}-suffix` }} + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ```
@@ -569,8 +1352,8 @@ The rule does not have any configuration options. #### ❌ Invalid Code ```html -{{ [42] + '-suffix' }} - ~~~~~~~~~~~~~~~~ +{{ null + `prefix-${value}-suffix` }} + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ```
@@ -596,10 +1379,118 @@ The rule does not have any configuration options. #### ❌ Invalid Code ```html -{{ `prefix-${value}-suffix` + '-suffix' }} +{{ undefined + `prefix-${value}-suffix` }} ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ``` +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```html +{{ false + `prefix-${value}-suffix` }} + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```html +{{ value2 + `prefix-${value}-suffix` }} + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```html +{{ value2() + `prefix-${value}-suffix` }} + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```html +{{ [42] + `prefix-${value}-suffix` }} + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``` +
@@ -734,7 +1625,59 @@ The rule does not have any configuration options. #### ✅ Valid Code ```html -{{ 'text' | pipe }} +{{ 'simple-quote' | pipe }} +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ✅ Valid Code + +```html +{{ "double-quote" }}" +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ✅ Valid Code + +```html +{{ `backquote` }} ```
@@ -837,6 +1780,32 @@ The rule does not have any configuration options. #### ✅ Valid Code +```html + +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ✅ Valid Code + ```html ``` From 05b2cebea7a07d5729c45ba282f9b8e227c62db7 Mon Sep 17 00:00:00 2001 From: Guillaume Drouard Date: Sun, 23 Mar 2025 01:50:12 +0100 Subject: [PATCH 7/8] test(prefer-template-literal): add @let test --- .../docs/rules/prefer-template-literal.md | 53 +++++++++++++++++++ .../rules/prefer-template-literal/cases.ts | 14 +++++ 2 files changed, 67 insertions(+) diff --git a/packages/eslint-plugin-template/docs/rules/prefer-template-literal.md b/packages/eslint-plugin-template/docs/rules/prefer-template-literal.md index 510d24ce8..7f7251c0e 100644 --- a/packages/eslint-plugin-template/docs/rules/prefer-template-literal.md +++ b/packages/eslint-plugin-template/docs/rules/prefer-template-literal.md @@ -325,6 +325,33 @@ The rule does not have any configuration options. #### ❌ Invalid Code +```html +@let letValue = value() + '-suffix'; + ~~~~~~~~~~~~~~~~~~~ +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ❌ Invalid Code + ```html {{ 'prefix-' + 42 }} ~~~~~~~~~~~~~~ @@ -1754,6 +1781,32 @@ The rule does not have any configuration options. #### ✅ Valid Code +```html +@let letValue = `prefix-${value}-suffix` +``` + +
+ +--- + +
+ +#### Default Config + +```json +{ + "rules": { + "@angular-eslint/template/prefer-template-literal": [ + "error" + ] + } +} +``` + +
+ +#### ✅ Valid Code + ```html

{{ `prefix-${value}-suffix` }}

``` diff --git a/packages/eslint-plugin-template/tests/rules/prefer-template-literal/cases.ts b/packages/eslint-plugin-template/tests/rules/prefer-template-literal/cases.ts index de107ae41..ed154e09a 100644 --- a/packages/eslint-plugin-template/tests/rules/prefer-template-literal/cases.ts +++ b/packages/eslint-plugin-template/tests/rules/prefer-template-literal/cases.ts @@ -20,6 +20,7 @@ export const valid: readonly (string | ValidTestCase)[] = [ '{{ `backquote` }}', '@if (`prefix-${value}-suffix`) {}', '@defer (when `prefix-${value}-suffix`) {}', + '@let letValue = `prefix-${value}-suffix`', '

{{ `prefix-${value}-suffix` }}

', '', '', @@ -163,6 +164,19 @@ export const invalid: readonly InvalidTestCase[] = [ `, }), + convertAnnotatedSourceToFailureCase({ + messageId, + description: 'should fail concatenation with let', + annotatedSource: ` + @let letValue = value() + '-suffix'; + ~~~~~~~~~~~~~~~~~~~ + `, + annotatedOutput: ` + @let letValue = \`\${value()}-suffix\`; + + `, + }), + // Left : simple quote convertAnnotatedSourceToFailureCase({ messageId, From f7abca1f6d414560e83d134610e5e8fe915be40e Mon Sep 17 00:00:00 2001 From: Guillaume Drouard Date: Fri, 25 Apr 2025 10:27:04 +0200 Subject: [PATCH 8/8] fix(prefer-template-literal): add missing end dot --- packages/eslint-plugin-template/README.md | 2 +- .../docs/rules/prefer-template-literal.md | 2 +- .../eslint-plugin-template/src/rules/prefer-template-literal.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/eslint-plugin-template/README.md b/packages/eslint-plugin-template/README.md index bf7231959..f28cb7178 100644 --- a/packages/eslint-plugin-template/README.md +++ b/packages/eslint-plugin-template/README.md @@ -64,7 +64,7 @@ Please see https://github.com/angular-eslint/angular-eslint for full usage instr | [`prefer-contextual-for-variables`](https://github.com/angular-eslint/angular-eslint/blob/main/packages/eslint-plugin-template/docs/rules/prefer-contextual-for-variables.md) | Ensures that contextual variables are used in @for blocks where possible instead of aliasing them. | | :wrench: | | | | [`prefer-control-flow`](https://github.com/angular-eslint/angular-eslint/blob/main/packages/eslint-plugin-template/docs/rules/prefer-control-flow.md) | Ensures that the built-in control flow is used. | | | | | | [`prefer-ngsrc`](https://github.com/angular-eslint/angular-eslint/blob/main/packages/eslint-plugin-template/docs/rules/prefer-ngsrc.md) | Ensures ngSrc is used instead of src for img elements | | | | | -| [`prefer-template-literal`](https://github.com/angular-eslint/angular-eslint/blob/main/packages/eslint-plugin-template/docs/rules/prefer-template-literal.md) | Ensure that template literals are used instead of concatenating strings or expressions | | :wrench: | | | +| [`prefer-template-literal`](https://github.com/angular-eslint/angular-eslint/blob/main/packages/eslint-plugin-template/docs/rules/prefer-template-literal.md) | Ensure that template literals are used instead of concatenating strings or expressions. | | :wrench: | | | | [`role-has-required-aria`](https://github.com/angular-eslint/angular-eslint/blob/main/packages/eslint-plugin-template/docs/rules/role-has-required-aria.md) | [Accessibility] Ensures elements with ARIA roles have all required properties for that role. | | | :bulb: | :accessibility: | | [`table-scope`](https://github.com/angular-eslint/angular-eslint/blob/main/packages/eslint-plugin-template/docs/rules/table-scope.md) | [Accessibility] Ensures that the `scope` attribute is only used on the `` element | | :wrench: | | :accessibility: | | [`use-track-by-function`](https://github.com/angular-eslint/angular-eslint/blob/main/packages/eslint-plugin-template/docs/rules/use-track-by-function.md) | Ensures trackBy function is used | | | | | diff --git a/packages/eslint-plugin-template/docs/rules/prefer-template-literal.md b/packages/eslint-plugin-template/docs/rules/prefer-template-literal.md index 7f7251c0e..c8f23457f 100644 --- a/packages/eslint-plugin-template/docs/rules/prefer-template-literal.md +++ b/packages/eslint-plugin-template/docs/rules/prefer-template-literal.md @@ -15,7 +15,7 @@ # `@angular-eslint/template/prefer-template-literal` -Ensure that template literals are used instead of concatenating strings or expressions +Ensure that template literals are used instead of concatenating strings or expressions. - Type: suggestion - 🔧 Supports autofix (`--fix`) diff --git a/packages/eslint-plugin-template/src/rules/prefer-template-literal.ts b/packages/eslint-plugin-template/src/rules/prefer-template-literal.ts index e07b959eb..22a896453 100644 --- a/packages/eslint-plugin-template/src/rules/prefer-template-literal.ts +++ b/packages/eslint-plugin-template/src/rules/prefer-template-literal.ts @@ -25,7 +25,7 @@ export default createESLintRule({ type: 'suggestion', docs: { description: - 'Ensure that template literals are used instead of concatenating strings or expressions', + 'Ensure that template literals are used instead of concatenating strings or expressions.', }, fixable: 'code', schema: [],