From 003543c807c54d869bee28150b2fb9b189eef356 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20R=C3=B8ed?= Date: Mon, 13 Apr 2026 13:12:53 +0200 Subject: [PATCH 1/3] Fix template-no-action false positive in GJS/GTS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `action` is an ambient strict-mode keyword in Ember (registered in STRICT_MODE_KEYWORDS at @ember/template-compiler/lib/plugins/index.ts), so `{{action this.x}}` works in .gjs/.gts templates without an import. The rule should still flag those uses — its purpose is to discourage the deprecated keyword everywhere — but skip cases where `action` resolves to a JS-scope binding or a template block param: - `import action from './my-action-helper'; {{action this.x}}` (user import) - `const action = (h) => () => h(); {{action this.x}}` (local declaration) - `{{#each items as |action|}}{{action this.x}}{{/each}}` (block-param shadow) - `{{action this.x}}` (element-block-param shadow) Add the same JS-scope-walk + block-param tracking pattern used by template-no-log, template-no-unbound, etc.: walk sourceCode.getScope().variables for the JS side, and push/pop GlimmerBlockStatement.program.blockParams and GlimmerElementNode.blockParams for template-side scopes. The `@action` and `this.action` head-type checks are unchanged (those are JS-side namespaces, not the template keyword). Tests: 22 (was 8) — adds 9 new valid cases (5 JS-scope, 4 block-param) and 5 new invalid cases (3 ambient-keyword in .gjs/.gts, 1 unrelated import does not mask, 1 ambient-after-block-exit). Docs updated to document the strict-mode behavior. --- docs/rules/template-no-action.md | 19 ++++++++ lib/rules/template-no-action.js | 70 +++++++++++++++++---------- tests/lib/rules/template-no-action.js | 70 +++++++++++++++++++++++++++ 3 files changed, 134 insertions(+), 25 deletions(-) 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'; + +``` + +```gjs + +``` + +## 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..965e31f4d6 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} */ @@ -39,31 +38,52 @@ module.exports = { }, 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..b1686f1a07 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, { ``, + + // 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", + }, + { + filename: 'test.gjs', + code: "import action from './my-action';\n", + }, + { + filename: 'test.gjs', + code: "import action from './my-action';\n", + }, + { + filename: 'test.gts', + code: "import action from './my-action';\n", + }, + { + filename: 'test.gjs', + code: 'const action = (h) => () => h();\n', + }, + + // Template block-param shadowing — `action` is the iterator/let-bound + // value, not the ambient keyword. + '', + '', + '', + '', ], invalid: [ @@ -83,5 +114,44 @@ ruleTester.run('template-no-action', rule, { }, ], }, + + // GJS/GTS: ambient `action` keyword usage with no shadowing import or + // block param. Still flagged. + { + filename: 'test.gjs', + code: '', + 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", + 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: '', + output: null, + errors: [{ messageId: 'mustache' }], + }, ], }); From eb79415cdfebf30867155404a5d0db329da08991 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20R=C3=B8ed?= Date: Mon, 13 Apr 2026 20:36:30 +0200 Subject: [PATCH 2/3] Add deprecation version to action error messages {{action}} was deprecated in Ember 5.9 and removed in 6.0. Mention this in all three message variants so users know the timeline without having to look it up. --- lib/rules/template-no-action.js | 7 ++++--- tests/lib/rules/template-no-action.js | 8 ++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/rules/template-no-action.js b/lib/rules/template-no-action.js index 965e31f4d6..d0f8375917 100644 --- a/lib/rules/template-no-action.js +++ b/lib/rules/template-no-action.js @@ -30,10 +30,11 @@ 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.', }, }, diff --git a/tests/lib/rules/template-no-action.js b/tests/lib/rules/template-no-action.js index b1686f1a07..536cffe06e 100644 --- a/tests/lib/rules/template-no-action.js +++ b/tests/lib/rules/template-no-action.js @@ -70,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', }, ], @@ -83,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', }, ], @@ -96,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', }, ], @@ -109,7 +109,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', }, ], From a7a1be923b8e7ca4bec2c1b1a02796dea344b15e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20R=C3=B8ed?= Date: Mon, 13 Apr 2026 20:44:49 +0200 Subject: [PATCH 3/3] Fix no-useless-escape: remove accidental backslash-escaping of backticks --- tests/lib/rules/template-no-action.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/lib/rules/template-no-action.js b/tests/lib/rules/template-no-action.js index 536cffe06e..d8559b9ae7 100644 --- a/tests/lib/rules/template-no-action.js +++ b/tests/lib/rules/template-no-action.js @@ -70,7 +70,7 @@ ruleTester.run('template-no-action', rule, { errors: [ { message: - 'Do not use \`action\` as (action ...) — deprecated in Ember 5.9, removed in 6.0. Use the \`fn\` helper instead.', + 'Do not use `action` as (action ...) — deprecated in Ember 5.9, removed in 6.0. Use the `fn` helper instead.', type: 'GlimmerSubExpression', }, ],