Skip to content

Commit 6bc2977

Browse files
committed
Extract rule: template-no-yield-only
1 parent d87b305 commit 6bc2977

4 files changed

Lines changed: 279 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-only](docs/rules/template-no-yield-only.md) | disallow components that only yield | | | |
257258
| [template-no-yield-to-default](docs/rules/template-no-yield-to-default.md) | disallow yield to default block | | | |
258259
| [template-require-button-type](docs/rules/template-require-button-type.md) | require button elements to have a valid type attribute | | 🔧 | |
259260
| [template-require-each-key](docs/rules/template-require-each-key.md) | require key attribute in {{#each}} loops | | 🔧 | |
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# ember/template-no-yield-only
2+
3+
<!-- end auto-generated rule header -->
4+
5+
Disallows components that only yield without any wrapper or additional functionality.
6+
7+
## Rule Details
8+
9+
Components should provide some structure or functionality beyond just yielding. If a component only yields, it adds unnecessary indirection.
10+
11+
## Examples
12+
13+
Examples of **incorrect** code for this rule:
14+
15+
```gjs
16+
<template>
17+
{{yield}}
18+
</template>
19+
```
20+
21+
Examples of **correct** code for this rule:
22+
23+
```gjs
24+
<template>
25+
<div class="wrapper">
26+
{{yield}}
27+
</div>
28+
</template>
29+
```
30+
31+
```gjs
32+
<template>
33+
{{this.setup}}
34+
{{yield}}
35+
</template>
36+
```
37+
38+
## Migration
39+
40+
- delete all files that are flagged by this rule
41+
42+
## References
43+
44+
- [eslint-plugin-ember template-no-yield-only](https://github.com/ember-cli/eslint-plugin-ember/blob/master/docs/rules/template-no-yield-only.md)
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
const IGNORABLE_TYPES = new Set(['GlimmerTextNode', 'GlimmerMustacheCommentStatement']);
2+
3+
function isBareYield(node) {
4+
return (
5+
node.type === 'GlimmerMustacheStatement' &&
6+
node.path &&
7+
node.path.type === 'GlimmerPathExpression' &&
8+
node.path.original === 'yield' &&
9+
(!node.params || node.params.length === 0) &&
10+
(!node.hash || !node.hash.pairs || node.hash.pairs.length === 0)
11+
);
12+
}
13+
14+
function isMeaningfulContent(node) {
15+
if (node.type === 'GlimmerTextNode') {
16+
return node.chars && node.chars.trim().length > 0;
17+
}
18+
return !IGNORABLE_TYPES.has(node.type);
19+
}
20+
21+
/** @type {import('eslint').Rule.RuleModule} */
22+
module.exports = {
23+
meta: {
24+
type: 'suggestion',
25+
docs: {
26+
description: 'disallow components that only yield',
27+
category: 'Best Practices',
28+
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-no-yield-only.md',
29+
templateMode: 'both',
30+
},
31+
fixable: null,
32+
schema: [],
33+
messages: {
34+
noYieldOnly:
35+
'Component should not only yield. Add wrapper element or additional functionality.',
36+
},
37+
originallyFrom: {
38+
name: 'ember-template-lint',
39+
rule: 'lib/rules/no-yield-only.js',
40+
docs: 'docs/rule/no-yield-only.md',
41+
tests: 'test/unit/rules/no-yield-only-test.js',
42+
},
43+
},
44+
45+
create(context) {
46+
function checkChildren(children) {
47+
let yieldNode = null;
48+
49+
for (const child of children) {
50+
if (isBareYield(child)) {
51+
yieldNode = child;
52+
} else if (isMeaningfulContent(child)) {
53+
return;
54+
}
55+
}
56+
57+
if (yieldNode) {
58+
context.report({ node: yieldNode, messageId: 'noYieldOnly' });
59+
}
60+
}
61+
62+
return {
63+
GlimmerTemplate(node) {
64+
if (!node.body || node.body.length === 0) {
65+
return;
66+
}
67+
68+
const firstChild = node.body[0];
69+
if (firstChild && firstChild.type === 'GlimmerElementNode') {
70+
// gjs/gts: body[0] is the <template> element, check its children
71+
checkChildren(firstChild.children || []);
72+
} else {
73+
// hbs: body directly contains the template nodes
74+
checkChildren(node.body);
75+
}
76+
},
77+
};
78+
},
79+
};
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
//------------------------------------------------------------------------------
2+
// Requirements
3+
//------------------------------------------------------------------------------
4+
5+
const rule = require('../../../lib/rules/template-no-yield-only');
6+
const RuleTester = require('eslint').RuleTester;
7+
8+
//------------------------------------------------------------------------------
9+
// Tests
10+
//------------------------------------------------------------------------------
11+
12+
const ruleTester = new RuleTester({
13+
parser: require.resolve('ember-eslint-parser'),
14+
parserOptions: { ecmaVersion: 2022, sourceType: 'module' },
15+
});
16+
17+
ruleTester.run('template-no-yield-only', rule, {
18+
valid: [
19+
`<template>
20+
<div>
21+
{{yield}}
22+
</div>
23+
</template>`,
24+
`<template>
25+
{{this.something}}
26+
{{yield}}
27+
</template>`,
28+
`<template>
29+
<div></div>
30+
</template>`,
31+
32+
'<template>{{yield (hash someProp=someValue)}}</template>',
33+
'<template>{{field}}</template>',
34+
'<template>{{#yield}}{{/yield}}</template>',
35+
'<template><Yield/></template>',
36+
'<template><yield/></template>',
37+
],
38+
39+
invalid: [
40+
{
41+
code: `<template>
42+
{{yield}}
43+
</template>`,
44+
output: null,
45+
errors: [
46+
{
47+
message:
48+
'Component should not only yield. Add wrapper element or additional functionality.',
49+
type: 'GlimmerMustacheStatement',
50+
},
51+
],
52+
},
53+
{
54+
code: `<template>
55+
56+
{{yield}}
57+
58+
</template>`,
59+
output: null,
60+
errors: [
61+
{
62+
message:
63+
'Component should not only yield. Add wrapper element or additional functionality.',
64+
type: 'GlimmerMustacheStatement',
65+
},
66+
],
67+
},
68+
{
69+
code: '<template>{{yield}}</template>',
70+
output: null,
71+
errors: [
72+
{
73+
message:
74+
'Component should not only yield. Add wrapper element or additional functionality.',
75+
type: 'GlimmerMustacheStatement',
76+
},
77+
],
78+
},
79+
80+
{
81+
code: '<template> {{yield}}</template>',
82+
output: null,
83+
errors: [
84+
{
85+
message:
86+
'Component should not only yield. Add wrapper element or additional functionality.',
87+
},
88+
],
89+
},
90+
{
91+
code: `<template>
92+
{{yield}}
93+
</template>`,
94+
output: null,
95+
errors: [
96+
{
97+
message:
98+
'Component should not only yield. Add wrapper element or additional functionality.',
99+
},
100+
],
101+
},
102+
{
103+
code: `<template>
104+
{{! some comment }} {{yield}}
105+
</template>`,
106+
output: null,
107+
errors: [
108+
{
109+
message:
110+
'Component should not only yield. Add wrapper element or additional functionality.',
111+
},
112+
],
113+
},
114+
],
115+
});
116+
117+
const hbsRuleTester = new RuleTester({
118+
parser: require.resolve('ember-eslint-parser/hbs'),
119+
parserOptions: {
120+
ecmaVersion: 2022,
121+
sourceType: 'module',
122+
},
123+
});
124+
125+
hbsRuleTester.run('template-no-yield-only', rule, {
126+
valid: [
127+
'{{yield (hash someProp=someValue)}}',
128+
'{{field}}',
129+
'{{#yield}}{{/yield}}',
130+
'<Yield/>',
131+
'<yield/>',
132+
],
133+
invalid: [
134+
{
135+
code: '{{yield}}',
136+
output: null,
137+
errors: [{ messageId: 'noYieldOnly' }],
138+
},
139+
{
140+
code: ' {{yield}}',
141+
output: null,
142+
errors: [{ messageId: 'noYieldOnly' }],
143+
},
144+
{
145+
code: '\n {{yield}}\n ',
146+
output: null,
147+
errors: [{ messageId: 'noYieldOnly' }],
148+
},
149+
{
150+
code: '\n{{! some comment }} {{yield}}\n ',
151+
output: null,
152+
errors: [{ messageId: 'noYieldOnly' }],
153+
},
154+
],
155+
});

0 commit comments

Comments
 (0)