Skip to content

Commit d87b305

Browse files
Merge pull request #2597 from NullVoxPopuli/nvp/template-lint-extract-rule-template-no-yield-to-default
Extract rule: template-no-yield-to-default
2 parents 6c80cb5 + 6e0d9f0 commit d87b305

4 files changed

Lines changed: 247 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,7 @@ rules in templates can be disabled with eslint directives with mustache or html
254254
| [template-no-obsolete-elements](docs/rules/template-no-obsolete-elements.md) | disallow obsolete HTML elements | | | |
255255
| [template-no-outlet-outside-routes](docs/rules/template-no-outlet-outside-routes.md) | disallow {{outlet}} outside of route templates | | | |
256256
| [template-no-page-title-component](docs/rules/template-no-page-title-component.md) | disallow usage of ember-page-title component | | | |
257+
| [template-no-yield-to-default](docs/rules/template-no-yield-to-default.md) | disallow yield to default block | | | |
257258
| [template-require-button-type](docs/rules/template-require-button-type.md) | require button elements to have a valid type attribute | | 🔧 | |
258259
| [template-require-each-key](docs/rules/template-require-each-key.md) | require key attribute in {{#each}} loops | | 🔧 | |
259260
| [template-require-form-method](docs/rules/template-require-form-method.md) | require form method attribute | | 🔧 | |
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# ember/template-no-yield-to-default
2+
3+
<!-- end auto-generated rule header -->
4+
5+
The `yield` keyword can be used for invoking blocks passed into a component. The `to` named argument specifies which of the blocks to yield too. Specifying `{{yield to="default"}}` is unnecessary, as that is the default behavior. Likewise, `{{has-block}}` and `{{has-block-params}}` also defaults to checking the "default" block.
6+
7+
This rule disallow yield to named blocks with the name "default".
8+
9+
## Examples
10+
11+
This rule **forbids** the following:
12+
13+
```gjs
14+
<template>
15+
{{yield to="default"}}
16+
</template>
17+
```
18+
19+
```gjs
20+
<template>
21+
{{has-block "default"}}
22+
</template>
23+
```
24+
25+
```gjs
26+
<template>
27+
{{has-block-params "default"}}
28+
</template>
29+
```
30+
31+
This rule **allows** the following:
32+
33+
```gjs
34+
<template>
35+
{{yield}}
36+
</template>
37+
```
38+
39+
```gjs
40+
<template>
41+
{{has-block}}
42+
</template>
43+
```
44+
45+
```gjs
46+
<template>
47+
{{has-block-params}}
48+
</template>
49+
```
50+
51+
## References
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
const ERROR_MESSAGE = 'A block named "default" is not valid';
2+
3+
/** @type {import('eslint').Rule.RuleModule} */
4+
module.exports = {
5+
meta: {
6+
type: 'suggestion',
7+
docs: {
8+
description: 'disallow yield to default block',
9+
category: 'Best Practices',
10+
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-no-yield-to-default.md',
11+
templateMode: 'both',
12+
},
13+
fixable: null,
14+
schema: [],
15+
messages: {
16+
invalidDefaultBlock: ERROR_MESSAGE,
17+
},
18+
originallyFrom: {
19+
name: 'ember-template-lint',
20+
rule: 'lib/rules/no-yield-to-default.js',
21+
docs: 'docs/rule/no-yield-to-default.md',
22+
tests: 'test/unit/rules/no-yield-to-default-test.js',
23+
},
24+
},
25+
26+
create(context) {
27+
const BLOCK_HELPERS = new Set(['has-block', 'has-block-params', 'hasBlock', 'hasBlockParams']);
28+
29+
function checkDefaultBlockHelper(node) {
30+
if (
31+
node.path &&
32+
node.path.type === 'GlimmerPathExpression' &&
33+
BLOCK_HELPERS.has(node.path.original) &&
34+
node.params &&
35+
node.params.length > 0 &&
36+
node.params[0].type === 'GlimmerStringLiteral' &&
37+
node.params[0].value === 'default'
38+
) {
39+
context.report({
40+
node: node.params[0],
41+
messageId: 'invalidDefaultBlock',
42+
});
43+
}
44+
}
45+
46+
return {
47+
GlimmerMustacheStatement(node) {
48+
if (
49+
node.path &&
50+
node.path.type === 'GlimmerPathExpression' &&
51+
node.path.original === 'yield' &&
52+
node.hash &&
53+
node.hash.pairs
54+
) {
55+
for (const pair of node.hash.pairs) {
56+
if (
57+
pair.key === 'to' &&
58+
pair.value.type === 'GlimmerStringLiteral' &&
59+
pair.value.value === 'default'
60+
) {
61+
context.report({
62+
node: pair,
63+
messageId: 'invalidDefaultBlock',
64+
});
65+
}
66+
}
67+
}
68+
checkDefaultBlockHelper(node);
69+
},
70+
GlimmerSubExpression(node) {
71+
checkDefaultBlockHelper(node);
72+
},
73+
};
74+
},
75+
};
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
const rule = require('../../../lib/rules/template-no-yield-to-default');
2+
const RuleTester = require('eslint').RuleTester;
3+
4+
const ERROR_MESSAGE = 'A block named "default" is not valid';
5+
6+
const validHbs = [
7+
'{{yield}}',
8+
'{{yield to="title"}}',
9+
'{{has-block}}',
10+
'{{has-block "title"}}',
11+
'{{has-block-params}}',
12+
'{{has-block-params "title"}}',
13+
'{{hasBlock}}',
14+
'{{hasBlock "title"}}',
15+
'{{hasBlockParams}}',
16+
'{{hasBlockParams "title"}}',
17+
];
18+
19+
const invalidHbs = [
20+
{
21+
code: '{{yield to="default"}}',
22+
output: null,
23+
errors: [{ message: ERROR_MESSAGE }],
24+
},
25+
{
26+
code: '{{has-block "default"}}',
27+
output: null,
28+
errors: [{ message: ERROR_MESSAGE }],
29+
},
30+
{
31+
code: '{{has-block-params "default"}}',
32+
output: null,
33+
errors: [{ message: ERROR_MESSAGE }],
34+
},
35+
{
36+
code: '{{hasBlock "default"}}',
37+
output: null,
38+
errors: [{ message: ERROR_MESSAGE }],
39+
},
40+
{
41+
code: '{{hasBlockParams "default"}}',
42+
output: null,
43+
errors: [{ message: ERROR_MESSAGE }],
44+
},
45+
{
46+
code: '{{if (has-block "default")}}',
47+
output: null,
48+
errors: [{ message: ERROR_MESSAGE }],
49+
},
50+
{
51+
code: '{{#if (has-block "default")}}{{/if}}',
52+
output: null,
53+
errors: [{ message: ERROR_MESSAGE }],
54+
},
55+
{
56+
code: '{{if (has-block-params "default")}}',
57+
output: null,
58+
errors: [{ message: ERROR_MESSAGE }],
59+
},
60+
{
61+
code: '{{#if (has-block-params "default")}}{{/if}}',
62+
output: null,
63+
errors: [{ message: ERROR_MESSAGE }],
64+
},
65+
{
66+
code: '{{if (hasBlock "default")}}',
67+
output: null,
68+
errors: [{ message: ERROR_MESSAGE }],
69+
},
70+
{
71+
code: '{{#if (hasBlock "default")}}{{/if}}',
72+
output: null,
73+
errors: [{ message: ERROR_MESSAGE }],
74+
},
75+
{
76+
code: '{{if (hasBlockParams "default")}}',
77+
output: null,
78+
errors: [{ message: ERROR_MESSAGE }],
79+
},
80+
{
81+
code: '{{#if (hasBlockParams "default")}}{{/if}}',
82+
output: null,
83+
errors: [{ message: ERROR_MESSAGE }],
84+
},
85+
];
86+
87+
function wrapTemplate(entry) {
88+
if (typeof entry === 'string') {
89+
return `<template>${entry}</template>`;
90+
}
91+
92+
return {
93+
...entry,
94+
code: `<template>${entry.code}</template>`,
95+
output: entry.output ? `<template>${entry.output}</template>` : entry.output,
96+
};
97+
}
98+
99+
const gjsRuleTester = new RuleTester({
100+
parser: require.resolve('ember-eslint-parser'),
101+
parserOptions: { ecmaVersion: 2022, sourceType: 'module' },
102+
});
103+
104+
gjsRuleTester.run('template-no-yield-to-default', rule, {
105+
valid: validHbs.map(wrapTemplate),
106+
invalid: invalidHbs.map(wrapTemplate),
107+
});
108+
109+
const hbsRuleTester = new RuleTester({
110+
parser: require.resolve('ember-eslint-parser/hbs'),
111+
parserOptions: {
112+
ecmaVersion: 2022,
113+
sourceType: 'module',
114+
},
115+
});
116+
117+
hbsRuleTester.run('template-no-yield-to-default', rule, {
118+
valid: validHbs,
119+
invalid: invalidHbs,
120+
});

0 commit comments

Comments
 (0)