Skip to content

Commit aaf9e75

Browse files
Merge pull request #2457 from NullVoxPopuli/nvp/template-lint-extract-rule-template-no-bare-yield
Extract rule: template-no-bare-yield
2 parents 4847938 + 2ab29ca commit aaf9e75

4 files changed

Lines changed: 178 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@ rules in templates can be disabled with eslint directives with mustache or html
204204
| [template-no-action-on-submit-button](docs/rules/template-no-action-on-submit-button.md) | disallow action attribute on submit buttons | | | |
205205
| [template-no-arguments-for-html-elements](docs/rules/template-no-arguments-for-html-elements.md) | disallow @arguments on HTML elements | | | |
206206
| [template-no-array-prototype-extensions](docs/rules/template-no-array-prototype-extensions.md) | disallow usage of Ember Array prototype extensions | | | |
207+
| [template-no-bare-yield](docs/rules/template-no-bare-yield.md) | disallow templates whose only meaningful content is a bare {{yield}} | | | |
207208
| [template-no-block-params-for-html-elements](docs/rules/template-no-block-params-for-html-elements.md) | disallow block params on HTML elements | | | |
208209
| [template-no-capital-arguments](docs/rules/template-no-capital-arguments.md) | disallow capital arguments (use lowercase @arg instead of @Arg) | | | |
209210
| [template-no-chained-this](docs/rules/template-no-chained-this.md) | disallow redundant `this.this` in templates | | 🔧 | |
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# ember/template-no-bare-yield
2+
3+
<!-- end auto-generated rule header -->
4+
5+
Disallow `{{yield}}` without parameters outside of contextual components.
6+
7+
## Rule Details
8+
9+
This rule enforces passing parameters to `{{yield}}` to make component APIs more explicit.
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+
{{yield (Object greeting="hello there")}}
26+
</template>
27+
28+
<template>
29+
{{yield @model}}
30+
</template>
31+
```
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
function isEmptyNode(node) {
2+
return (
3+
node.type === 'GlimmerMustacheCommentStatement' ||
4+
node.type === 'GlimmerCommentStatement' ||
5+
(node.type === 'GlimmerTextNode' && !node.chars.trim())
6+
);
7+
}
8+
9+
function isBareYield(node) {
10+
return (
11+
node.type === 'GlimmerMustacheStatement' &&
12+
node.path.original === 'yield' &&
13+
(!node.params || node.params.length === 0)
14+
);
15+
}
16+
17+
/** @type {import('eslint').Rule.RuleModule} */
18+
module.exports = {
19+
meta: {
20+
type: 'problem',
21+
docs: {
22+
description: 'disallow templates whose only meaningful content is a bare {{yield}}',
23+
category: 'Best Practices',
24+
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-no-bare-yield.md',
25+
templateMode: 'both',
26+
},
27+
schema: [],
28+
messages: {
29+
noBareYield: '{{yieldCall}}-only templates are not allowed.',
30+
},
31+
originallyFrom: {
32+
name: 'ember-template-lint',
33+
rule: 'lib/rules/no-yield-only.js',
34+
docs: 'docs/rule/no-yield-only.md',
35+
tests: 'test/unit/rules/no-yield-only-test.js',
36+
},
37+
},
38+
39+
create(context) {
40+
return {
41+
GlimmerTemplate(node) {
42+
// In GJS/GTS mode the content lives inside a GlimmerElementNode wrapper
43+
// with tag="template"; in HBS mode the body is the content directly.
44+
let body = node.body;
45+
if (
46+
body.length === 1 &&
47+
body[0].type === 'GlimmerElementNode' &&
48+
body[0].tag === 'template'
49+
) {
50+
body = body[0].children;
51+
}
52+
53+
const nonEmptyNodes = body.filter((n) => !isEmptyNode(n));
54+
if (nonEmptyNodes.length === 1 && isBareYield(nonEmptyNodes[0])) {
55+
context.report({
56+
node: nonEmptyNodes[0],
57+
messageId: 'noBareYield',
58+
data: { yieldCall: '{{yield}}' },
59+
});
60+
}
61+
},
62+
};
63+
},
64+
};
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
//------------------------------------------------------------------------------
2+
// Requirements
3+
//------------------------------------------------------------------------------
4+
5+
const rule = require('../../../lib/rules/template-no-bare-yield');
6+
const RuleTester = require('eslint').RuleTester;
7+
8+
const ruleTester = new RuleTester({
9+
parser: require.resolve('ember-eslint-parser'),
10+
parserOptions: { ecmaVersion: 2022, sourceType: 'module' },
11+
});
12+
13+
ruleTester.run('template-no-bare-yield', rule, {
14+
valid: [
15+
// yield with params is fine
16+
'<template>{{yield this}}</template>',
17+
'<template>{{yield @model}}</template>',
18+
'<template>{{yield (hash someProp=someValue)}}</template>',
19+
// yield is not the only content
20+
'<template>{{yield}}<div>Content</div></template>',
21+
'<template><div>Content</div>{{yield}}</template>',
22+
// no yield at all
23+
'<template><div>Content</div></template>',
24+
],
25+
invalid: [
26+
{
27+
code: '<template>{{yield}}</template>',
28+
output: null,
29+
errors: [{ messageId: 'noBareYield' }],
30+
},
31+
{
32+
// whitespace around yield doesn't count as other content
33+
code: '<template> {{yield}} </template>',
34+
output: null,
35+
errors: [{ messageId: 'noBareYield' }],
36+
},
37+
{
38+
// comments don't count as other content
39+
code: '<template>{{! a comment }}{{yield}}</template>',
40+
output: null,
41+
errors: [{ messageId: 'noBareYield' }],
42+
},
43+
],
44+
});
45+
46+
const hbsRuleTester = new RuleTester({
47+
parser: require.resolve('ember-eslint-parser/hbs'),
48+
parserOptions: { ecmaVersion: 2022, sourceType: 'module' },
49+
});
50+
51+
hbsRuleTester.run('template-no-bare-yield', rule, {
52+
valid: [
53+
'{{yield (hash someProp=someValue)}}',
54+
'{{yield this}}',
55+
// yield with other content
56+
'{{yield}}<div>Content</div>',
57+
],
58+
invalid: [
59+
{
60+
code: '{{yield}}',
61+
output: null,
62+
errors: [{ messageId: 'noBareYield' }],
63+
},
64+
{
65+
// whitespace around yield doesn't count as other content
66+
code: ' {{yield}}',
67+
output: null,
68+
errors: [{ messageId: 'noBareYield' }],
69+
},
70+
{
71+
code: '\n {{yield}}\n ',
72+
output: null,
73+
errors: [{ messageId: 'noBareYield' }],
74+
},
75+
{
76+
// comments don't count as other content
77+
code: '\n{{! some comment }} {{yield}}\n ',
78+
output: null,
79+
errors: [{ messageId: 'noBareYield' }],
80+
},
81+
],
82+
});

0 commit comments

Comments
 (0)