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 + +``` + +```gjs + +``` + +```gjs + +``` + +### Correct ✅ + +```gjs + +``` + +```gjs + +``` + +```gjs + +``` + +## 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: [ + '', + '', + '', + ], + + invalid: [ + { + code: '', + output: null, + errors: [ + { + message: 'Do not use dynamic helper invocations. Use explicit helper names instead.', + type: 'GlimmerSubExpression', + }, + ], + }, + { + code: '', + output: null, + errors: [ + { + message: 'Do not use dynamic helper invocations. Use explicit helper names instead.', + type: 'GlimmerSubExpression', + }, + ], + }, + { + code: '', + output: null, + errors: [ + { + message: 'Do not use dynamic helper invocations. Use explicit helper names instead.', + type: 'GlimmerMustacheStatement', + }, + ], + }, + ], +});