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..a9044f146e --- /dev/null +++ b/docs/rules/template-no-unbound.md @@ -0,0 +1,31 @@ +# 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: + +```gjs + +``` + +```gjs + +``` + +## 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..29eb9d2c07 --- /dev/null +++ b/lib/rules/template-no-unbound.js @@ -0,0 +1,36 @@ +/** @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 {{unboundHelper}} 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', + data: { unboundHelper: '{{unbound}}' }, + }); + } + } + 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..5a7013f477 --- /dev/null +++ b/tests/lib/rules/template-no-unbound.js @@ -0,0 +1,53 @@ +const rule = require('../../../lib/rules/template-no-unbound'); +const RuleTester = require('eslint').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' }, +}); + +gjsRuleTester.run('template-no-unbound', rule, { + valid: validHbs.map(wrapTemplate), + invalid: invalidHbs.map(wrapTemplate), +}); + +const hbsRuleTester = new RuleTester({ + parser: require.resolve('ember-eslint-parser/hbs'), + parserOptions: { + ecmaVersion: 2022, + sourceType: 'module', + }, +}); + +hbsRuleTester.run('template-no-unbound', rule, { + valid: validHbs, + invalid: invalidHbs, +});