diff --git a/README.md b/README.md index 1ad0042ccb..473fcef55b 100644 --- a/README.md +++ b/README.md @@ -198,6 +198,7 @@ rules in templates can be disabled with eslint directives with mustache or html | [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-block-params](docs/rules/template-no-block-params.md) | disallow the use of block params (`as \|...\|`) | | | | | [template-no-block-params-for-html-elements](docs/rules/template-no-block-params-for-html-elements.md) | disallow block params on HTML elements | | | | | [template-no-capital-arguments](docs/rules/template-no-capital-arguments.md) | disallow capital arguments (use lowercase @arg instead of @Arg) | | | | | [template-no-chained-this](docs/rules/template-no-chained-this.md) | disallow redundant `this.this` in templates | | 🔧 | | diff --git a/docs/rules/template-no-block-params.md b/docs/rules/template-no-block-params.md new file mode 100644 index 0000000000..bcb057bc55 --- /dev/null +++ b/docs/rules/template-no-block-params.md @@ -0,0 +1,85 @@ +# ember/template-no-block-params + + + +Disallow the use of block params (`as |...|`). + +## Rule Details + +This rule disallows all usage of block params syntax (`as |...|`) in templates. This includes: + +- Angle-bracket component invocations: `` +- Curly component invocations: `{{#my-component as |val|}}` +- Built-in helpers: `{{#each items as |item|}}`, `{{#let val as |v|}}` +- HTML elements: `
` + +This is a strict rule for codebases that want to completely avoid block params in favor of alternative patterns (e.g., contextual helpers, direct property access, or explicit argument passing). + +## Examples + +Examples of **incorrect** code for this rule: + +```gjs + +``` + +```gjs + +``` + +```gjs + +``` + +```gjs + +``` + +Examples of **correct** code for this rule: + +```gjs + +``` + +```gjs + +``` + +```gjs + +``` + +```gjs + +``` + +## Related Rules + +- [template-no-block-params-for-html-elements](template-no-block-params-for-html-elements.md) — only disallows block params on HTML elements +- [template-no-unused-block-params](template-no-unused-block-params.md) — disallows block params that are declared but never used diff --git a/lib/rules/template-no-block-params.js b/lib/rules/template-no-block-params.js new file mode 100644 index 0000000000..12c97f0c4c --- /dev/null +++ b/lib/rules/template-no-block-params.js @@ -0,0 +1,58 @@ +/** @type {import('eslint').Rule.RuleModule} */ +module.exports = { + meta: { + type: 'suggestion', + docs: { + description: 'disallow the use of block params (`as |...|`)', + category: 'Best Practices', + recommended: false, + url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-no-block-params.md', + templateMode: 'both', + }, + fixable: null, + schema: [], + messages: { + noBlockParams: + 'Block params (`as |...|`) are not allowed. Avoid using `as |{{params}}|` on `{{name}}`.', + }, + }, + + create(context) { + return { + // Catches angle-bracket invocations with block params: + // + //
+ GlimmerElementNode(node) { + if (node.blockParams && node.blockParams.length > 0) { + context.report({ + node, + messageId: 'noBlockParams', + data: { + params: node.blockParams.join(', '), + name: node.tag, + }, + }); + } + }, + + // Catches curly block invocations with block params: + // {{#each items as |item|}} + // {{#let foo as |bar|}} + // {{#my-component as |val|}} + GlimmerBlockStatement(node) { + const blockParams = node.program?.blockParams || []; + if (blockParams.length > 0) { + const pathName = node.path?.original || node.path?.head?.original || 'block'; + context.report({ + node, + messageId: 'noBlockParams', + data: { + params: blockParams.join(', '), + name: pathName, + }, + }); + } + }, + }; + }, +}; diff --git a/tests/lib/rules/template-no-block-params.js b/tests/lib/rules/template-no-block-params.js new file mode 100644 index 0000000000..792c177743 --- /dev/null +++ b/tests/lib/rules/template-no-block-params.js @@ -0,0 +1,212 @@ +const rule = require('../../../lib/rules/template-no-block-params'); +const RuleTester = require('eslint').RuleTester; + +const ruleTester = new RuleTester({ + parser: require.resolve('ember-eslint-parser'), + parserOptions: { ecmaVersion: 2022, sourceType: 'module' }, +}); + +ruleTester.run('template-no-block-params', rule, { + valid: [ + // Plain HTML elements without block params + '', + '', + '', + + // Components without block params + '', + '', + + // Mustache statements without block params + '', + '', + + // Simple {{yield}} (no block params) + '', + + // Angle bracket self-closing + '', + ], + + invalid: [ + // Component with block params (angle bracket) + { + code: '', + output: null, + errors: [ + { + messageId: 'noBlockParams', + type: 'GlimmerElementNode', + }, + ], + }, + + // Component with multiple block params + { + code: '', + output: null, + errors: [ + { + messageId: 'noBlockParams', + type: 'GlimmerElementNode', + }, + ], + }, + + // HTML element with block params + { + code: '', + output: null, + errors: [ + { + messageId: 'noBlockParams', + type: 'GlimmerElementNode', + }, + ], + }, + + // {{#each}} with block params + { + code: '', + output: null, + errors: [ + { + messageId: 'noBlockParams', + type: 'GlimmerBlockStatement', + }, + ], + }, + + // {{#each}} with multiple block params + { + code: '', + output: null, + errors: [ + { + messageId: 'noBlockParams', + type: 'GlimmerBlockStatement', + }, + ], + }, + + // {{#let}} with block params + { + code: '', + output: null, + errors: [ + { + messageId: 'noBlockParams', + type: 'GlimmerBlockStatement', + }, + ], + }, + + // Nested block params — two errors + { + code: '', + output: null, + errors: [ + { + messageId: 'noBlockParams', + type: 'GlimmerElementNode', + }, + { + messageId: 'noBlockParams', + type: 'GlimmerBlockStatement', + }, + ], + }, + + // Named blocks with block params + { + code: '', + output: null, + errors: [ + { + messageId: 'noBlockParams', + type: 'GlimmerElementNode', + }, + ], + }, + + // Curly component with block params + { + code: '', + output: null, + errors: [ + { + messageId: 'noBlockParams', + type: 'GlimmerBlockStatement', + }, + ], + }, + ], +}); + +const hbsRuleTester = new RuleTester({ + parser: require.resolve('ember-eslint-parser/hbs'), + parserOptions: { ecmaVersion: 2022, sourceType: 'module' }, +}); + +hbsRuleTester.run('template-no-block-params (hbs)', rule, { + valid: [ + '
Content
', + 'Content', + '', + '{{my-helper arg}}', + '{{yield}}', + ], + + invalid: [ + { + code: '{{item.name}}', + output: null, + errors: [ + { + messageId: 'noBlockParams', + type: 'GlimmerElementNode', + }, + ], + }, + { + code: '
{{content}}
', + output: null, + errors: [ + { + messageId: 'noBlockParams', + type: 'GlimmerElementNode', + }, + ], + }, + { + code: '{{#each this.items as |item|}}
  • {{item}}
  • {{/each}}', + output: null, + errors: [ + { + messageId: 'noBlockParams', + type: 'GlimmerBlockStatement', + }, + ], + }, + { + code: '{{#let this.name as |name|}}{{name}}{{/let}}', + output: null, + errors: [ + { + messageId: 'noBlockParams', + type: 'GlimmerBlockStatement', + }, + ], + }, + { + code: '{{#my-component as |val|}}{{val}}{{/my-component}}', + output: null, + errors: [ + { + messageId: 'noBlockParams', + type: 'GlimmerBlockStatement', + }, + ], + }, + ], +});