Skip to content

Commit 4a7841d

Browse files
committed
Extract rule: template-no-duplicate-attributes
1 parent 484c3c6 commit 4a7841d

4 files changed

Lines changed: 280 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ rules in templates can be disabled with eslint directives with mustache or html
202202
| [template-no-capital-arguments](docs/rules/template-no-capital-arguments.md) | disallow capital arguments (use lowercase @arg instead of @Arg) | | | |
203203
| [template-no-chained-this](docs/rules/template-no-chained-this.md) | disallow redundant `this.this` in templates | | 🔧 | |
204204
| [template-no-debugger](docs/rules/template-no-debugger.md) | disallow {{debugger}} in templates | | | |
205+
| [template-no-duplicate-attributes](docs/rules/template-no-duplicate-attributes.md) | disallow duplicate attribute names in templates | | 🔧 | |
205206
| [template-no-element-event-actions](docs/rules/template-no-element-event-actions.md) | disallow element event actions (use {{on}} modifier instead) | | | |
206207
| [template-no-inline-event-handlers](docs/rules/template-no-inline-event-handlers.md) | disallow DOM event handler attributes | | | |
207208
| [template-no-inline-styles](docs/rules/template-no-inline-styles.md) | disallow inline styles | | | |
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# ember/template-no-duplicate-attributes
2+
3+
🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
4+
5+
<!-- end auto-generated rule header -->
6+
7+
Disallows duplicate attribute names in templates.
8+
9+
Duplicate attributes on the same element can lead to unexpected behavior and are often a mistake.
10+
11+
## Rule Details
12+
13+
This rule disallows duplicate attributes on HTML elements, components, and helpers.
14+
15+
## Examples
16+
17+
Examples of **incorrect** code for this rule:
18+
19+
```gjs
20+
<template>
21+
<div class="foo" class="bar"></div>
22+
</template>
23+
```
24+
25+
```gjs
26+
<template>
27+
<input type="text" disabled type="email" />
28+
</template>
29+
```
30+
31+
```gjs
32+
<template>
33+
{{helper foo="bar" foo="baz"}}
34+
</template>
35+
```
36+
37+
Examples of **correct** code for this rule:
38+
39+
```gjs
40+
<template>
41+
<div class="foo bar"></div>
42+
</template>
43+
```
44+
45+
```gjs
46+
<template>
47+
<input type="email" disabled />
48+
</template>
49+
```
50+
51+
```gjs
52+
<template>
53+
{{helper foo="bar" baz="qux"}}
54+
</template>
55+
```
56+
57+
## References
58+
59+
- [eslint-plugin-ember template-no-duplicate-attributes](https://github.com/ember-cli/eslint-plugin-ember/blob/master/docs/rules/template-no-duplicate-attributes.md)
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/** @type {import('eslint').Rule.RuleModule} */
2+
module.exports = {
3+
meta: {
4+
type: 'problem',
5+
docs: {
6+
description: 'disallow duplicate attribute names in templates',
7+
category: 'Best Practices',
8+
strictGjs: true,
9+
strictGts: true,
10+
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-no-duplicate-attributes.md',
11+
},
12+
fixable: 'code',
13+
schema: [],
14+
messages: {
15+
duplicateElement: "Duplicate attribute '{{name}}' found in the Element.",
16+
duplicateBlock: "Duplicate attribute '{{name}}' found in the BlockStatement.",
17+
duplicateMustache: "Duplicate attribute '{{name}}' found in the MustacheStatement.",
18+
duplicateSubExpr: "Duplicate attribute '{{name}}' found in the SubExpression.",
19+
},
20+
originallyFrom: {
21+
name: 'ember-template-lint',
22+
rule: 'lib/rules/no-duplicate-attributes.js',
23+
docs: 'docs/rule/no-duplicate-attributes.md',
24+
tests: 'test/unit/rules/no-duplicate-attributes-test.js',
25+
},
26+
},
27+
28+
create(context) {
29+
function checkForDuplicates(node, attributes, identifier, messageId) {
30+
if (!attributes || attributes.length < 2) {
31+
return;
32+
}
33+
34+
const seen = new Map();
35+
36+
for (const attr of attributes) {
37+
const key = attr[identifier];
38+
if (seen.has(key)) {
39+
context.report({
40+
node: attr,
41+
messageId,
42+
data: { name: key },
43+
fix(fixer) {
44+
// Remove the duplicate attribute including preceding whitespace
45+
const sourceCode = context.sourceCode;
46+
const text = sourceCode.getText();
47+
const attrStart = attr.range[0];
48+
const attrEnd = attr.range[1];
49+
50+
// Look for whitespace before the attribute
51+
let removeStart = attrStart;
52+
while (removeStart > 0 && /\s/.test(text[removeStart - 1])) {
53+
removeStart--;
54+
}
55+
56+
return fixer.removeRange([removeStart, attrEnd]);
57+
},
58+
});
59+
} else {
60+
seen.set(key, attr);
61+
}
62+
}
63+
}
64+
65+
return {
66+
GlimmerElementNode(node) {
67+
checkForDuplicates(node, node.attributes, 'name', 'duplicateElement');
68+
},
69+
70+
GlimmerBlockStatement(node) {
71+
const attributes = node.hash?.pairs || [];
72+
checkForDuplicates(node, attributes, 'key', 'duplicateBlock');
73+
},
74+
75+
GlimmerMustacheStatement(node) {
76+
const attributes = node.hash?.pairs || [];
77+
checkForDuplicates(node, attributes, 'key', 'duplicateMustache');
78+
},
79+
80+
GlimmerSubExpression(node) {
81+
const attributes = node.hash?.pairs || [];
82+
checkForDuplicates(node, attributes, 'key', 'duplicateSubExpr');
83+
},
84+
};
85+
},
86+
};
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
//------------------------------------------------------------------------------
2+
// Requirements
3+
//------------------------------------------------------------------------------
4+
5+
const rule = require('../../../lib/rules/template-no-duplicate-attributes');
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-duplicate-attributes', rule, {
18+
valid: [
19+
`<template>
20+
<div class="foo" id="bar"></div>
21+
</template>`,
22+
`<template>
23+
<button type="button" disabled></button>
24+
</template>`,
25+
`<template>
26+
{{helper arg1="a" arg2="b"}}
27+
</template>`,
28+
`<template>
29+
{{#each items as |item|}}
30+
{{item}}
31+
{{/each}}
32+
</template>`,
33+
34+
// Test cases ported from eslint-plugin-ember
35+
'<template>{{my-component firstName=firstName lastName=lastName}}</template>',
36+
'<template> {{fullName}}</template>',
37+
'<template><a class="btn">{{btnLabel}}</a></template>',
38+
'<template>{{employee-profile employee=(hash firstName=firstName lastName=lastName age=age)}}</template>',
39+
'<template>{{employee-profile employee=(hash fullName=(hash firstName=firstName lastName=lastName) age=age)}}</template>',
40+
],
41+
42+
invalid: [
43+
{
44+
code: `<template>
45+
<div class="foo" class="bar"></div>
46+
</template>`,
47+
output: `<template>
48+
<div class="foo"></div>
49+
</template>`,
50+
errors: [
51+
{
52+
message: "Duplicate attribute 'class' found in the Element.",
53+
type: 'GlimmerAttrNode',
54+
},
55+
],
56+
},
57+
{
58+
code: `<template>
59+
<input type="text" disabled type="email" />
60+
</template>`,
61+
output: `<template>
62+
<input type="text" disabled />
63+
</template>`,
64+
errors: [
65+
{
66+
message: "Duplicate attribute 'type' found in the Element.",
67+
type: 'GlimmerAttrNode',
68+
},
69+
],
70+
},
71+
{
72+
code: `<template>
73+
{{helper foo="bar" foo="baz"}}
74+
</template>`,
75+
output: `<template>
76+
{{helper foo="bar"}}
77+
</template>`,
78+
errors: [
79+
{
80+
message: "Duplicate attribute 'foo' found in the MustacheStatement.",
81+
type: 'GlimmerHashPair',
82+
},
83+
],
84+
},
85+
{
86+
code: `<template>
87+
{{#if condition key="a" key="b"}}
88+
content
89+
{{/if}}
90+
</template>`,
91+
output: `<template>
92+
{{#if condition key="a"}}
93+
content
94+
{{/if}}
95+
</template>`,
96+
errors: [
97+
{
98+
message: "Duplicate attribute 'key' found in the BlockStatement.",
99+
type: 'GlimmerHashPair',
100+
},
101+
],
102+
},
103+
104+
// Test cases ported from eslint-plugin-ember
105+
{
106+
code: '<template>{{my-component firstName=firstName lastName=lastName firstName=firstName}}</template>',
107+
output: '<template>{{my-component firstName=firstName lastName=lastName}}</template>',
108+
errors: [{ messageId: 'duplicateMustache', data: { name: 'firstName' } }],
109+
},
110+
{
111+
code: '<template>{{#my-component firstName=firstName lastName=lastName firstName=firstName as |fullName|}}{{/my-component}}</template>',
112+
output:
113+
'<template>{{#my-component firstName=firstName lastName=lastName as |fullName|}}{{/my-component}}</template>',
114+
errors: [{ messageId: 'duplicateBlock', data: { name: 'firstName' } }],
115+
},
116+
{
117+
code: '<template><a class="btn" class="btn">{{btnLabel}}</a></template>',
118+
output: '<template><a class="btn">{{btnLabel}}</a></template>',
119+
errors: [{ messageId: 'duplicateElement', data: { name: 'class' } }],
120+
},
121+
{
122+
code: '<template>{{employee-profile employee=(hash firstName=firstName lastName=lastName age=age firstName=firstName)}}</template>',
123+
output:
124+
'<template>{{employee-profile employee=(hash firstName=firstName lastName=lastName age=age)}}</template>',
125+
errors: [{ messageId: 'duplicateSubExpr', data: { name: 'firstName' } }],
126+
},
127+
{
128+
code: '<template>{{employee-profile employee=(hash fullName=(hash firstName=firstName lastName=lastName firstName=firstName) age=age)}}</template>',
129+
output:
130+
'<template>{{employee-profile employee=(hash fullName=(hash firstName=firstName lastName=lastName) age=age)}}</template>',
131+
errors: [{ messageId: 'duplicateSubExpr', data: { name: 'firstName' } }],
132+
},
133+
],
134+
});

0 commit comments

Comments
 (0)