Skip to content

Commit 24deb76

Browse files
Merge pull request #2476 from NullVoxPopuli/nvp/template-lint-extract-rule-template-no-jsx-attributes
Extract rule: template-no-jsx-attributes
2 parents 140ae42 + b66bb57 commit 24deb76

4 files changed

Lines changed: 359 additions & 0 deletions

File tree

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,12 @@ rules in templates can be disabled with eslint directives with mustache or html
331331
| [no-runloop](docs/rules/no-runloop.md) | disallow usage of `@ember/runloop` functions || | |
332332
| [require-fetch-import](docs/rules/require-fetch-import.md) | enforce explicit import for `fetch()` | | | |
333333

334+
### Possible Errors
335+
336+
| Name | Description | 💼 | 🔧 | 💡 |
337+
| :--------------------------------------------------------------------- | :-------------------------------------- | :- | :- | :- |
338+
| [template-no-jsx-attributes](docs/rules/template-no-jsx-attributes.md) | disallow JSX-style camelCase attributes | | 🔧 | |
339+
334340
### Routes
335341

336342
| Name                             | Description | 💼 | 🔧 | 💡 |
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# ember/template-no-jsx-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 JSX-style camelCase attributes in templates.
8+
9+
Folks coming from React may have developed habits around how they type attributes on elements.
10+
JSX isn't HTML (it's JS), so in JS, you can't have kebab-case identifiers, so JSX uses camelCase.
11+
12+
However, since Ember uses HTML, camelCase attributes are not valid when writing components.
13+
14+
## Examples
15+
16+
This rule **forbids** the following attributes:
17+
18+
- acceptCharset
19+
- accessKey
20+
- allowFullScreen
21+
- allowTransparency
22+
- autoComplete
23+
- autoFocus
24+
- autoPlay
25+
- cellPadding
26+
- cellSpacing
27+
- charSet
28+
- className
29+
- contentEditable
30+
- contextMenu
31+
- crossOrigin
32+
- dataTime
33+
- encType
34+
- formAction
35+
- formEncType
36+
- formMethod
37+
- formNoValidate
38+
- formTarget
39+
- frameBorder
40+
- httpEquiv
41+
- inputMode
42+
- keyParams
43+
- keyType
44+
- noValidate
45+
- marginHeight
46+
- marginWidth
47+
- maxLength
48+
- mediaGroup
49+
- minLength
50+
- radioGroup
51+
- readOnly
52+
- rowSpan
53+
- spellCheck
54+
- srcDoc
55+
- srcSet
56+
- tabIndex
57+
- useMap
58+
59+
This rule **forbids** the following:
60+
61+
```gjs
62+
<template>
63+
<div className='foo'></div>
64+
<div contentEditable='true'></div>
65+
<img srcSet='image.jpg 1x, image@2x.jpg 2x' />
66+
</template>
67+
```
68+
69+
This rule **allows** the following:
70+
71+
```gjs
72+
<template>
73+
<div class='foo'></div>
74+
<div contenteditable='true'></div>
75+
<img srcset='image.jpg 1x, image@2x.jpg 2x' />
76+
</template>
77+
```
78+
79+
## Migration
80+
81+
Convert attributes to kebab-case[^camelCaseNote]
82+
83+
- `<div className="...">` -> `<div class="...">`
84+
- `<video autoPlay>` -> `<video auto-play>`
85+
- `<div contentEditable>` -> `<div content-editable>`
86+
- etc
87+
88+
[^camelCaseNote]: keep in mind that `@args`, and `<:blocks>` should be js-compatible identifiers and be camelCase
89+
90+
## References
91+
92+
- [HTML Attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes)
93+
- [React JSX differences](https://reactjs.org/docs/dom-elements.html#differences-in-attributes)
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
const fixMap = {
2+
acceptCharset: 'accept-charset',
3+
srcSet: 'srcset',
4+
accessKey: 'accesskey',
5+
allowFullScreen: 'allowfullscreen',
6+
allowTransparency: 'allowtransparency',
7+
autoComplete: 'autocomplete',
8+
autoFocus: 'autofocus',
9+
autoPlay: 'autoplay',
10+
cellPadding: 'cellpadding',
11+
cellSpacing: 'cellspacing',
12+
charSet: 'charset',
13+
className: 'class',
14+
contentEditable: 'contenteditable',
15+
contextMenu: 'contextmenu',
16+
crossOrigin: 'crossorigin',
17+
dateTime: 'datetime',
18+
encType: 'enctype',
19+
formAction: 'formaction',
20+
formEncType: 'formenctype',
21+
formMethod: 'formmethod',
22+
formNoValidate: 'formnovalidate',
23+
formTarget: 'formtarget',
24+
frameBorder: 'frameborder',
25+
httpEquiv: 'http-equiv',
26+
inputMode: 'inputmode',
27+
keyType: 'keytype',
28+
noValidate: 'novalidate',
29+
marginHeight: 'marginheight',
30+
marginWidth: 'marginwidth',
31+
maxLength: 'maxlength',
32+
minLength: 'minlength',
33+
radioGroup: 'radiogroup',
34+
readOnly: 'readonly',
35+
rowSpan: 'rowspan',
36+
colSpan: 'colspan',
37+
spellCheck: 'spellcheck',
38+
srcDoc: 'srcdoc',
39+
tabIndex: 'tabindex',
40+
useMap: 'usemap',
41+
};
42+
43+
const camelCaseAttributes = Object.keys(fixMap);
44+
45+
function getMessage(name) {
46+
if (name === 'className') {
47+
return `Attribute, ${name}, does not assign the 'class' attribute as it would in JSX. To assign the 'class' attribute, set the 'class' attribute, instead of 'className'. In HTML, all attributes are valid, but 'className' doesn't do anything.`;
48+
}
49+
50+
return `Incorrect html attribute name detected - "${name}", is probably unintended. Attributes in HTML are kebeb case.`;
51+
}
52+
53+
/** @type {import('eslint').Rule.RuleModule} */
54+
module.exports = {
55+
meta: {
56+
type: 'problem',
57+
docs: {
58+
description: 'disallow JSX-style camelCase attributes',
59+
category: 'Possible Errors',
60+
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-no-jsx-attributes.md',
61+
templateMode: 'both',
62+
},
63+
fixable: 'code',
64+
schema: [],
65+
messages: {},
66+
originallyFrom: {
67+
name: 'ember-template-lint',
68+
rule: 'lib/rules/no-jsx-attributes.js',
69+
docs: 'docs/rule/no-jsx-attributes.md',
70+
tests: 'test/unit/rules/no-jsx-attributes-test.js',
71+
},
72+
},
73+
74+
create(context) {
75+
return {
76+
GlimmerAttrNode(node) {
77+
const key = node.name;
78+
const isJSXProbably = camelCaseAttributes.includes(key);
79+
80+
if (!isJSXProbably) {
81+
return;
82+
}
83+
84+
context.report({
85+
node,
86+
message: getMessage(key),
87+
fix: fixMap[key]
88+
? (fixer) => {
89+
const sourceCode = context.getSourceCode();
90+
const text = sourceCode.getText(node);
91+
const valueMatch = text.match(/^[^=]+(=.*)?$/);
92+
const value = valueMatch && valueMatch[1] ? valueMatch[1] : '';
93+
return fixer.replaceText(node, `${fixMap[key]}${value}`);
94+
}
95+
: null,
96+
});
97+
},
98+
};
99+
},
100+
};
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
const rule = require('../../../lib/rules/template-no-jsx-attributes');
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-jsx-attributes', rule, {
10+
valid: [
11+
'<template><div></div></template>',
12+
'<template><div class="foo"></div></template>',
13+
'<template><div class></div></template>',
14+
'<template><div autoplay></div></template>',
15+
'<template><div contenteditable="true"></div></template>',
16+
],
17+
invalid: [
18+
{
19+
code: '<template><div acceptCharset="utf-8"></div></template>',
20+
output: '<template><div accept-charset="utf-8"></div></template>',
21+
errors: [
22+
{
23+
message:
24+
'Incorrect html attribute name detected - "acceptCharset", is probably unintended. Attributes in HTML are kebeb case.',
25+
},
26+
],
27+
},
28+
{
29+
code: '<template><div contentEditable="true"></div></template>',
30+
output: '<template><div contenteditable="true"></div></template>',
31+
errors: [
32+
{
33+
message:
34+
'Incorrect html attribute name detected - "contentEditable", is probably unintended. Attributes in HTML are kebeb case.',
35+
},
36+
],
37+
},
38+
{
39+
code: '<template><div className></div></template>',
40+
output: '<template><div class></div></template>',
41+
errors: [
42+
{
43+
message:
44+
"Attribute, className, does not assign the 'class' attribute as it would in JSX. To assign the 'class' attribute, set the 'class' attribute, instead of 'className'. In HTML, all attributes are valid, but 'className' doesn't do anything.",
45+
},
46+
],
47+
},
48+
{
49+
code: '<template><div className="foo"></div></template>',
50+
output: '<template><div class="foo"></div></template>',
51+
errors: [
52+
{
53+
message:
54+
"Attribute, className, does not assign the 'class' attribute as it would in JSX. To assign the 'class' attribute, set the 'class' attribute, instead of 'className'. In HTML, all attributes are valid, but 'className' doesn't do anything.",
55+
},
56+
],
57+
},
58+
59+
{
60+
code: '<template><div autoPlay></div></template>',
61+
output: '<template><div autoplay></div></template>',
62+
errors: [
63+
{
64+
message:
65+
'Incorrect html attribute name detected - "autoPlay", is probably unintended. Attributes in HTML are kebeb case.',
66+
},
67+
],
68+
},
69+
{
70+
code: '<template><div contentEditable></div></template>',
71+
output: '<template><div contenteditable></div></template>',
72+
errors: [
73+
{
74+
message:
75+
'Incorrect html attribute name detected - "contentEditable", is probably unintended. Attributes in HTML are kebeb case.',
76+
},
77+
],
78+
},
79+
],
80+
});
81+
82+
const hbsRuleTester = new RuleTester({
83+
parser: require.resolve('ember-eslint-parser/hbs'),
84+
parserOptions: {
85+
ecmaVersion: 2022,
86+
sourceType: 'module',
87+
},
88+
});
89+
90+
hbsRuleTester.run('template-no-jsx-attributes', rule, {
91+
valid: [
92+
'<div></div>',
93+
'<div class="foo"></div>',
94+
'<div class></div>',
95+
'<div autoplay></div>',
96+
'<div contenteditable="true"></div>',
97+
],
98+
invalid: [
99+
{
100+
code: '<div acceptCharset="utf-8"></div>',
101+
output: '<div accept-charset="utf-8"></div>',
102+
errors: [
103+
{
104+
message:
105+
'Incorrect html attribute name detected - "acceptCharset", is probably unintended. Attributes in HTML are kebeb case.',
106+
},
107+
],
108+
},
109+
{
110+
code: '<div contentEditable="true"></div>',
111+
output: '<div contenteditable="true"></div>',
112+
errors: [
113+
{
114+
message:
115+
'Incorrect html attribute name detected - "contentEditable", is probably unintended. Attributes in HTML are kebeb case.',
116+
},
117+
],
118+
},
119+
{
120+
code: '<div className></div>',
121+
output: '<div class></div>',
122+
errors: [
123+
{
124+
message:
125+
"Attribute, className, does not assign the 'class' attribute as it would in JSX. To assign the 'class' attribute, set the 'class' attribute, instead of 'className'. In HTML, all attributes are valid, but 'className' doesn't do anything.",
126+
},
127+
],
128+
},
129+
{
130+
code: '<div className="foo"></div>',
131+
output: '<div class="foo"></div>',
132+
errors: [
133+
{
134+
message:
135+
"Attribute, className, does not assign the 'class' attribute as it would in JSX. To assign the 'class' attribute, set the 'class' attribute, instead of 'className'. In HTML, all attributes are valid, but 'className' doesn't do anything.",
136+
},
137+
],
138+
},
139+
{
140+
code: '<div autoPlay></div>',
141+
output: '<div autoplay></div>',
142+
errors: [
143+
{
144+
message:
145+
'Incorrect html attribute name detected - "autoPlay", is probably unintended. Attributes in HTML are kebeb case.',
146+
},
147+
],
148+
},
149+
{
150+
code: '<div contentEditable></div>',
151+
output: '<div contenteditable></div>',
152+
errors: [
153+
{
154+
message:
155+
'Incorrect html attribute name detected - "contentEditable", is probably unintended. Attributes in HTML are kebeb case.',
156+
},
157+
],
158+
},
159+
],
160+
});

0 commit comments

Comments
 (0)