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..c9780a93b1 --- /dev/null +++ b/lib/rules/template-no-forbidden-elements.js @@ -0,0 +1,77 @@ +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' }, + { + type: 'object', + properties: { + forbidden: { type: 'array', items: { type: 'string' } }, + }, + additionalProperties: false, + }, + ], + }, + ], + 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 if (rawConfig && typeof rawConfig === 'object') { + forbiddenList = rawConfig.forbidden ?? DEFAULT_FORBIDDEN; + } 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..2361ef3ffe --- /dev/null +++ b/tests/lib/rules/template-no-forbidden-elements.js @@ -0,0 +1,129 @@ +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']] }, + // Object config form + { code: '', options: [{ forbidden: ['script'] }] }, + { code: '', options: [{ forbidden: ['html'] }] }, + '', + '', + '', + '', + ], + invalid: [ + { + code: '', + output: null, + options: [{ forbidden: ['script'] }], + errors: [{ messageId: 'forbidden' }], + }, + { + 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']], + }, + // Object config form. + { + code: '', + options: [{ forbidden: ['html', 'meta', 'style'] }], + }, + ], + invalid: [ + // Default config. + { + code: '', + output: null, + errors: [{ message: 'Use of forbidden element