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 + +``` + +```gjs + +``` + +Examples of **correct** code for this rule: + +```gjs + +``` + +```gjs + +``` + +```gjs + +``` + +```gjs + +``` + +## 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 { + + } + `, + output: null, + }, + { + filename: 'my-component.gjs', + code: ` + import Component from '@glimmer/component'; + export default class MyComponent extends Component { + + } + `, + output: null, + }, + + '', + '', + '', + '', + '', + '', + '', + '', + ], + + invalid: [ + { + filename: 'my-component.gjs', + code: ` + import Component from '@glimmer/component'; + export default class MyComponent extends Component { + + } + `, + output: null, + errors: [ + { + messageId: 'noPotentialPathStrings', + }, + ], + }, + { + filename: 'my-component.gjs', + code: ` + import Component from '@glimmer/component'; + export default class MyComponent extends Component { + + } + `, + 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 - -``` - -```gjs - -``` - -Examples of **correct** code for this rule: - -```gjs - -``` - -```gjs - -``` +This rule **allows** the following: -```gjs - +```hbs + ``` -```gjs - +```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 { - - } - `, - output: null, - }, - { - filename: 'my-component.gjs', - code: ` - import Component from '@glimmer/component'; - export default class MyComponent extends Component { - - } - `, - 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 { - - } - `, + 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 { - - } - `, + 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}}?', + }, ], }, ],