From 3ba27aa0a7b4f02a704e3acccc13a065019ef899 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Wed, 18 Mar 2026 17:09:49 -0400 Subject: [PATCH 1/2] Extract rule: template-no-unbound --- README.md | 1 + docs/rules/template-no-unbound.md | 27 ++++++++++++++ lib/rules/template-no-unbound.js | 32 ++++++++++++++++ tests/lib/rules/template-no-unbound.js | 51 ++++++++++++++++++++++++++ 4 files changed, 111 insertions(+) create mode 100644 docs/rules/template-no-unbound.md create mode 100644 lib/rules/template-no-unbound.js create mode 100644 tests/lib/rules/template-no-unbound.js diff --git a/README.md b/README.md index a5a3d3070f..b2e4f6832e 100644 --- a/README.md +++ b/README.md @@ -340,6 +340,7 @@ rules in templates can be disabled with eslint directives with mustache or html | [template-no-attrs-in-components](docs/rules/template-no-attrs-in-components.md) | disallow attrs in component templates | | | | | [template-no-link-to-positional-params](docs/rules/template-no-link-to-positional-params.md) | disallow positional params in LinkTo component | | | | | [template-no-link-to-tagname](docs/rules/template-no-link-to-tagname.md) | disallow tagName attribute on LinkTo component | | | | +| [template-no-unbound](docs/rules/template-no-unbound.md) | disallow {{unbound}} helper | | | | | [template-no-with](docs/rules/template-no-with.md) | disallow {{with}} helper | | | | ### Ember Data diff --git a/docs/rules/template-no-unbound.md b/docs/rules/template-no-unbound.md new file mode 100644 index 0000000000..5fe5e7e49e --- /dev/null +++ b/docs/rules/template-no-unbound.md @@ -0,0 +1,27 @@ +# ember/template-no-unbound + +> **HBS Only**: This rule applies to classic `.hbs` template files only (loose mode). It is not relevant for `gjs`/`gts` files (strict mode), where these patterns cannot occur. + + + +`{{unbound}}` is a legacy hold over from the days in which Ember's template engine was less performant. Its use today +is vestigial, and it no longer offers performance benefits. + +It is also a poor practice to use it for rendering only the initial value of a property that may later change. + +## Examples + +This rule **forbids** the following: + +```hbs +{{unbound aVar}} +``` + +```hbs +{{some-component foo=(unbound aVar)}} +``` + +## References + +- [deprecations/unbound block syntax](https://deprecations.emberjs.com/v1.x/#toc_block-and-multi-argument-unbound-helper) +- [Ember api/unbound helper](https://api.emberjs.com/ember/release/classes/Ember.Templates.helpers/methods/each?anchor=unbound) diff --git a/lib/rules/template-no-unbound.js b/lib/rules/template-no-unbound.js new file mode 100644 index 0000000000..9d15a18cfa --- /dev/null +++ b/lib/rules/template-no-unbound.js @@ -0,0 +1,32 @@ +/** @type {import('eslint').Rule.RuleModule} */ +module.exports = { + meta: { + type: 'suggestion', + docs: { + description: 'disallow {{unbound}} helper', + category: 'Deprecations', + url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-no-unbound.md', + templateMode: 'loose', + }, + schema: [], + messages: { unexpected: 'Unexpected unbound helper usage.' }, + originallyFrom: { + name: 'ember-template-lint', + rule: 'lib/rules/no-unbound.js', + docs: 'docs/rule/no-unbound.md', + tests: 'test/unit/rules/no-unbound-test.js', + }, + }, + create(context) { + function check(node) { + if (node.path?.type === 'GlimmerPathExpression' && node.path.original === 'unbound') { + context.report({ node, messageId: 'unexpected' }); + } + } + return { + GlimmerMustacheStatement: check, + GlimmerBlockStatement: check, + GlimmerSubExpression: check, + }; + }, +}; diff --git a/tests/lib/rules/template-no-unbound.js b/tests/lib/rules/template-no-unbound.js new file mode 100644 index 0000000000..c2035cc066 --- /dev/null +++ b/tests/lib/rules/template-no-unbound.js @@ -0,0 +1,51 @@ +const rule = require('../../../lib/rules/template-no-unbound'); +const RuleTester = require('eslint').RuleTester; + +const ruleTester = new RuleTester({ + parser: require.resolve('ember-eslint-parser'), + parserOptions: { ecmaVersion: 2022, sourceType: 'module' }, +}); +ruleTester.run('template-no-unbound', rule, { + valid: [ + '', + '', + '', + ], + invalid: [ + { + code: '', + output: null, + errors: [{ messageId: 'unexpected' }], + }, + + { + code: '', + output: null, + errors: [{ messageId: 'unexpected' }], + }, + ], +}); + +const hbsRuleTester = new RuleTester({ + parser: require.resolve('ember-eslint-parser/hbs'), + parserOptions: { + ecmaVersion: 2022, + sourceType: 'module', + }, +}); + +hbsRuleTester.run('template-no-unbound', rule, { + valid: ['{{foo}}', '{{button}}'], + invalid: [ + { + code: '{{unbound foo}}', + output: null, + errors: [{ message: 'Unexpected unbound helper usage.' }], + }, + { + code: '{{my-thing foo=(unbound foo)}}', + output: null, + errors: [{ message: 'Unexpected unbound helper usage.' }], + }, + ], +}); From 1afad8d12610662d80ee0af7b49ad0de8de988a7 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Sat, 21 Mar 2026 00:02:03 -0400 Subject: [PATCH 2/2] Sync with template-lint --- docs/rules/template-no-unbound.md | 12 +++-- lib/rules/template-no-unbound.js | 8 ++- tests/lib/rules/template-no-unbound.js | 68 +++++++++++++------------- 3 files changed, 49 insertions(+), 39 deletions(-) diff --git a/docs/rules/template-no-unbound.md b/docs/rules/template-no-unbound.md index 5fe5e7e49e..a9044f146e 100644 --- a/docs/rules/template-no-unbound.md +++ b/docs/rules/template-no-unbound.md @@ -13,12 +13,16 @@ It is also a poor practice to use it for rendering only the initial value of a p This rule **forbids** the following: -```hbs -{{unbound aVar}} +```gjs + ``` -```hbs -{{some-component foo=(unbound aVar)}} +```gjs + ``` ## References diff --git a/lib/rules/template-no-unbound.js b/lib/rules/template-no-unbound.js index 9d15a18cfa..29eb9d2c07 100644 --- a/lib/rules/template-no-unbound.js +++ b/lib/rules/template-no-unbound.js @@ -9,7 +9,7 @@ module.exports = { templateMode: 'loose', }, schema: [], - messages: { unexpected: 'Unexpected unbound helper usage.' }, + messages: { unexpected: 'Unexpected {{unboundHelper}} usage.' }, originallyFrom: { name: 'ember-template-lint', rule: 'lib/rules/no-unbound.js', @@ -20,7 +20,11 @@ module.exports = { create(context) { function check(node) { if (node.path?.type === 'GlimmerPathExpression' && node.path.original === 'unbound') { - context.report({ node, messageId: 'unexpected' }); + context.report({ + node, + messageId: 'unexpected', + data: { unboundHelper: '{{unbound}}' }, + }); } } return { diff --git a/tests/lib/rules/template-no-unbound.js b/tests/lib/rules/template-no-unbound.js index c2035cc066..5a7013f477 100644 --- a/tests/lib/rules/template-no-unbound.js +++ b/tests/lib/rules/template-no-unbound.js @@ -1,29 +1,42 @@ const rule = require('../../../lib/rules/template-no-unbound'); const RuleTester = require('eslint').RuleTester; -const ruleTester = new RuleTester({ +const validHbs = ['{{foo}}', '{{button}}']; + +const invalidHbs = [ + { + code: '{{unbound foo}}', + output: null, + errors: [{ message: 'Unexpected {{unbound}} usage.' }], + }, + { + code: '{{my-thing foo=(unbound foo)}}', + output: null, + errors: [{ message: 'Unexpected {{unbound}} usage.' }], + }, +]; + +function wrapTemplate(entry) { + if (typeof entry === 'string') { + return ``; + } + + return { + ...entry, + code: ``, + output: entry.output ? `` : entry.output, + errors: entry.errors.map(() => ({ messageId: 'unexpected' })), + }; +} + +const gjsRuleTester = new RuleTester({ parser: require.resolve('ember-eslint-parser'), parserOptions: { ecmaVersion: 2022, sourceType: 'module' }, }); -ruleTester.run('template-no-unbound', rule, { - valid: [ - '', - '', - '', - ], - invalid: [ - { - code: '', - output: null, - errors: [{ messageId: 'unexpected' }], - }, - - { - code: '', - output: null, - errors: [{ messageId: 'unexpected' }], - }, - ], + +gjsRuleTester.run('template-no-unbound', rule, { + valid: validHbs.map(wrapTemplate), + invalid: invalidHbs.map(wrapTemplate), }); const hbsRuleTester = new RuleTester({ @@ -35,17 +48,6 @@ const hbsRuleTester = new RuleTester({ }); hbsRuleTester.run('template-no-unbound', rule, { - valid: ['{{foo}}', '{{button}}'], - invalid: [ - { - code: '{{unbound foo}}', - output: null, - errors: [{ message: 'Unexpected unbound helper usage.' }], - }, - { - code: '{{my-thing foo=(unbound foo)}}', - output: null, - errors: [{ message: 'Unexpected unbound helper usage.' }], - }, - ], + valid: validHbs, + invalid: invalidHbs, });