Skip to content

Commit bd0c7f6

Browse files
committed
Extract rule: template-no-nested-splattributes
1 parent b3ea4a3 commit bd0c7f6

4 files changed

Lines changed: 250 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ rules in templates can be disabled with eslint directives with mustache or html
208208
| [template-no-input-placeholder](docs/rules/template-no-input-placeholder.md) | disallow placeholder attribute on input elements | | | |
209209
| [template-no-input-tagname](docs/rules/template-no-input-tagname.md) | disallow tagName attribute on {{input}} helper | | | |
210210
| [template-no-log](docs/rules/template-no-log.md) | disallow {{log}} in templates | | | |
211+
| [template-no-nested-splattributes](docs/rules/template-no-nested-splattributes.md) | disallow nested ...attributes usage | | | |
211212
| [template-no-obscure-array-access](docs/rules/template-no-obscure-array-access.md) | disallow obscure array access patterns like `objectPath.@each.property` | | | |
212213
| [template-no-obsolete-elements](docs/rules/template-no-obsolete-elements.md) | disallow obsolete HTML elements | | | |
213214
| [template-no-outlet-outside-routes](docs/rules/template-no-outlet-outside-routes.md) | disallow {{outlet}} outside of route templates | | | |
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# ember/template-no-nested-splattributes
2+
3+
<!-- end auto-generated rule header -->
4+
5+
Disallow nested `...attributes` usage.
6+
7+
Having `...attributes` on multiple elements nested within each other in a component can cause unintended results. This rule prevents `...attributes` on an element if any of its parent elements already has `...attributes`.
8+
9+
## Rule Details
10+
11+
This rule disallows `...attributes` on an element when an ancestor element already has `...attributes`.
12+
13+
## Examples
14+
15+
### Incorrect ❌
16+
17+
```gjs
18+
<template>
19+
<div ...attributes>
20+
<span ...attributes>Text</span>
21+
</div>
22+
</template>
23+
```
24+
25+
```gjs
26+
<template>
27+
<section ...attributes>
28+
<div>
29+
<button ...attributes>Click</button>
30+
</div>
31+
</section>
32+
</template>
33+
```
34+
35+
### Correct ✅
36+
37+
```gjs
38+
<template>
39+
<div ...attributes>Content</div>
40+
</template>
41+
```
42+
43+
```gjs
44+
<template>
45+
<div ...attributes>
46+
<span>Text</span>
47+
</div>
48+
</template>
49+
```
50+
51+
```gjs
52+
<template>
53+
<div ...attributes>first</div>
54+
<div ...attributes>second</div>
55+
</template>
56+
```
57+
58+
## Migration
59+
60+
- Remove the inner `...attributes` declaration
61+
62+
## References
63+
64+
- [Ember Guides - Splattributes](https://guides.emberjs.com/release/components/component-arguments-and-html-attributes/#toc_html-attributes)
65+
- [eslint-plugin-ember template-no-nested-splattributes](https://github.com/ember-cli/eslint-plugin-ember/blob/master/docs/rules/template-no-nested-splattributes.md)
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/** @type {import('eslint').Rule.RuleModule} */
2+
module.exports = {
3+
meta: {
4+
type: 'problem',
5+
docs: {
6+
description: 'disallow nested ...attributes usage',
7+
category: 'Best Practices',
8+
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-no-nested-splattributes.md',
9+
templateMode: 'both',
10+
},
11+
fixable: null,
12+
schema: [],
13+
messages: {
14+
noNestedSplattributes:
15+
'Do not use ...attributes on nested elements. Only use it on the top-level element of a component.',
16+
},
17+
originallyFrom: {
18+
name: 'ember-template-lint',
19+
rule: 'lib/rules/no-nested-splattributes.js',
20+
docs: 'docs/rule/no-nested-splattributes.md',
21+
tests: 'test/unit/rules/no-nested-splattributes-test.js',
22+
},
23+
},
24+
25+
create(context) {
26+
const splattributesStack = [];
27+
28+
return {
29+
GlimmerElementNode(node) {
30+
const hasSplattributes = node.attributes.some(
31+
(attr) => attr.type === 'GlimmerAttrNode' && attr.name === '...attributes'
32+
);
33+
34+
if (hasSplattributes) {
35+
if (splattributesStack.length > 0) {
36+
// Found ...attributes on an element nested inside another element with ...attributes
37+
const attr = node.attributes.find(
38+
(a) => a.type === 'GlimmerAttrNode' && a.name === '...attributes'
39+
);
40+
context.report({
41+
node: attr,
42+
messageId: 'noNestedSplattributes',
43+
});
44+
}
45+
splattributesStack.push(node);
46+
}
47+
},
48+
49+
'GlimmerElementNode:exit'(node) {
50+
if (splattributesStack.length > 0 && splattributesStack.at(-1) === node) {
51+
splattributesStack.pop();
52+
}
53+
},
54+
};
55+
},
56+
};
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
const rule = require('../../../lib/rules/template-no-nested-splattributes');
2+
const RuleTester = require('eslint').RuleTester;
3+
4+
const ruleTester = new RuleTester({
5+
parser: require.resolve('ember-eslint-parser'),
6+
parserOptions: { ecmaVersion: 2022, sourceType: 'module' },
7+
});
8+
9+
ruleTester.run('template-no-nested-splattributes', rule, {
10+
valid: [
11+
// Note: In standalone gjs/gts templates, the concept of "top-level" is different.
12+
// These tests focus on the clear nested cases.
13+
'<template><div><p>No splattributes here</p></div></template>',
14+
15+
'<template><div>...</div></template>',
16+
'<template><div><div ...attributes>...</div></div></template>',
17+
'<template><div ...attributes>...</div></template>',
18+
'<template><div ...attributes>...</div><div ...attributes>...</div></template>',
19+
],
20+
21+
invalid: [
22+
{
23+
code: '<template><div ...attributes><span ...attributes>Text</span></div></template>',
24+
output: null,
25+
errors: [
26+
{
27+
message:
28+
'Do not use ...attributes on nested elements. Only use it on the top-level element of a component.',
29+
type: 'GlimmerAttrNode',
30+
},
31+
],
32+
},
33+
{
34+
code: '<template><section ...attributes><div><button ...attributes>Click</button></div></section></template>',
35+
output: null,
36+
errors: [
37+
{
38+
message:
39+
'Do not use ...attributes on nested elements. Only use it on the top-level element of a component.',
40+
type: 'GlimmerAttrNode',
41+
},
42+
],
43+
},
44+
{
45+
code: '<template><div ...attributes class="wrapper"><input ...attributes /></div></template>',
46+
output: null,
47+
errors: [
48+
{
49+
message:
50+
'Do not use ...attributes on nested elements. Only use it on the top-level element of a component.',
51+
type: 'GlimmerAttrNode',
52+
},
53+
],
54+
},
55+
56+
{
57+
code: `<template><div ...attributes>
58+
<div ...attributes>
59+
...
60+
</div>
61+
</div>
62+
</template>`,
63+
output: null,
64+
errors: [
65+
{
66+
message:
67+
'Do not use ...attributes on nested elements. Only use it on the top-level element of a component.',
68+
},
69+
],
70+
},
71+
{
72+
code: `<template><div ...attributes>
73+
<div>
74+
<div ...attributes>
75+
...
76+
</div>
77+
</div>
78+
</div>
79+
</template>`,
80+
output: null,
81+
errors: [
82+
{
83+
message:
84+
'Do not use ...attributes on nested elements. Only use it on the top-level element of a component.',
85+
},
86+
],
87+
},
88+
],
89+
});
90+
91+
const hbsRuleTester = new RuleTester({
92+
parser: require.resolve('ember-eslint-parser/hbs'),
93+
parserOptions: {
94+
ecmaVersion: 2022,
95+
sourceType: 'module',
96+
},
97+
});
98+
99+
hbsRuleTester.run('template-no-nested-splattributes', rule, {
100+
valid: [
101+
'<div>...</div>',
102+
'<div><div ...attributes>...</div></div>',
103+
'<div ...attributes>...</div>',
104+
'<div ...attributes>...</div><div ...attributes>...</div>',
105+
],
106+
invalid: [
107+
{
108+
code: '<div ...attributes>\n <div ...attributes>\n ...\n </div>\n</div>\n',
109+
output: null,
110+
errors: [
111+
{
112+
message:
113+
'Do not use ...attributes on nested elements. Only use it on the top-level element of a component.',
114+
},
115+
],
116+
},
117+
{
118+
code: '<div ...attributes>\n <div>\n <div ...attributes>\n ...\n </div>\n </div>\n</div>\n',
119+
output: null,
120+
errors: [
121+
{
122+
message:
123+
'Do not use ...attributes on nested elements. Only use it on the top-level element of a component.',
124+
},
125+
],
126+
},
127+
],
128+
});

0 commit comments

Comments
 (0)