From b63b79d1d872474e89aa77dab79edf893a5a32e4 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Tue, 10 Mar 2026 18:28:43 -0400 Subject: [PATCH 1/2] Extract rule: template-no-forbidden-elements --- README.md | 1 + docs/rules/template-no-forbidden-elements.md | 57 +++++++++ lib/rules/template-no-forbidden-elements.js | 65 ++++++++++ .../rules/template-no-forbidden-elements.js | 115 ++++++++++++++++++ 4 files changed, 238 insertions(+) create mode 100644 docs/rules/template-no-forbidden-elements.md create mode 100644 lib/rules/template-no-forbidden-elements.js create mode 100644 tests/lib/rules/template-no-forbidden-elements.js diff --git a/README.md b/README.md index c2924ba272..fe57ea27d1 100644 --- a/README.md +++ b/README.md @@ -215,6 +215,7 @@ rules in templates can be disabled with eslint directives with mustache or html | [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-element-event-actions](docs/rules/template-no-element-event-actions.md) | disallow element event actions (use {{on}} modifier instead) | | | | +| [template-no-forbidden-elements](docs/rules/template-no-forbidden-elements.md) | disallow specific HTML elements | | | | | [template-no-html-comments](docs/rules/template-no-html-comments.md) | disallow HTML comments in templates | | 🔧 | | | [template-no-implicit-this](docs/rules/template-no-implicit-this.md) | require explicit `this` in property access | | | | | [template-no-index-component-invocation](docs/rules/template-no-index-component-invocation.md) | disallow index component invocations | | | | diff --git a/docs/rules/template-no-forbidden-elements.md b/docs/rules/template-no-forbidden-elements.md new file mode 100644 index 0000000000..5fd00a24e8 --- /dev/null +++ b/docs/rules/template-no-forbidden-elements.md @@ -0,0 +1,57 @@ +# ember/template-no-forbidden-elements + + + +This rule disallows the use of forbidden elements in template files. + +The rule is configurable so teams can add their own disallowed elements. +The default list of forbidden elements are `meta`, `style`, `html`, and `script`. + +## Examples + +This rule **forbids** the following: + +```gjs + +``` + +```gjs + +``` + +```gjs + +``` + +```gjs + +``` + +This rule **allows** the following: + +```gjs + +``` + +```gjs + +``` + +```gjs + +``` + +Note: `` inside `` is allowed as an exception. + +## Configuration + +- `boolean` — `true` to enable with defaults / `false` to disable +- `string[]` — an array of element names to forbid (default: `['meta', 'style', 'html', 'script']`) + +## References + +- [Ember guides/template restrictions](https://guides.emberjs.com/release/components/#toc_restrictions) diff --git a/lib/rules/template-no-forbidden-elements.js b/lib/rules/template-no-forbidden-elements.js new file mode 100644 index 0000000000..3857f57f17 --- /dev/null +++ b/lib/rules/template-no-forbidden-elements.js @@ -0,0 +1,65 @@ +const DEFAULT_FORBIDDEN = ['meta', 'style', 'html', 'script']; + +/** @type {import('eslint').Rule.RuleModule} */ +module.exports = { + meta: { + type: 'suggestion', + docs: { + description: 'disallow specific HTML elements', + category: 'Best Practices', + recommendedGjs: false, + recommendedGts: false, + url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-no-forbidden-elements.md', + templateMode: 'both', + }, + schema: [ + { + oneOf: [{ type: 'array', items: { type: 'string' } }, { type: 'boolean' }], + }, + ], + messages: { forbidden: 'Use of forbidden element <{{element}}>' }, + originallyFrom: { + name: 'ember-template-lint', + rule: 'lib/rules/no-forbidden-elements.js', + docs: 'docs/rule/no-forbidden-elements.md', + tests: 'test/unit/rules/no-forbidden-elements-test.js', + }, + }, + create(context) { + const rawConfig = context.options[0]; + let forbiddenList; + + if (rawConfig === true || rawConfig === undefined) { + forbiddenList = DEFAULT_FORBIDDEN; + } else if (Array.isArray(rawConfig)) { + forbiddenList = rawConfig; + } else { + forbiddenList = []; + } + + const forbidden = new Set(forbiddenList); + + // Track element stack for in exception + const elementStack = []; + + return { + GlimmerElementNode(node) { + elementStack.push(node.tag); + + if (!forbidden.has(node.tag)) { + return; + } + + // Exception: inside is allowed + if (node.tag === 'meta' && elementStack.includes('head')) { + return; + } + + context.report({ node, messageId: 'forbidden', data: { element: node.tag } }); + }, + 'GlimmerElementNode:exit'() { + elementStack.pop(); + }, + }; + }, +}; diff --git a/tests/lib/rules/template-no-forbidden-elements.js b/tests/lib/rules/template-no-forbidden-elements.js new file mode 100644 index 0000000000..3127415030 --- /dev/null +++ b/tests/lib/rules/template-no-forbidden-elements.js @@ -0,0 +1,115 @@ +const rule = require('../../../lib/rules/template-no-forbidden-elements'); +const RuleTester = require('eslint').RuleTester; + +const ruleTester = new RuleTester({ + parser: require.resolve('ember-eslint-parser'), + parserOptions: { ecmaVersion: 2022, sourceType: 'module' }, +}); +ruleTester.run('template-no-forbidden-elements', rule, { + valid: [ + { code: '', options: [['script']] }, + '', + '', + '', + '', + ], + invalid: [ + { + code: '', + output: null, + options: [['script']], + errors: [{ messageId: 'forbidden' }], + }, + + { + code: '', + output: null, + errors: [{ messageId: 'forbidden' }], + }, + { + code: '', + output: null, + errors: [{ messageId: 'forbidden' }], + }, + { + code: '', + output: null, + errors: [{ messageId: 'forbidden' }], + }, + { + code: '', + output: null, + errors: [{ messageId: 'forbidden' }], + }, + { + code: '', + output: null, + options: [['Foo']], + errors: [{ messageId: 'forbidden' }], + }, + ], +}); + +const hbsRuleTester = new RuleTester({ + parser: require.resolve('ember-eslint-parser/hbs'), + parserOptions: { + ecmaVersion: 2022, + sourceType: 'module', + }, +}); + +hbsRuleTester.run('template-no-forbidden-elements', rule, { + valid: [ + '
', + '
', + '', + '

', + '', + // Custom forbidden list (script not included). + { + code: '', + options: [['html', 'meta', 'style']], + }, + ], + invalid: [ + // Default config. + { + code: '', + output: null, + errors: [{ message: 'Use of forbidden element ', options: [{ forbidden: ['html'] }] }, '', '', '', '', ], invalid: [ + { + code: '', + output: null, + options: [{ forbidden: ['script'] }], + errors: [{ messageId: 'forbidden' }], + }, { code: '', output: null, @@ -70,6 +79,11 @@ hbsRuleTester.run('template-no-forbidden-elements', rule, { code: '', options: [['html', 'meta', 'style']], }, + // Object config form. + { + code: '', + options: [{ forbidden: ['html', 'meta', 'style'] }], + }, ], invalid: [ // Default config.