diff --git a/docs/rules/template-no-action.md b/docs/rules/template-no-action.md
index 3b6f043820..fe0fa16f79 100644
--- a/docs/rules/template-no-action.md
+++ b/docs/rules/template-no-action.md
@@ -42,6 +42,25 @@ Examples of **correct** code for this rule:
```
+```gjs
+import action from './my-action-helper';
+
+ {{action this.handleClick}}
+
+```
+
+```gjs
+
+ {{#each items as |action|}}
+
+ {{/each}}
+
+```
+
+## Strict-mode behavior
+
+`action` is an ambient strict-mode keyword in Ember (registered in `STRICT_MODE_KEYWORDS`), so `{{action this.x}}` works in `.gjs`/`.gts` templates without an explicit import. The rule still flags those uses to discourage the deprecated keyword — but skips reports when `action` resolves to a JS-scope binding (an import or local declaration) or a template block param.
+
## Migration
- Replace `(action "methodName")` with method references or `(fn this.methodName)`
diff --git a/lib/rules/template-no-action.js b/lib/rules/template-no-action.js
index 007ba7eaa4..d0f8375917 100644
--- a/lib/rules/template-no-action.js
+++ b/lib/rules/template-no-action.js
@@ -1,20 +1,19 @@
-function isActionHelper(node) {
+function isActionHelperPath(node) {
if (!node.path || node.path.type !== 'GlimmerPathExpression') {
return false;
}
// Check if it's the action helper (not this.action or @action)
const path = node.path;
- if (path.original === 'action') {
- // Check head.type to avoid deprecated data/this properties
- const head = path.head;
- if (head && (head.type === 'AtHead' || head.type === 'ThisHead')) {
- return false;
- }
- return true;
+ if (path.original !== 'action') {
+ return false;
}
-
- return false;
+ // Avoid deprecated data/this properties — those are not the helper.
+ const head = path.head;
+ if (head && (head.type === 'AtHead' || head.type === 'ThisHead')) {
+ return false;
+ }
+ return true;
}
/** @type {import('eslint').Rule.RuleModule} */
@@ -31,39 +30,61 @@ module.exports = {
schema: [],
messages: {
subExpression:
- 'Do not use `action` as (action ...). Instead, use the `on` modifier and `fn` helper.',
- mustache: 'Do not use `action` in templates. Instead, use the `on` modifier and `fn` helper.',
+ 'Do not use `action` as (action ...) — deprecated in Ember 5.9, removed in 6.0. Use the `fn` helper instead.',
+ mustache:
+ 'Do not use `action` in templates — deprecated in Ember 5.9, removed in 6.0. Use the `on` modifier and `fn` helper instead.',
modifier:
- 'Do not use `action` as an element modifier. Instead, use the `on` modifier and `fn` helper.',
+ 'Do not use `action` as an element modifier — deprecated in Ember 5.9, removed in 6.0. Use the `on` modifier instead.',
},
},
create(context) {
+ // `action` is an ambient strict-mode keyword in Ember (registered in
+ // STRICT_MODE_KEYWORDS), so `{{action this.x}}` works in .gjs/.gts without
+ // an import. Still flag the ambient keyword everywhere — but skip when
+ // `action` resolves to a binding (JS import/const, or template block param).
+ // ember-eslint-parser already registers template block params in scope, so
+ // a single getScope walk covers both.
+ const sourceCode = context.sourceCode;
+
+ function isInScope(node) {
+ if (!sourceCode) {
+ return false;
+ }
+ try {
+ let scope = sourceCode.getScope(node);
+ while (scope) {
+ if (scope.variables.some((v) => v.name === 'action')) {
+ return true;
+ }
+ scope = scope.upper;
+ }
+ } catch {
+ // getScope not available in .hbs-only mode
+ }
+ return false;
+ }
+
+ function shouldFlag(node) {
+ return isActionHelperPath(node) && !isInScope(node);
+ }
+
return {
GlimmerSubExpression(node) {
- if (isActionHelper(node)) {
- context.report({
- node,
- messageId: 'subExpression',
- });
+ if (shouldFlag(node)) {
+ context.report({ node, messageId: 'subExpression' });
}
},
GlimmerMustacheStatement(node) {
- if (isActionHelper(node)) {
- context.report({
- node,
- messageId: 'mustache',
- });
+ if (shouldFlag(node)) {
+ context.report({ node, messageId: 'mustache' });
}
},
GlimmerElementModifierStatement(node) {
- if (isActionHelper(node)) {
- context.report({
- node,
- messageId: 'modifier',
- });
+ if (shouldFlag(node)) {
+ context.report({ node, messageId: 'modifier' });
}
},
};
diff --git a/tests/lib/rules/template-no-action.js b/tests/lib/rules/template-no-action.js
index 769955b730..d8559b9ae7 100644
--- a/tests/lib/rules/template-no-action.js
+++ b/tests/lib/rules/template-no-action.js
@@ -28,6 +28,37 @@ ruleTester.run('template-no-action', rule, {
`
{{@action}}
`,
+
+ // GJS/GTS: a JS-scope binding shadows the ambient `action` keyword. The
+ // user has imported (or declared) their own `action`, so flagging would
+ // corrupt their reference.
+ {
+ filename: 'test.gjs',
+ code: "import action from './my-action';\n{{action this.handleClick}}",
+ },
+ {
+ filename: 'test.gjs',
+ code: "import action from './my-action';\n{{(action this.handleClick)}}",
+ },
+ {
+ filename: 'test.gjs',
+ code: "import action from './my-action';\n",
+ },
+ {
+ filename: 'test.gts',
+ code: "import action from './my-action';\n{{action this.handleClick}}",
+ },
+ {
+ filename: 'test.gjs',
+ code: 'const action = (h) => () => h();\n{{action this.handleClick}}',
+ },
+
+ // Template block-param shadowing — `action` is the iterator/let-bound
+ // value, not the ambient keyword.
+ '{{#each items as |action|}}{{action this.x}}{{/each}}',
+ '{{#let (component "x") as |action|}}{{action this.x}}{{/let}}',
+ '{{#each items as |action|}}{{/each}}',
+ '{{action this.x}}',
],
invalid: [
@@ -39,7 +70,7 @@ ruleTester.run('template-no-action', rule, {
errors: [
{
message:
- 'Do not use `action` as (action ...). Instead, use the `on` modifier and `fn` helper.',
+ 'Do not use `action` as (action ...) — deprecated in Ember 5.9, removed in 6.0. Use the `fn` helper instead.',
type: 'GlimmerSubExpression',
},
],
@@ -52,7 +83,7 @@ ruleTester.run('template-no-action', rule, {
errors: [
{
message:
- 'Do not use `action` in templates. Instead, use the `on` modifier and `fn` helper.',
+ 'Do not use `action` in templates — deprecated in Ember 5.9, removed in 6.0. Use the `on` modifier and `fn` helper instead.',
type: 'GlimmerMustacheStatement',
},
],
@@ -65,7 +96,7 @@ ruleTester.run('template-no-action', rule, {
errors: [
{
message:
- 'Do not use `action` as an element modifier. Instead, use the `on` modifier and `fn` helper.',
+ 'Do not use `action` as an element modifier — deprecated in Ember 5.9, removed in 6.0. Use the `on` modifier instead.',
type: 'GlimmerElementModifierStatement',
},
],
@@ -78,10 +109,49 @@ ruleTester.run('template-no-action', rule, {
errors: [
{
message:
- 'Do not use `action` in templates. Instead, use the `on` modifier and `fn` helper.',
+ 'Do not use `action` in templates — deprecated in Ember 5.9, removed in 6.0. Use the `on` modifier and `fn` helper instead.',
type: 'GlimmerMustacheStatement',
},
],
},
+
+ // GJS/GTS: ambient `action` keyword usage with no shadowing import or
+ // block param. Still flagged.
+ {
+ filename: 'test.gjs',
+ code: '{{action "save"}}',
+ output: null,
+ errors: [{ messageId: 'mustache', type: 'GlimmerMustacheStatement' }],
+ },
+ {
+ filename: 'test.gjs',
+ code: '',
+ output: null,
+ errors: [{ messageId: 'subExpression', type: 'GlimmerSubExpression' }],
+ },
+ {
+ filename: 'test.gts',
+ code: '',
+ output: null,
+ errors: [{ messageId: 'modifier', type: 'GlimmerElementModifierStatement' }],
+ },
+
+ // Unrelated JS bindings do NOT mask the rule. Only a binding named
+ // `action` should be respected.
+ {
+ filename: 'test.gjs',
+ code: "import handler from './handler';\n{{action this.x}}",
+ output: null,
+ errors: [{ messageId: 'mustache' }],
+ },
+
+ // Ambient `action` outside a block-param scope is still flagged, even
+ // when an inner block legitimately shadows it.
+ {
+ filename: 'test.gjs',
+ code: '{{#each items as |action|}}{{action this.x}}{{/each}}{{action this.y}}',
+ output: null,
+ errors: [{ messageId: 'mustache' }],
+ },
],
});