diff --git a/packages/angular-eslint/src/configs/template-all.ts b/packages/angular-eslint/src/configs/template-all.ts
index dc6bf4894..8598dd46b 100644
--- a/packages/angular-eslint/src/configs/template-all.ts
+++ b/packages/angular-eslint/src/configs/template-all.ts
@@ -43,6 +43,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 8002ed741..f28cb7178 100644
--- a/packages/eslint-plugin-template/README.md
+++ b/packages/eslint-plugin-template/README.md
@@ -64,6 +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: | | |
| [`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..c8f23457f
--- /dev/null
+++ b/packages/eslint-plugin-template/docs/rules/prefer-template-literal.md
@@ -0,0 +1,1894 @@
+
+
+
+
+# `@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
+{{ '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
+@let letValue = value() + '-suffix';
+ ~~~~~~~~~~~~~~~~~~~
+```
+
+
+
+---
+
+
+
+#### 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 }}
+ ~~~~~~~~~~~~~~~~~~~~~
+```
+
+
+
+---
+
+
+
+#### 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-${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() }}
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+```
+
+
+
+---
+
+
+
+#### 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
+{{ 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
+{{ `'pre\`fix"-${value}-"suf\`fix'` + '-suf`fix' }}
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+```
+
+
+
+---
+
+
+
+#### 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
+{{ `'pre\`fix"-${value}-"suf\`fix'` + "-suf`fix" }}
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+```
+
+
+
+---
+
+
+
+#### Default Config
+
+```json
+{
+ "rules": {
+ "@angular-eslint/template/prefer-template-literal": [
+ "error"
+ ]
+ }
+}
+```
+
+
+
+#### ❌ Invalid Code
+
+```html
+{{ 42 + `prefix-${value}-suffix` }}
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+```
+
+
+
+---
+
+
+
+#### Default Config
+
+```json
+{
+ "rules": {
+ "@angular-eslint/template/prefer-template-literal": [
+ "error"
+ ]
+ }
+}
+```
+
+
+
+#### ❌ Invalid Code
+
+```html
+{{ null + `prefix-${value}-suffix` }}
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+```
+
+
+
+---
+
+
+
+#### Default Config
+
+```json
+{
+ "rules": {
+ "@angular-eslint/template/prefer-template-literal": [
+ "error"
+ ]
+ }
+}
+```
+
+
+
+#### ❌ Invalid Code
+
+```html
+{{ 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` }}
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+```
+
+
+
+
+
+---
+
+
+
+
+✅ - 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
+{{ '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` }}
+```
+
+
+
+---
+
+
+
+#### 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
+@let letValue = `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
+
+```
+
+
+
+---
+
+
+
+#### 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 ef987c2aa..5fcd30d67 100644
--- a/packages/eslint-plugin-template/src/configs/all.json
+++ b/packages/eslint-plugin-template/src/configs/all.json
@@ -29,6 +29,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 cb2b1ce72..45db70eb0 100644
--- a/packages/eslint-plugin-template/src/index.ts
+++ b/packages/eslint-plugin-template/src/index.ts
@@ -76,6 +76,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';
@@ -123,6 +126,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..22a896453
--- /dev/null
+++ b/packages/eslint-plugin-template/src/rules/prefer-template-literal.ts
@@ -0,0 +1,156 @@
+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 {
+ getLiteralPrimitiveStringValue,
+ isLiteralPrimitive,
+ 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;
+
+ 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),
+ end: sourceCode.getLocFromIndex(end),
+ },
+ messageId,
+ fix: (fixer) => {
+ // 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],
+ `${quote}${getLiteralPrimitiveStringValue(left, quote)}${getLiteralPrimitiveStringValue(right, quote)}${quote}`,
+ );
+ }
+
+ const fixes = new Array();
+
+ // Fix the left side
+ fixes.push(...getLeftSideFixes(fixer, left));
+
+ // Remove the `+` sign
+ fixes.push(
+ fixer.removeRange([left.sourceSpan.end, right.sourceSpan.start]),
+ );
+
+ // Fix the right side
+ fixes.push(...getRightSideFixes(fixer, right));
+
+ return fixes;
+ },
+ });
+ },
+ };
+ },
+});
+
+function getLeftSideFixes(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 (isLiteralPrimitive(left)) {
+ // Transform left side to template literal
+ return [
+ fixer.replaceTextRange(
+ [start, end],
+ `\`${getLiteralPrimitiveStringValue(left, '`')}`,
+ ),
+ ];
+ } else {
+ // Transform left side to template literal
+ return [
+ fixer.insertTextBeforeRange([start, end], '`${'),
+ fixer.insertTextAfterRange([start, end], '}'),
+ ];
+ }
+}
+
+function getRightSideFixes(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 (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 [
+ 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..337be06ca
--- /dev/null
+++ b/packages/eslint-plugin-template/src/utils/literal-primitive.ts
@@ -0,0 +1,23 @@
+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';
+}
+
+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
new file mode 100644
index 000000000..ed154e09a
--- /dev/null
+++ b/packages/eslint-plugin-template/tests/rules/prefer-template-literal/cases.ts
@@ -0,0 +1,739 @@
+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() }}',
+ "{{ 'simple-quote' | pipe }}",
+ '{{ "double-quote" }}"',
+ '{{ `backquote` }}',
+ '@if (`prefix-${value}-suffix`) {}',
+ '@defer (when `prefix-${value}-suffix`) {}',
+ '@let letValue = `prefix-${value}-suffix`',
+ '{{ `prefix-${value}-suffix` }}',
+ '',
+ '',
+ '',
+];
+
+export const invalid: readonly InvalidTestCase[] = [
+ convertAnnotatedSourceToFailureCase({
+ messageId,
+ description:
+ 'should fail concatenation (left: simple quote, right: simple quote)',
+ annotatedSource: `
+ {{ 'pre"fix-' + '-suf\\'fix' }}
+ ~~~~~~~~~~~~~~~~~~~~~~~~
+ `,
+ annotatedOutput: `
+ {{ '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" }}
+
+ `,
+ }),
+
+ 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 (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',
+ 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) {}
+
+ `,
+ }),
+
+ convertAnnotatedSourceToFailureCase({
+ messageId,
+ description: 'should fail concatenation with let',
+ annotatedSource: `
+ @let letValue = value() + '-suffix';
+ ~~~~~~~~~~~~~~~~~~~
+ `,
+ annotatedOutput: `
+ @let letValue = \`\${value()}-suffix\`;
+
+ `,
+ }),
+
+ // Left : simple quote
+ convertAnnotatedSourceToFailureCase({
+ messageId,
+ description:
+ 'should fail concatenation (left: simple quote, right: number)',
+ annotatedSource: `
+ {{ 'prefix-' + 42 }}
+ ~~~~~~~~~~~~~~
+ `,
+ annotatedOutput: `
+ {{ 'prefix-42' }}
+
+ `,
+ }),
+ convertAnnotatedSourceToFailureCase({
+ messageId,
+ description: 'should fail concatenation (left: simple quote, right: null)',
+ annotatedSource: `
+ {{ 'prefix-' + null }}
+ ~~~~~~~~~~~~~~~~
+ `,
+ annotatedOutput: `
+ {{ 'prefix-null' }}
+
+ `,
+ }),
+ convertAnnotatedSourceToFailureCase({
+ messageId,
+ description:
+ 'should fail concatenation (left: simple quote, right: undefined)',
+ annotatedSource: `
+ {{ 'prefix-' + undefined }}
+ ~~~~~~~~~~~~~~~~~~~~~
+ `,
+ annotatedOutput: `
+ {{ 'prefix-undefined' }}
+
+ `,
+ }),
+ convertAnnotatedSourceToFailureCase({
+ messageId,
+ description:
+ 'should fail concatenation (left: simple quote, right: boolean)',
+ annotatedSource: `
+ {{ 'prefix-' + true }}
+ ~~~~~~~~~~~~~~~~
+ `,
+ annotatedOutput: `
+ {{ 'prefix-true' }}
+
+ `,
+ }),
+ convertAnnotatedSourceToFailureCase({
+ messageId,
+ description:
+ 'should fail concatenation (left: simple quote, right: property read)',
+ annotatedSource: `
+ {{ 'prefix-' + value }}
+ ~~~~~~~~~~~~~~~~~
+ `,
+ annotatedOutput: `
+ {{ \`prefix-\${value}\` }}
+
+ `,
+ }),
+ convertAnnotatedSourceToFailureCase({
+ messageId,
+ description: 'should fail concatenation (left: simple quote, right: call)',
+ annotatedSource: `
+ {{ 'prefix-' + value() }}
+ ~~~~~~~~~~~~~~~~~~~
+ `,
+ annotatedOutput: `
+ {{ \`prefix-\${value()}\` }}
+
+ `,
+ }),
+ convertAnnotatedSourceToFailureCase({
+ messageId,
+ description: 'should fail concatenation (left: simple quote, right: array)',
+ annotatedSource: `
+ {{ 'prefix-' + [42] }}
+ ~~~~~~~~~~~~~~~~
+ `,
+ annotatedOutput: `
+ {{ \`prefix-\${[42]}\` }}
+
+ `,
+ }),
+
+ // Left : double quote
+ convertAnnotatedSourceToFailureCase({
+ messageId,
+ description:
+ 'should fail concatenation (left: double quote, right: number)',
+ annotatedSource: `
+ {{ "prefix-" + 42 }}
+ ~~~~~~~~~~~~~~
+ `,
+ annotatedOutput: `
+ {{ "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]}\` }}
+
+ `,
+ }),
+
+ // Left : template
+ convertAnnotatedSourceToFailureCase({
+ messageId,
+ description: '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' }}
+ ~~~~~~~~~~~~~~
+ `,
+ annotatedOutput: `
+ {{ '42-suffix' }}
+
+ `,
+ }),
+ convertAnnotatedSourceToFailureCase({
+ messageId,
+ description: 'should fail concatenation (left: null, right: simple quote)',
+ annotatedSource: `
+ {{ null + '-suffix' }}
+ ~~~~~~~~~~~~~~~~
+ `,
+ annotatedOutput: `
+ {{ 'null-suffix' }}
+
+ `,
+ }),
+ convertAnnotatedSourceToFailureCase({
+ messageId,
+ description:
+ 'should fail concatenation (left: undefined, right: simple quote)',
+ annotatedSource: `
+ {{ undefined + '-suffix' }}
+ ~~~~~~~~~~~~~~~~~~~~~
+ `,
+ annotatedOutput: `
+ {{ 'undefined-suffix' }}
+
+ `,
+ }),
+ convertAnnotatedSourceToFailureCase({
+ messageId,
+ description:
+ 'should fail concatenation (left: boolean, right: simple quote)',
+ annotatedSource: `
+ {{ true + '-suffix' }}
+ ~~~~~~~~~~~~~~~~
+ `,
+ annotatedOutput: `
+ {{ 'true-suffix' }}
+
+ `,
+ }),
+ convertAnnotatedSourceToFailureCase({
+ messageId,
+ description:
+ 'should fail concatenation (left: property read, right: simple quote)',
+ annotatedSource: `
+ {{ value + '-suffix' }}
+ ~~~~~~~~~~~~~~~~~
+ `,
+ annotatedOutput: `
+ {{ \`\${value}-suffix\` }}
+
+ `,
+ }),
+ convertAnnotatedSourceToFailureCase({
+ messageId,
+ description: 'should fail concatenation (left: call, right: simple quote)',
+ annotatedSource: `
+ {{ value() + '-suffix' }}
+ ~~~~~~~~~~~~~~~~~~~
+ `,
+ annotatedOutput: `
+ {{ \`\${value()}-suffix\` }}
+
+ `,
+ }),
+ convertAnnotatedSourceToFailureCase({
+ messageId,
+ description: 'should fail concatenation (left: array, right: simple quote)',
+ annotatedSource: `
+ {{ [42] + '-suffix' }}
+ ~~~~~~~~~~~~~~~~
+ `,
+ annotatedOutput: `
+ {{ \`\${[42]}-suffix\` }}
+
+ `,
+ }),
+ convertAnnotatedSourceToFailureCase({
+ messageId,
+ description:
+ 'should fail concatenation (left: template, right: simple quote)',
+ annotatedSource: `
+ {{ \`'pre\\\`fix"-\${value}-"suf\\\`fix'\` + '-suf\`fix' }}
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ `,
+ annotatedOutput: `
+ {{ \`'pre\\\`fix"-\${value}-"suf\\\`fix'-suf\\\`fix\` }}
+
+ `,
+ }),
+
+ // 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\` }}
+
+ `,
+ }),
+];
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,
+});
|