Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 | | | |
Expand Down
57 changes: 57 additions & 0 deletions docs/rules/template-no-forbidden-elements.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# ember/template-no-forbidden-elements

<!-- end auto-generated rule header -->

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
<template><script></script></template>
```

```gjs
<template><style></style></template>
```

```gjs
<template><html></html></template>
```

```gjs
<template><meta charset='utf-8' /></template>
```

This rule **allows** the following:

```gjs
<template><header></header></template>
```

```gjs
<template><div></div></template>
```

```gjs
<template>
<head>
<meta charset='utf-8' />
</head>
</template>
```

Note: `<meta>` inside `<head>` 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)
77 changes: 77 additions & 0 deletions lib/rules/template-no-forbidden-elements.js
Original file line number Diff line number Diff line change
@@ -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 <meta> in <head> exception
const elementStack = [];

return {
GlimmerElementNode(node) {
elementStack.push(node.tag);

if (!forbidden.has(node.tag)) {
return;
}

// Exception: <meta> inside <head> is allowed
if (node.tag === 'meta' && elementStack.includes('head')) {
return;
}

context.report({ node, messageId: 'forbidden', data: { element: node.tag } });
},
'GlimmerElementNode:exit'() {
elementStack.pop();
},
};
},
};
129 changes: 129 additions & 0 deletions tests/lib/rules/template-no-forbidden-elements.js
Original file line number Diff line number Diff line change
@@ -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: '<template><div></div></template>', options: [['script']] },
// Object config form
{ code: '<template><div></div></template>', options: [{ forbidden: ['script'] }] },
{ code: '<template><script></script></template>', options: [{ forbidden: ['html'] }] },
'<template><header></header></template>',
'<template><footer></footer></template>',
'<template><p></p></template>',
'<template><head><meta charset="utf-8"></head></template>',
],
invalid: [
{
code: '<template><script></script></template>',
output: null,
options: [{ forbidden: ['script'] }],
errors: [{ messageId: 'forbidden' }],
},
{
code: '<template><script></script></template>',
output: null,
options: [['script']],
errors: [{ messageId: 'forbidden' }],
},

{
code: '<template><html></html></template>',
output: null,
errors: [{ messageId: 'forbidden' }],
},
{
code: '<template><style></style></template>',
output: null,
errors: [{ messageId: 'forbidden' }],
},
{
code: '<template><meta charset="utf-8"></template>',
output: null,
errors: [{ messageId: 'forbidden' }],
},
{
code: '<template><head><html></html></head></template>',
output: null,
errors: [{ messageId: 'forbidden' }],
},
{
code: '<template><Foo /></template>',
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: [
'<header></header>',
'<div></div>',
'<footer></footer>',
'<p></p>',
'<head><meta charset="utf-8"></head>',
// Custom forbidden list (script not included).
{
code: '<script></script>',
options: [['html', 'meta', 'style']],
},
// Object config form.
{
code: '<script></script>',
options: [{ forbidden: ['html', 'meta', 'style'] }],
},
],
invalid: [
// Default config.
{
code: '<script></script>',
output: null,
errors: [{ message: 'Use of forbidden element <script>' }],
},
{
code: '<html></html>',
output: null,
errors: [{ message: 'Use of forbidden element <html>' }],
},
{
code: '<style></style>',
output: null,
errors: [{ message: 'Use of forbidden element <style>' }],
},
{
code: '<meta charset="utf-8">',
output: null,
errors: [{ message: 'Use of forbidden element <meta>' }],
},
{
code: '<head><html></html></head>',
output: null,
errors: [{ message: 'Use of forbidden element <html>' }],
},
// Custom forbidden list.
{
code: '<div></div>',
output: null,
options: [['div']],
errors: [{ message: 'Use of forbidden element <div>' }],
},
{
code: '<Foo />',
output: null,
options: [['Foo']],
errors: [{ message: 'Use of forbidden element <Foo>' }],
},
],
});
Loading