From 8f5a5e7a43733b60bc72754e72c730fb1fefa0bc Mon Sep 17 00:00:00 2001
From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com>
Date: Tue, 17 Mar 2026 19:17:38 -0400
Subject: [PATCH 1/2] Extract rule: template-no-potential-path-strings
---
README.md | 1 +
.../template-no-potential-path-strings.md | 80 ++++++++
.../template-no-potential-path-strings.js | 62 ++++++
.../template-no-potential-path-strings.js | 178 ++++++++++++++++++
4 files changed, 321 insertions(+)
create mode 100644 docs/rules/template-no-potential-path-strings.md
create mode 100644 lib/rules/template-no-potential-path-strings.js
create mode 100644 tests/lib/rules/template-no-potential-path-strings.js
diff --git a/README.md b/README.md
index 342e92ccde..502cf7e30a 100644
--- a/README.md
+++ b/README.md
@@ -257,6 +257,7 @@ rules in templates can be disabled with eslint directives with mustache or html
| [template-no-obsolete-elements](docs/rules/template-no-obsolete-elements.md) | disallow obsolete HTML elements | | | |
| [template-no-outlet-outside-routes](docs/rules/template-no-outlet-outside-routes.md) | disallow {{outlet}} outside of route templates | | | |
| [template-no-page-title-component](docs/rules/template-no-page-title-component.md) | disallow usage of ember-page-title component | | | |
+| [template-no-potential-path-strings](docs/rules/template-no-potential-path-strings.md) | disallow potential path strings in templates | | | |
| [template-no-splattributes-with-class](docs/rules/template-no-splattributes-with-class.md) | disallow splattributes with class attribute | | | |
| [template-no-trailing-spaces](docs/rules/template-no-trailing-spaces.md) | disallow trailing whitespace at the end of lines in templates | | 🔧 | |
| [template-no-unavailable-this](docs/rules/template-no-unavailable-this.md) | disallow `this` in templates that are not inside a class or function | | | |
diff --git a/docs/rules/template-no-potential-path-strings.md b/docs/rules/template-no-potential-path-strings.md
new file mode 100644
index 0000000000..fea73afac0
--- /dev/null
+++ b/docs/rules/template-no-potential-path-strings.md
@@ -0,0 +1,80 @@
+# ember/template-no-potential-path-strings
+
+
+
+Disallow potential path strings that should be dynamic values in templates.
+
+## Rule Details
+
+It might happen sometimes that `{{` and `}}` are forgotten when invoking a component, and the string that is passed was actually supposed to be a property path or argument.
+
+This rule warns about attribute values and text content that look like they should be dynamic paths. Specifically, it catches:
+
+- **Attribute values** that start with `this.` or `@` (e.g. `
` or `
`)
+- **Text content** that contains path-like strings (e.g. `
this.propertyName
` or `foo.bar
`)
+
+## Examples
+
+Examples of **incorrect** code for this rule:
+
+```gjs
+
+
+
+```
+
+```gjs
+
+
+
+```
+
+```gjs
+
+ this.propertyName
+
+```
+
+```gjs
+
+ foo.bar
+
+```
+
+Examples of **correct** code for this rule:
+
+```gjs
+
+
+
+```
+
+```gjs
+
+
+
+```
+
+```gjs
+
+ {{this.propertyName}}
+
+```
+
+```gjs
+
+ {{this.foo.bar}}
+
+```
+
+## Migration
+
+- Replace the surrounding `"` characters with `{{`/`}}`
+
+## Related Rules
+
+- [no-arguments-for-html-elements](template-no-arguments-for-html-elements.md)
+
+## References
+
+- [Component Arguments and HTML Attributes](https://guides.emberjs.com/release/components/component-arguments-and-html-attributes/)
diff --git a/lib/rules/template-no-potential-path-strings.js b/lib/rules/template-no-potential-path-strings.js
new file mode 100644
index 0000000000..4dd88404be
--- /dev/null
+++ b/lib/rules/template-no-potential-path-strings.js
@@ -0,0 +1,62 @@
+/** @type {import('eslint').Rule.RuleModule} */
+module.exports = {
+ meta: {
+ type: 'suggestion',
+ docs: {
+ description: 'disallow potential path strings in templates',
+ category: 'Best Practices',
+ recommended: false,
+ url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-no-potential-path-strings.md',
+ templateMode: 'both',
+ },
+ fixable: null,
+ schema: [],
+ messages: {
+ noPotentialPathStrings:
+ 'Potential path string detected. Use dynamic values instead of path strings.',
+ },
+ originallyFrom: {
+ name: 'ember-template-lint',
+ rule: 'lib/rules/no-potential-path-strings.js',
+ docs: 'docs/rule/no-potential-path-strings.md',
+ tests: 'test/unit/rules/no-potential-path-strings-test.js',
+ },
+ },
+
+ create(context) {
+ const attrTextNodes = new WeakSet();
+
+ return {
+ GlimmerAttrNode(node) {
+ if (node.value && node.value.type === 'GlimmerTextNode') {
+ attrTextNodes.add(node.value);
+ const text = node.value.chars;
+ // Check for potential paths in attribute values:
+ // - this.something (should be {{this.something}})
+ // - @argName without / \ | (should be {{@argName}})
+ if (/^this\.\w+/.test(text) || /^@[\w-]+$/.test(text)) {
+ context.report({
+ node: node.value,
+ messageId: 'noPotentialPathStrings',
+ });
+ }
+ }
+ },
+
+ GlimmerTextNode(node) {
+ if (!node.chars || attrTextNodes.has(node)) {
+ return;
+ }
+
+ // Check if text content looks like it could be a path (e.g., "foo.bar" or "this.foo")
+ const pathPattern = /\b(this\.\w+|\w+\.\w+)\b/;
+ if (pathPattern.test(node.chars)) {
+ context.report({
+ node,
+ messageId: 'noPotentialPathStrings',
+ });
+ }
+ },
+ };
+ },
+};
diff --git a/tests/lib/rules/template-no-potential-path-strings.js b/tests/lib/rules/template-no-potential-path-strings.js
new file mode 100644
index 0000000000..a6e57efe0d
--- /dev/null
+++ b/tests/lib/rules/template-no-potential-path-strings.js
@@ -0,0 +1,178 @@
+const { RuleTester } = require('eslint');
+const rule = require('../../../lib/rules/template-no-potential-path-strings');
+
+const ruleTester = new RuleTester({
+ parser: require.resolve('ember-eslint-parser'),
+ parserOptions: { ecmaVersion: 2022, sourceType: 'module' },
+});
+
+ruleTester.run('template-no-potential-path-strings', rule, {
+ valid: [
+ {
+ filename: 'my-component.gjs',
+ code: `
+ import Component from '@glimmer/component';
+ export default class MyComponent extends Component {
+
+ {{this.propertyName}}
+
+ }
+ `,
+ output: null,
+ },
+ {
+ filename: 'my-component.gjs',
+ code: `
+ import Component from '@glimmer/component';
+ export default class MyComponent extends Component {
+
+ Hello world
+
+ }
+ `,
+ output: null,
+ },
+
+ '
',
+ '
',
+ '
',
+ '
',
+ '',
+ '',
+ '',
+ '',
+ ],
+
+ invalid: [
+ {
+ filename: 'my-component.gjs',
+ code: `
+ import Component from '@glimmer/component';
+ export default class MyComponent extends Component {
+
+ this.propertyName
+
+ }
+ `,
+ output: null,
+ errors: [
+ {
+ messageId: 'noPotentialPathStrings',
+ },
+ ],
+ },
+ {
+ filename: 'my-component.gjs',
+ code: `
+ import Component from '@glimmer/component';
+ export default class MyComponent extends Component {
+
+ foo.bar
+
+ }
+ `,
+ output: null,
+ errors: [
+ {
+ messageId: 'noPotentialPathStrings',
+ },
+ ],
+ },
+
+ {
+ code: '
',
+ output: null,
+ errors: [{ messageId: 'noPotentialPathStrings' }],
+ },
+ {
+ code: '
',
+ output: null,
+ errors: [{ messageId: 'noPotentialPathStrings' }],
+ },
+ {
+ code: '
',
+ output: null,
+ errors: [{ messageId: 'noPotentialPathStrings' }],
+ },
+ {
+ code: '
',
+ output: null,
+ errors: [{ messageId: 'noPotentialPathStrings' }],
+ },
+ {
+ code: '',
+ output: null,
+ errors: [{ messageId: 'noPotentialPathStrings' }],
+ },
+ {
+ code: '',
+ output: null,
+ errors: [{ messageId: 'noPotentialPathStrings' }],
+ },
+ ],
+});
+
+const hbsRuleTester = new RuleTester({
+ parser: require.resolve('ember-eslint-parser/hbs'),
+ parserOptions: {
+ ecmaVersion: 2022,
+ sourceType: 'module',
+ },
+});
+
+hbsRuleTester.run('template-no-potential-path-strings', rule, {
+ valid: [
+ '
',
+ '
',
+ '
',
+ '
',
+ '',
+ '',
+ '',
+ '',
+ ],
+ invalid: [
+ {
+ code: '
',
+ output: null,
+ errors: [
+ { message: 'Potential path string detected. Use dynamic values instead of path strings.' },
+ ],
+ },
+ {
+ code: '
',
+ output: null,
+ errors: [
+ { message: 'Potential path string detected. Use dynamic values instead of path strings.' },
+ ],
+ },
+ {
+ code: '
',
+ output: null,
+ errors: [
+ { message: 'Potential path string detected. Use dynamic values instead of path strings.' },
+ ],
+ },
+ {
+ code: '
',
+ output: null,
+ errors: [
+ { message: 'Potential path string detected. Use dynamic values instead of path strings.' },
+ ],
+ },
+ {
+ code: '',
+ output: null,
+ errors: [
+ { message: 'Potential path string detected. Use dynamic values instead of path strings.' },
+ ],
+ },
+ {
+ code: '',
+ output: null,
+ errors: [
+ { message: 'Potential path string detected. Use dynamic values instead of path strings.' },
+ ],
+ },
+ ],
+});
From c9c9fcb1e3b9c639f3c4a4a8f4adc353f59e5d56 Mon Sep 17 00:00:00 2001
From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com>
Date: Sat, 21 Mar 2026 12:54:44 -0400
Subject: [PATCH 2/2] Sync with ember-template-lint
---
README.md | 2 +-
.../template-no-potential-path-strings.md | 61 ++--------
.../template-no-potential-path-strings.js | 37 ++----
.../template-no-potential-path-strings.js | 108 +++++++-----------
4 files changed, 65 insertions(+), 143 deletions(-)
diff --git a/README.md b/README.md
index 502cf7e30a..c5187e15a9 100644
--- a/README.md
+++ b/README.md
@@ -257,7 +257,7 @@ rules in templates can be disabled with eslint directives with mustache or html
| [template-no-obsolete-elements](docs/rules/template-no-obsolete-elements.md) | disallow obsolete HTML elements | | | |
| [template-no-outlet-outside-routes](docs/rules/template-no-outlet-outside-routes.md) | disallow {{outlet}} outside of route templates | | | |
| [template-no-page-title-component](docs/rules/template-no-page-title-component.md) | disallow usage of ember-page-title component | | | |
-| [template-no-potential-path-strings](docs/rules/template-no-potential-path-strings.md) | disallow potential path strings in templates | | | |
+| [template-no-potential-path-strings](docs/rules/template-no-potential-path-strings.md) | disallow potential path strings in attribute values | | | |
| [template-no-splattributes-with-class](docs/rules/template-no-splattributes-with-class.md) | disallow splattributes with class attribute | | | |
| [template-no-trailing-spaces](docs/rules/template-no-trailing-spaces.md) | disallow trailing whitespace at the end of lines in templates | | 🔧 | |
| [template-no-unavailable-this](docs/rules/template-no-unavailable-this.md) | disallow `this` in templates that are not inside a class or function | | | |
diff --git a/docs/rules/template-no-potential-path-strings.md b/docs/rules/template-no-potential-path-strings.md
index fea73afac0..1b13578c28 100644
--- a/docs/rules/template-no-potential-path-strings.md
+++ b/docs/rules/template-no-potential-path-strings.md
@@ -2,69 +2,30 @@
-Disallow potential path strings that should be dynamic values in templates.
-
-## Rule Details
-
It might happen sometimes that `{{` and `}}` are forgotten when invoking a component, and the string that is passed was actually supposed to be a property path or argument.
-This rule warns about attribute values and text content that look like they should be dynamic paths. Specifically, it catches:
-
-- **Attribute values** that start with `this.` or `@` (e.g. `
` or `
`)
-- **Text content** that contains path-like strings (e.g. `this.propertyName
` or `foo.bar
`)
+This rule warns about all arguments and attributes that start with `this.` or `@`, but are missing the surrounding `{{` and `}}` characters.
## Examples
-Examples of **incorrect** code for this rule:
+This rule **forbids** the following:
-```gjs
-
-
-
+```hbs
+
```
-```gjs
-
-
-
+```hbs
+
```
-```gjs
-
- this.propertyName
-
-```
-
-```gjs
-
- foo.bar
-
-```
-
-Examples of **correct** code for this rule:
-
-```gjs
-
-
-
-```
-
-```gjs
-
-
-
-```
+This rule **allows** the following:
-```gjs
-
- {{this.propertyName}}
-
+```hbs
+
```
-```gjs
-
- {{this.foo.bar}}
-
+```hbs
+
```
## Migration
diff --git a/lib/rules/template-no-potential-path-strings.js b/lib/rules/template-no-potential-path-strings.js
index 4dd88404be..2dd83d94b5 100644
--- a/lib/rules/template-no-potential-path-strings.js
+++ b/lib/rules/template-no-potential-path-strings.js
@@ -1,11 +1,12 @@
+const FINE_SYMBOLS = ['|', '/', '\\'];
+
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: 'suggestion',
docs: {
- description: 'disallow potential path strings in templates',
+ description: 'disallow potential path strings in attribute values',
category: 'Best Practices',
- recommended: false,
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-no-potential-path-strings.md',
templateMode: 'both',
},
@@ -13,7 +14,7 @@ module.exports = {
schema: [],
messages: {
noPotentialPathStrings:
- 'Potential path string detected. Use dynamic values instead of path strings.',
+ 'Potential path in attribute string detected. Did you mean {{{{path}}}}?',
},
originallyFrom: {
name: 'ember-template-lint',
@@ -24,36 +25,20 @@ module.exports = {
},
create(context) {
- const attrTextNodes = new WeakSet();
-
return {
GlimmerAttrNode(node) {
- if (node.value && node.value.type === 'GlimmerTextNode') {
- attrTextNodes.add(node.value);
- const text = node.value.chars;
- // Check for potential paths in attribute values:
- // - this.something (should be {{this.something}})
- // - @argName without / \ | (should be {{@argName}})
- if (/^this\.\w+/.test(text) || /^@[\w-]+$/.test(text)) {
- context.report({
- node: node.value,
- messageId: 'noPotentialPathStrings',
- });
- }
- }
- },
-
- GlimmerTextNode(node) {
- if (!node.chars || attrTextNodes.has(node)) {
+ if (!node.value || node.value.type !== 'GlimmerTextNode') {
return;
}
- // Check if text content looks like it could be a path (e.g., "foo.bar" or "this.foo")
- const pathPattern = /\b(this\.\w+|\w+\.\w+)\b/;
- if (pathPattern.test(node.chars)) {
+ const chars = node.value.chars;
+ const hasSpecialPrefix = chars.startsWith('this.') || chars.startsWith('@');
+
+ if (hasSpecialPrefix && !FINE_SYMBOLS.some((symbol) => chars.includes(symbol))) {
context.report({
- node,
+ node: node.value,
messageId: 'noPotentialPathStrings',
+ data: { path: chars },
});
}
},
diff --git a/tests/lib/rules/template-no-potential-path-strings.js b/tests/lib/rules/template-no-potential-path-strings.js
index a6e57efe0d..52e3af9d21 100644
--- a/tests/lib/rules/template-no-potential-path-strings.js
+++ b/tests/lib/rules/template-no-potential-path-strings.js
@@ -8,31 +8,6 @@ const ruleTester = new RuleTester({
ruleTester.run('template-no-potential-path-strings', rule, {
valid: [
- {
- filename: 'my-component.gjs',
- code: `
- import Component from '@glimmer/component';
- export default class MyComponent extends Component {
-
- {{this.propertyName}}
-
- }
- `,
- output: null,
- },
- {
- filename: 'my-component.gjs',
- code: `
- import Component from '@glimmer/component';
- export default class MyComponent extends Component {
-
- Hello world
-
- }
- `,
- output: null,
- },
-
'
',
'
',
'
',
@@ -45,69 +20,58 @@ ruleTester.run('template-no-potential-path-strings', rule, {
invalid: [
{
- filename: 'my-component.gjs',
- code: `
- import Component from '@glimmer/component';
- export default class MyComponent extends Component {
-
- this.propertyName
-
- }
- `,
+ code: '
',
output: null,
errors: [
{
- messageId: 'noPotentialPathStrings',
+ message: 'Potential path in attribute string detected. Did you mean {{this.picture}}?',
},
],
},
{
- filename: 'my-component.gjs',
- code: `
- import Component from '@glimmer/component';
- export default class MyComponent extends Component {
-
- foo.bar
-
- }
- `,
+ code: '
',
output: null,
errors: [
{
- messageId: 'noPotentialPathStrings',
+ message: 'Potential path in attribute string detected. Did you mean {{this.picture}}?',
},
],
},
-
- {
- code: '
',
- output: null,
- errors: [{ messageId: 'noPotentialPathStrings' }],
- },
- {
- code: '
',
- output: null,
- errors: [{ messageId: 'noPotentialPathStrings' }],
- },
{
code: '
',
output: null,
- errors: [{ messageId: 'noPotentialPathStrings' }],
+ errors: [
+ {
+ message: 'Potential path in attribute string detected. Did you mean {{@img}}?',
+ },
+ ],
},
{
code: '
',
output: null,
- errors: [{ messageId: 'noPotentialPathStrings' }],
+ errors: [
+ {
+ message: 'Potential path in attribute string detected. Did you mean {{@img}}?',
+ },
+ ],
},
{
code: '',
output: null,
- errors: [{ messageId: 'noPotentialPathStrings' }],
+ errors: [
+ {
+ message: 'Potential path in attribute string detected. Did you mean {{@bar}}?',
+ },
+ ],
},
{
code: '',
output: null,
- errors: [{ messageId: 'noPotentialPathStrings' }],
+ errors: [
+ {
+ message: 'Potential path in attribute string detected. Did you mean {{this.bar}}?',
+ },
+ ],
},
],
});
@@ -136,42 +100,54 @@ hbsRuleTester.run('template-no-potential-path-strings', rule, {
code: '
',
output: null,
errors: [
- { message: 'Potential path string detected. Use dynamic values instead of path strings.' },
+ {
+ message: 'Potential path in attribute string detected. Did you mean {{this.picture}}?',
+ },
],
},
{
code: '
',
output: null,
errors: [
- { message: 'Potential path string detected. Use dynamic values instead of path strings.' },
+ {
+ message: 'Potential path in attribute string detected. Did you mean {{this.picture}}?',
+ },
],
},
{
code: '
',
output: null,
errors: [
- { message: 'Potential path string detected. Use dynamic values instead of path strings.' },
+ {
+ message: 'Potential path in attribute string detected. Did you mean {{@img}}?',
+ },
],
},
{
code: '
',
output: null,
errors: [
- { message: 'Potential path string detected. Use dynamic values instead of path strings.' },
+ {
+ message: 'Potential path in attribute string detected. Did you mean {{@img}}?',
+ },
],
},
{
code: '',
output: null,
errors: [
- { message: 'Potential path string detected. Use dynamic values instead of path strings.' },
+ {
+ message: 'Potential path in attribute string detected. Did you mean {{@bar}}?',
+ },
],
},
{
code: '',
output: null,
errors: [
- { message: 'Potential path string detected. Use dynamic values instead of path strings.' },
+ {
+ message: 'Potential path in attribute string detected. Did you mean {{this.bar}}?',
+ },
],
},
],