Skip to content

Commit 99b3acd

Browse files
committed
Extract rule: template-no-duplicate-attributes
1 parent ffc4ad8 commit 99b3acd

4 files changed

Lines changed: 237 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ rules in templates can be disabled with eslint directives with mustache or html
186186
| :----------------------------------------------------------------------------------------- | :-------------------------------------------------------- | :- | :- | :- |
187187
| [template-builtin-component-arguments](docs/rules/template-builtin-component-arguments.md) | disallow setting certain attributes on builtin components | | | |
188188
| [template-no-debugger](docs/rules/template-no-debugger.md) | disallow {{debugger}} in templates | | | |
189+
| [template-no-duplicate-attributes](docs/rules/template-no-duplicate-attributes.md) | disallow duplicate attribute names in templates | | 🔧 | |
189190
| [template-no-log](docs/rules/template-no-log.md) | disallow {{log}} in templates | | | |
190191

191192
### Components
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 no-duplicate-attributes](https://github.com/eslint-plugin-ember/eslint-plugin-ember/blob/master/docs/rule/no-duplicate-attributes.md)
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
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+
},
21+
22+
create(context) {
23+
function checkForDuplicates(node, attributes, identifier, messageId) {
24+
if (!attributes || attributes.length < 2) {
25+
return;
26+
}
27+
28+
const seen = new Map();
29+
30+
for (const attr of attributes) {
31+
const key = attr[identifier];
32+
if (seen.has(key)) {
33+
context.report({
34+
node: attr,
35+
messageId,
36+
data: { name: key },
37+
fix(fixer) {
38+
// Remove the duplicate attribute including preceding whitespace
39+
const sourceCode = context.sourceCode;
40+
const text = sourceCode.getText();
41+
const attrStart = attr.range[0];
42+
const attrEnd = attr.range[1];
43+
44+
// Look for whitespace before the attribute
45+
let removeStart = attrStart;
46+
while (removeStart > 0 && /\s/.test(text[removeStart - 1])) {
47+
removeStart--;
48+
}
49+
50+
return fixer.removeRange([removeStart, attrEnd]);
51+
},
52+
});
53+
} else {
54+
seen.set(key, attr);
55+
}
56+
}
57+
}
58+
59+
return {
60+
GlimmerElementNode(node) {
61+
checkForDuplicates(node, node.attributes, 'name', 'duplicateElement');
62+
},
63+
64+
GlimmerBlockStatement(node) {
65+
const attributes = node.hash?.pairs || [];
66+
checkForDuplicates(node, attributes, 'key', 'duplicateBlock');
67+
},
68+
69+
GlimmerMustacheStatement(node) {
70+
const attributes = node.hash?.pairs || [];
71+
checkForDuplicates(node, attributes, 'key', 'duplicateMustache');
72+
},
73+
74+
GlimmerSubExpression(node) {
75+
const attributes = node.hash?.pairs || [];
76+
checkForDuplicates(node, attributes, 'key', 'duplicateSubExpr');
77+
},
78+
};
79+
},
80+
};
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
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+
35+
invalid: [
36+
{
37+
code: `<template>
38+
<div class="foo" class="bar"></div>
39+
</template>`,
40+
output: `<template>
41+
<div class="foo"></div>
42+
</template>`,
43+
errors: [
44+
{
45+
message: "Duplicate attribute 'class' found in the Element.",
46+
type: 'GlimmerAttrNode',
47+
},
48+
],
49+
},
50+
{
51+
code: `<template>
52+
<input type="text" disabled type="email" />
53+
</template>`,
54+
output: `<template>
55+
<input type="text" disabled />
56+
</template>`,
57+
errors: [
58+
{
59+
message: "Duplicate attribute 'type' found in the Element.",
60+
type: 'GlimmerAttrNode',
61+
},
62+
],
63+
},
64+
{
65+
code: `<template>
66+
{{helper foo="bar" foo="baz"}}
67+
</template>`,
68+
output: `<template>
69+
{{helper foo="bar"}}
70+
</template>`,
71+
errors: [
72+
{
73+
message: "Duplicate attribute 'foo' found in the MustacheStatement.",
74+
type: 'GlimmerHashPair',
75+
},
76+
],
77+
},
78+
{
79+
code: `<template>
80+
{{#if condition key="a" key="b"}}
81+
content
82+
{{/if}}
83+
</template>`,
84+
output: `<template>
85+
{{#if condition key="a"}}
86+
content
87+
{{/if}}
88+
</template>`,
89+
errors: [
90+
{
91+
message: "Duplicate attribute 'key' found in the BlockStatement.",
92+
type: 'GlimmerHashPair',
93+
},
94+
],
95+
},
96+
],
97+
});

0 commit comments

Comments
 (0)