From d70c4bf707c977f48e0a9f2dd5a5168cfc8e055c Mon Sep 17 00:00:00 2001
From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com>
Date: Fri, 20 Feb 2026 11:59:09 -0500
Subject: [PATCH] Extract rule: template-no-dynamic-subexpression-invocations
---
README.md | 17 ++---
...te-no-dynamic-subexpression-invocations.md | 62 +++++++++++++++++++
...te-no-dynamic-subexpression-invocations.js | 59 ++++++++++++++++++
...te-no-dynamic-subexpression-invocations.js | 48 ++++++++++++++
4 files changed, 178 insertions(+), 8 deletions(-)
create mode 100644 docs/rules/template-no-dynamic-subexpression-invocations.md
create mode 100644 lib/rules/template-no-dynamic-subexpression-invocations.js
create mode 100644 tests/lib/rules/template-no-dynamic-subexpression-invocations.js
diff --git a/README.md b/README.md
index 5a853147d1..5d7358f097 100644
--- a/README.md
+++ b/README.md
@@ -189,14 +189,15 @@ rules in templates can be disabled with eslint directives with mustache or html
### Best Practices
-| Name | Description | 💼 | 🔧 | 💡 |
-| :----------------------------------------------------------------------------------------------- | :-------------------------------------------------------- | :- | :- | :- |
-| [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-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-debugger](docs/rules/template-no-debugger.md) | disallow {{debugger}} in templates | | | |
-| [template-no-log](docs/rules/template-no-log.md) | disallow {{log}} in templates | | | |
+| Name | Description | 💼 | 🔧 | 💡 |
+| :----------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------- | :- | :- | :- |
+| [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-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-debugger](docs/rules/template-no-debugger.md) | disallow {{debugger}} in templates | | | |
+| [template-no-dynamic-subexpression-invocations](docs/rules/template-no-dynamic-subexpression-invocations.md) | disallow dynamic subexpression invocations | | | |
+| [template-no-log](docs/rules/template-no-log.md) | disallow {{log}} in templates | | | |
### Components
diff --git a/docs/rules/template-no-dynamic-subexpression-invocations.md b/docs/rules/template-no-dynamic-subexpression-invocations.md
new file mode 100644
index 0000000000..699f304a4e
--- /dev/null
+++ b/docs/rules/template-no-dynamic-subexpression-invocations.md
@@ -0,0 +1,62 @@
+# ember/template-no-dynamic-subexpression-invocations
+
+
+
+Disallow dynamic helper invocations.
+
+Dynamic helper invocations (where the helper name comes from a property or argument) make code harder to understand and can have performance implications. Use explicit helper names instead.
+
+## Rule Details
+
+This rule disallows invoking helpers dynamically using `this` or `@` properties.
+
+## Examples
+
+### Incorrect ❌
+
+```gjs
+
+ {{(this.helper "arg")}}
+
+```
+
+```gjs
+
+ {{(@helperName "value")}}
+
+```
+
+```gjs
+
+ {{this.formatter this.data}}
+
+```
+
+### Correct ✅
+
+```gjs
+
+ {{format-date this.date}}
+
+```
+
+```gjs
+
+ {{(upper-case this.name)}}
+
+```
+
+```gjs
+
+ {{this.formattedData}}
+
+```
+
+## Related Rules
+
+- [template-no-implicit-this](./template-no-implicit-this.md)
+
+## References
+
+- [Ember Guides - Template Helpers](https://guides.emberjs.com/release/components/helper-functions/)
+- [eslint-plugin-ember template-no-dynamic-subexpression-invocations](https://github.com/ember-cli/eslint-plugin-ember/blob/master/docs/rules/template-no-dynamic-subexpression-invocations.md)
diff --git a/lib/rules/template-no-dynamic-subexpression-invocations.js b/lib/rules/template-no-dynamic-subexpression-invocations.js
new file mode 100644
index 0000000000..bbe36d76b9
--- /dev/null
+++ b/lib/rules/template-no-dynamic-subexpression-invocations.js
@@ -0,0 +1,59 @@
+/** @type {import('eslint').Rule.RuleModule} */
+module.exports = {
+ meta: {
+ type: 'problem',
+ docs: {
+ description: 'disallow dynamic subexpression invocations',
+ category: 'Best Practices',
+ strictGjs: true,
+ strictGts: true,
+ url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-no-dynamic-subexpression-invocations.md',
+ },
+ fixable: null,
+ schema: [],
+ messages: {
+ noDynamicSubexpressionInvocations:
+ 'Do not use dynamic helper invocations. Use explicit helper names instead.',
+ },
+ },
+
+ create(context) {
+ return {
+ GlimmerSubExpression(node) {
+ // Check if the path is dynamic (contains @ or this)
+ if (
+ node.path &&
+ node.path.type === 'GlimmerPathExpression' &&
+ (node.path.head?.type === 'AtHead' ||
+ node.path.head?.type === 'ThisHead' ||
+ node.path.parts?.length > 0)
+ ) {
+ // If it's not a simple identifier, it's dynamic
+ if (node.path.head?.type === 'AtHead' || node.path.head?.type === 'ThisHead') {
+ context.report({
+ node,
+ messageId: 'noDynamicSubexpressionInvocations',
+ });
+ }
+ }
+ },
+ GlimmerMustacheStatement(node) {
+ // Check for dynamic invocations in mustache statements
+ if (
+ node.path &&
+ node.path.type === 'GlimmerPathExpression' &&
+ node.params &&
+ node.params.length > 0
+ ) {
+ // If the helper name starts with @ or this, it's dynamic
+ if (node.path.head?.type === 'AtHead' || node.path.head?.type === 'ThisHead') {
+ context.report({
+ node,
+ messageId: 'noDynamicSubexpressionInvocations',
+ });
+ }
+ }
+ },
+ };
+ },
+};
diff --git a/tests/lib/rules/template-no-dynamic-subexpression-invocations.js b/tests/lib/rules/template-no-dynamic-subexpression-invocations.js
new file mode 100644
index 0000000000..44b5c59a4c
--- /dev/null
+++ b/tests/lib/rules/template-no-dynamic-subexpression-invocations.js
@@ -0,0 +1,48 @@
+const rule = require('../../../lib/rules/template-no-dynamic-subexpression-invocations');
+const RuleTester = require('eslint').RuleTester;
+
+const ruleTester = new RuleTester({
+ parser: require.resolve('ember-eslint-parser'),
+ parserOptions: { ecmaVersion: 2022, sourceType: 'module' },
+});
+
+ruleTester.run('template-no-dynamic-subexpression-invocations', rule, {
+ valid: [
+ '{{format-date this.date}}',
+ '{{(upper-case this.name)}}',
+ '{{helper "static"}}',
+ ],
+
+ invalid: [
+ {
+ code: '{{(this.helper "arg")}}',
+ output: null,
+ errors: [
+ {
+ message: 'Do not use dynamic helper invocations. Use explicit helper names instead.',
+ type: 'GlimmerSubExpression',
+ },
+ ],
+ },
+ {
+ code: '{{(@helperName "value")}}',
+ output: null,
+ errors: [
+ {
+ message: 'Do not use dynamic helper invocations. Use explicit helper names instead.',
+ type: 'GlimmerSubExpression',
+ },
+ ],
+ },
+ {
+ code: '{{this.formatter this.data}}',
+ output: null,
+ errors: [
+ {
+ message: 'Do not use dynamic helper invocations. Use explicit helper names instead.',
+ type: 'GlimmerMustacheStatement',
+ },
+ ],
+ },
+ ],
+});