From 77fbe796fbddbf7210d8577d5de8b8074363ec95 Mon Sep 17 00:00:00 2001
From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com>
Date: Tue, 10 Mar 2026 18:24:54 -0400
Subject: [PATCH 1/2] Extract rule: template-no-args-paths
---
README.md | 1 +
docs/rules/template-no-args-paths.md | 78 +++++++++++++
lib/rules/template-no-args-paths.js | 34 ++++++
tests/lib/rules/template-no-args-paths.js | 131 ++++++++++++++++++++++
4 files changed, 244 insertions(+)
create mode 100644 docs/rules/template-no-args-paths.md
create mode 100644 lib/rules/template-no-args-paths.js
create mode 100644 tests/lib/rules/template-no-args-paths.js
diff --git a/README.md b/README.md
index b695f7a52e..cd5aa94416 100644
--- a/README.md
+++ b/README.md
@@ -202,6 +202,7 @@ rules in templates can be disabled with eslint directives with mustache or html
| [template-builtin-component-arguments](docs/rules/template-builtin-component-arguments.md) | disallow setting certain attributes on builtin components | | | |
| [template-no-action-modifiers](docs/rules/template-no-action-modifiers.md) | disallow usage of {{action}} modifiers | | | |
| [template-no-action-on-submit-button](docs/rules/template-no-action-on-submit-button.md) | disallow action attribute on submit buttons | | | |
+| [template-no-args-paths](docs/rules/template-no-args-paths.md) | disallow @args in paths | | | |
| [template-no-arguments-for-html-elements](docs/rules/template-no-arguments-for-html-elements.md) | disallow @arguments on HTML elements | | | |
| [template-no-array-prototype-extensions](docs/rules/template-no-array-prototype-extensions.md) | disallow usage of Ember Array prototype extensions | | | |
| [template-no-at-ember-render-modifiers](docs/rules/template-no-at-ember-render-modifiers.md) | disallow usage of @ember/render-modifiers | | | |
diff --git a/docs/rules/template-no-args-paths.md b/docs/rules/template-no-args-paths.md
new file mode 100644
index 0000000000..bd51bce5c1
--- /dev/null
+++ b/docs/rules/template-no-args-paths.md
@@ -0,0 +1,78 @@
+# ember/template-no-args-paths
+
+
+
+Arguments that are passed to components are prefixed with the `@` symbol in Angle bracket syntax.
+Ember Octane leverages this in the component's templates by allowing users to directly refer to an argument using the same prefix:
+
+```gjs
+
+
+
+ {{#each @todos as |todo index|}}
+ -
+ {{yield (todo-item-component todo=todo) index}}
+
+ {{/each}}
+
+
+```
+
+We can immediately tell now by looking at this template that `@todos` is an argument that was passed to the component externally. This is in fact _always true_ - there is no way to modify the value referenced by `@todos` from the component class, it is the original, unmodified value.
+
+## Examples
+
+This rule **forbids** the following:
+
+```gjs
+
+ {{this.args.foo}}
+ {{args.foo}}
+
+```
+
+```gjs
+
+ {{my-helper this.args.foo}}
+ {{my-helper (hash value=this.args.foo)}}
+
+```
+
+```gjs
+
+
+
+
+```
+
+This rule **allows** the following:
+
+```gjs
+
+ {{my-helper this.args}}
+ {{my-helper (hash value=this.args)}}
+
+```
+
+```gjs
+
+ {{@foo}}
+
+
+
+```
+
+## Migration
+
+- find in templates `this.args.` replace to `@`
+
+## Related Rules
+
+- [no-curly-component-invocation](no-curly-component-invocation.md)
+
+## References
+
+- [RFC #276](https://github.com/emberjs/rfcs/blob/master/text/0276-named-args.md)
+- [Coming Soon in Ember Octane - Part 2: Named Argument Syntax](https://www.pzuraq.com/blog/coming-soon-in-ember-octane-part-2-angle-brackets-and-named-arguments/#namedargumentsyntax)
+- [Named arguments in Ember.js](https://www.balinterdi.com/blog/named-arguments-in-ember-js/)
+- [ember-named-arguments-polyfill](https://github.com/rwjblue/ember-named-arguments-polyfill)
diff --git a/lib/rules/template-no-args-paths.js b/lib/rules/template-no-args-paths.js
new file mode 100644
index 0000000000..34a4bd78bc
--- /dev/null
+++ b/lib/rules/template-no-args-paths.js
@@ -0,0 +1,34 @@
+/** @type {import('eslint').Rule.RuleModule} */
+module.exports = {
+ meta: {
+ type: 'problem',
+ docs: {
+ description: 'disallow @args in paths',
+ category: 'Best Practices',
+ url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-no-args-paths.md',
+ templateMode: 'both',
+ },
+ schema: [],
+ messages: { argsPath: 'Do not use paths with @args, use @argName directly instead.' },
+ originallyFrom: {
+ name: 'ember-template-lint',
+ rule: 'lib/rules/no-args-paths.js',
+ docs: 'docs/rule/no-args-paths.md',
+ tests: 'test/unit/rules/no-args-paths-test.js',
+ },
+ },
+ create(context) {
+ return {
+ GlimmerPathExpression(node) {
+ const path = node.original;
+ if (
+ path?.startsWith('@args.') ||
+ path?.startsWith('args.') ||
+ path?.startsWith('this.args.')
+ ) {
+ context.report({ node, messageId: 'argsPath' });
+ }
+ },
+ };
+ },
+};
diff --git a/tests/lib/rules/template-no-args-paths.js b/tests/lib/rules/template-no-args-paths.js
new file mode 100644
index 0000000000..5dbedcb0b2
--- /dev/null
+++ b/tests/lib/rules/template-no-args-paths.js
@@ -0,0 +1,131 @@
+const rule = require('../../../lib/rules/template-no-args-paths');
+const RuleTester = require('eslint').RuleTester;
+
+const ruleTester = new RuleTester({
+ parser: require.resolve('ember-eslint-parser'),
+ parserOptions: { ecmaVersion: 2022, sourceType: 'module' },
+});
+ruleTester.run('template-no-args-paths', rule, {
+ valid: [
+ '{{@foo}}',
+ '',
+ '{{foo (name this.args)}}',
+ '{{foo name=this.args}}',
+ '{{foo name=(extract this.args)}}',
+ '',
+ '',
+ '',
+ '',
+ ],
+ invalid: [
+ {
+ code: '{{@args.foo}}',
+ output: null,
+ errors: [{ messageId: 'argsPath' }],
+ },
+
+ {
+ code: '{{hello (format value=args.foo)}}',
+ output: null,
+ errors: [{ messageId: 'argsPath' }],
+ },
+ {
+ code: '{{hello value=args.foo}}',
+ output: null,
+ errors: [{ messageId: 'argsPath' }],
+ },
+ {
+ code: '{{hello (format args.foo.bar)}}',
+ output: null,
+ errors: [{ messageId: 'argsPath' }],
+ },
+ {
+ code: '
',
+ output: null,
+ errors: [{ messageId: 'argsPath' }],
+ },
+ {
+ code: '{{hello args.foo.bar}}',
+ output: null,
+ errors: [{ messageId: 'argsPath' }],
+ },
+ {
+ code: '{{args.foo.bar}}',
+ output: null,
+ errors: [{ messageId: 'argsPath' }],
+ },
+ {
+ code: '{{args.foo}}',
+ output: null,
+ errors: [{ messageId: 'argsPath' }],
+ },
+ {
+ code: '{{this.args.foo}}',
+ output: null,
+ errors: [{ messageId: 'argsPath' }],
+ },
+ ],
+});
+
+const hbsRuleTester = new RuleTester({
+ parser: require.resolve('ember-eslint-parser/hbs'),
+ parserOptions: {
+ ecmaVersion: 2022,
+ sourceType: 'module',
+ },
+});
+
+hbsRuleTester.run('template-no-args-paths', rule, {
+ valid: [
+ '
',
+ '{{foo (name this.args)}}',
+ '{{foo name=this.args}}',
+ '{{foo name=(extract this.args)}}',
+ '',
+ '',
+ '',
+ '',
+ ],
+ invalid: [
+ {
+ code: '{{hello (format value=args.foo)}}',
+ output: null,
+ errors: [{ message: 'Do not use paths with @args, use @argName directly instead.' }],
+ },
+ {
+ code: '{{hello value=args.foo}}',
+ output: null,
+ errors: [{ message: 'Do not use paths with @args, use @argName directly instead.' }],
+ },
+ {
+ code: '{{hello (format args.foo.bar)}}',
+ output: null,
+ errors: [{ message: 'Do not use paths with @args, use @argName directly instead.' }],
+ },
+ {
+ code: '
',
+ output: null,
+ errors: [{ message: 'Do not use paths with @args, use @argName directly instead.' }],
+ },
+ {
+ code: '{{hello args.foo.bar}}',
+ output: null,
+ errors: [{ message: 'Do not use paths with @args, use @argName directly instead.' }],
+ },
+ {
+ code: '{{args.foo.bar}}',
+ output: null,
+ errors: [{ message: 'Do not use paths with @args, use @argName directly instead.' }],
+ },
+ {
+ code: '{{args.foo}}',
+ output: null,
+ errors: [{ message: 'Do not use paths with @args, use @argName directly instead.' }],
+ },
+ {
+ code: '{{this.args.foo}}',
+ output: null,
+ errors: [{ message: 'Do not use paths with @args, use @argName directly instead.' }],
+ },
+ ],
+});
From d9befc81dd94e8ef502ba7250444d057b5e92f9d Mon Sep 17 00:00:00 2001
From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com>
Date: Fri, 13 Mar 2026 14:09:32 -0400
Subject: [PATCH 2/2] Apply PR Feedback
---
README.md | 2 +-
docs/rules/template-no-args-paths.md | 2 +
lib/rules/template-no-args-paths.js | 79 ++++++++++++++++++++---
tests/lib/rules/template-no-args-paths.js | 62 +++++++++---------
4 files changed, 106 insertions(+), 39 deletions(-)
diff --git a/README.md b/README.md
index cd5aa94416..c2924ba272 100644
--- a/README.md
+++ b/README.md
@@ -202,7 +202,7 @@ rules in templates can be disabled with eslint directives with mustache or html
| [template-builtin-component-arguments](docs/rules/template-builtin-component-arguments.md) | disallow setting certain attributes on builtin components | | | |
| [template-no-action-modifiers](docs/rules/template-no-action-modifiers.md) | disallow usage of {{action}} modifiers | | | |
| [template-no-action-on-submit-button](docs/rules/template-no-action-on-submit-button.md) | disallow action attribute on submit buttons | | | |
-| [template-no-args-paths](docs/rules/template-no-args-paths.md) | disallow @args in paths | | | |
+| [template-no-args-paths](docs/rules/template-no-args-paths.md) | disallow args.foo paths in templates, use @foo instead | | 🔧 | |
| [template-no-arguments-for-html-elements](docs/rules/template-no-arguments-for-html-elements.md) | disallow @arguments on HTML elements | | | |
| [template-no-array-prototype-extensions](docs/rules/template-no-array-prototype-extensions.md) | disallow usage of Ember Array prototype extensions | | | |
| [template-no-at-ember-render-modifiers](docs/rules/template-no-at-ember-render-modifiers.md) | disallow usage of @ember/render-modifiers | | | |
diff --git a/docs/rules/template-no-args-paths.md b/docs/rules/template-no-args-paths.md
index bd51bce5c1..c8753d8e4e 100644
--- a/docs/rules/template-no-args-paths.md
+++ b/docs/rules/template-no-args-paths.md
@@ -1,5 +1,7 @@
# ember/template-no-args-paths
+🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
+
Arguments that are passed to components are prefixed with the `@` symbol in Angle bracket syntax.
diff --git a/lib/rules/template-no-args-paths.js b/lib/rules/template-no-args-paths.js
index 34a4bd78bc..e687bcb112 100644
--- a/lib/rules/template-no-args-paths.js
+++ b/lib/rules/template-no-args-paths.js
@@ -3,13 +3,17 @@ module.exports = {
meta: {
type: 'problem',
docs: {
- description: 'disallow @args in paths',
+ description: 'disallow args.foo paths in templates, use @foo instead',
category: 'Best Practices',
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-no-args-paths.md',
templateMode: 'both',
},
+ fixable: 'code',
schema: [],
- messages: { argsPath: 'Do not use paths with @args, use @argName directly instead.' },
+ messages: {
+ argsPath:
+ 'Component templates should avoid "{{path}}" usage, try "@{{replacement}}" instead.',
+ },
originallyFrom: {
name: 'ember-template-lint',
rule: 'lib/rules/no-args-paths.js',
@@ -18,16 +22,75 @@ module.exports = {
},
},
create(context) {
+ const localScopes = [];
+
+ function pushLocals(params) {
+ localScopes.push(new Set(params || []));
+ }
+
+ function popLocals() {
+ localScopes.pop();
+ }
+
+ function isLocal(name) {
+ for (const scope of localScopes) {
+ if (scope.has(name)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
return {
+ GlimmerBlockStatement(node) {
+ if (node.program && node.program.blockParams) {
+ pushLocals(node.program.blockParams);
+ }
+ },
+ 'GlimmerBlockStatement:exit'(node) {
+ if (node.program && node.program.blockParams) {
+ popLocals();
+ }
+ },
+
+ GlimmerElementNode(node) {
+ if (node.blockParams && node.blockParams.length > 0) {
+ pushLocals(node.blockParams);
+ }
+ },
+ 'GlimmerElementNode:exit'(node) {
+ if (node.blockParams && node.blockParams.length > 0) {
+ popLocals();
+ }
+ },
+
GlimmerPathExpression(node) {
const path = node.original;
- if (
- path?.startsWith('@args.') ||
- path?.startsWith('args.') ||
- path?.startsWith('this.args.')
- ) {
- context.report({ node, messageId: 'argsPath' });
+
+ // @args.foo is a valid named argument — skip paths starting with @
+ if (node.head?.type === 'AtHead') {
+ return;
+ }
+
+ if (!path?.startsWith('args.') && !path?.startsWith('this.args.')) {
+ return;
+ }
+
+ // Skip when 'args' is a block param in the current scope
+ if (isLocal('args')) {
+ return;
}
+
+ const replacement = path.replace(/^(this\.)?args\./, '');
+
+ context.report({
+ node,
+ messageId: 'argsPath',
+ data: { path, replacement },
+ fix(fixer) {
+ return fixer.replaceText(node, `@${replacement}`);
+ },
+ });
},
};
},
diff --git a/tests/lib/rules/template-no-args-paths.js b/tests/lib/rules/template-no-args-paths.js
index 5dbedcb0b2..649468c2f9 100644
--- a/tests/lib/rules/template-no-args-paths.js
+++ b/tests/lib/rules/template-no-args-paths.js
@@ -8,6 +8,8 @@ const ruleTester = new RuleTester({
ruleTester.run('template-no-args-paths', rule, {
valid: [
'{{@foo}}',
+ // @args.foo is a valid named argument, not a path violation
+ '{{@args.foo}}',
'',
'{{foo (name this.args)}}',
'{{foo name=this.args}}',
@@ -16,52 +18,48 @@ ruleTester.run('template-no-args-paths', rule, {
'',
'',
'',
+ // args as a block param is not flagged
+ '{{#each items as |args|}}{{args.name}}{{/each}}',
],
invalid: [
- {
- code: '{{@args.foo}}',
- output: null,
- errors: [{ messageId: 'argsPath' }],
- },
-
{
code: '{{hello (format value=args.foo)}}',
- output: null,
+ output: '{{hello (format value=@foo)}}',
errors: [{ messageId: 'argsPath' }],
},
{
code: '{{hello value=args.foo}}',
- output: null,
+ output: '{{hello value=@foo}}',
errors: [{ messageId: 'argsPath' }],
},
{
code: '{{hello (format args.foo.bar)}}',
- output: null,
+ output: '{{hello (format @foo.bar)}}',
errors: [{ messageId: 'argsPath' }],
},
{
code: '
',
- output: null,
+ output: '
',
errors: [{ messageId: 'argsPath' }],
},
{
code: '{{hello args.foo.bar}}',
- output: null,
+ output: '{{hello @foo.bar}}',
errors: [{ messageId: 'argsPath' }],
},
{
code: '{{args.foo.bar}}',
- output: null,
+ output: '{{@foo.bar}}',
errors: [{ messageId: 'argsPath' }],
},
{
code: '{{args.foo}}',
- output: null,
+ output: '{{@foo}}',
errors: [{ messageId: 'argsPath' }],
},
{
code: '{{this.args.foo}}',
- output: null,
+ output: '{{@foo}}',
errors: [{ messageId: 'argsPath' }],
},
],
@@ -77,6 +75,8 @@ const hbsRuleTester = new RuleTester({
hbsRuleTester.run('template-no-args-paths', rule, {
valid: [
+ // @args.foo is a valid named argument
+ '{{@args.foo}}',
'',
'{{foo (name this.args)}}',
'{{foo name=this.args}}',
@@ -85,47 +85,49 @@ hbsRuleTester.run('template-no-args-paths', rule, {
'',
'',
'',
+ // args as a block param is not flagged
+ '{{#each items as |args|}}{{args.name}}{{/each}}',
],
invalid: [
{
code: '{{hello (format value=args.foo)}}',
- output: null,
- errors: [{ message: 'Do not use paths with @args, use @argName directly instead.' }],
+ output: '{{hello (format value=@foo)}}',
+ errors: [{ messageId: 'argsPath' }],
},
{
code: '{{hello value=args.foo}}',
- output: null,
- errors: [{ message: 'Do not use paths with @args, use @argName directly instead.' }],
+ output: '{{hello value=@foo}}',
+ errors: [{ messageId: 'argsPath' }],
},
{
code: '{{hello (format args.foo.bar)}}',
- output: null,
- errors: [{ message: 'Do not use paths with @args, use @argName directly instead.' }],
+ output: '{{hello (format @foo.bar)}}',
+ errors: [{ messageId: 'argsPath' }],
},
{
code: '
',
- output: null,
- errors: [{ message: 'Do not use paths with @args, use @argName directly instead.' }],
+ output: '
',
+ errors: [{ messageId: 'argsPath' }],
},
{
code: '{{hello args.foo.bar}}',
- output: null,
- errors: [{ message: 'Do not use paths with @args, use @argName directly instead.' }],
+ output: '{{hello @foo.bar}}',
+ errors: [{ messageId: 'argsPath' }],
},
{
code: '{{args.foo.bar}}',
- output: null,
- errors: [{ message: 'Do not use paths with @args, use @argName directly instead.' }],
+ output: '{{@foo.bar}}',
+ errors: [{ messageId: 'argsPath' }],
},
{
code: '{{args.foo}}',
- output: null,
- errors: [{ message: 'Do not use paths with @args, use @argName directly instead.' }],
+ output: '{{@foo}}',
+ errors: [{ messageId: 'argsPath' }],
},
{
code: '{{this.args.foo}}',
- output: null,
- errors: [{ message: 'Do not use paths with @args, use @argName directly instead.' }],
+ output: '{{@foo}}',
+ errors: [{ messageId: 'argsPath' }],
},
],
});