Skip to content

Commit ab9aeef

Browse files
author
Yannick Croissant
committed
Add self-closing-comp rule
1 parent 2dcbdc3 commit ab9aeef

File tree

6 files changed

+156
-3
lines changed

6 files changed

+156
-3
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ Finally, enable all of the rules that you would like to use.
4646
"react/no-multi-comp": 1,
4747
"react/prop-types": 1,
4848
"react/display-name": 1,
49-
"react/wrap-multilines": 1
49+
"react/wrap-multilines": 1,
50+
"react/self-closing-comp": 1
5051
}
5152
}
5253
```
@@ -57,6 +58,7 @@ Finally, enable all of the rules that you would like to use.
5758
* [prop-types](docs/rules/prop-types.md): Prevent missing propTypes in a React component definition
5859
* [display-name](docs/rules/display-name.md): Prevent missing displayName in a React component definition
5960
* [wrap-multilines](docs/rules/wrap-multilines.md): Prevent missing parentheses around multilines JSX
61+
* [self-closing-comp](docs/rules/self-closing-comp.md): Prevent extra closing tags for components without children
6062

6163
## To Do
6264

docs/rules/self-closing-comp.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Prevent extra closing tags for components without children (self-closing-comp)
2+
3+
Components without children can be self-closed to avoid unnecessary extra closing tag.
4+
5+
## Rule Details
6+
7+
The following patterns are considered warnings:
8+
9+
```js
10+
var HelloJohn = <Hello name="John"></Hello>;
11+
```
12+
13+
The following patterns are not warnings:
14+
15+
```js
16+
var contentContainer = <div className="content"></div>;
17+
18+
var HelloJohn = <Hello name="John" />;
19+
20+
var Profile = <Hello name="John"><img src="picture.png" /></Hello>;
21+
```

index.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ module.exports = {
55
'no-multi-comp': require('./lib/rules/no-multi-comp'),
66
'prop-types': require('./lib/rules/prop-types'),
77
'display-name': require('./lib/rules/display-name'),
8-
'wrap-multilines': require('./lib/rules/wrap-multilines')
8+
'wrap-multilines': require('./lib/rules/wrap-multilines'),
9+
'self-closing-comp': require('./lib/rules/self-closing-comp')
910
},
1011
rulesConfig: {
1112
'no-multi-comp': 0,
1213
'prop-types': 0,
1314
'display-name': 0,
14-
'wrap-multilines': 0
15+
'wrap-multilines': 0,
16+
'self-closing-comp': 0
1517
}
1618
};

lib/rules/self-closing-comp.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/**
2+
* @fileoverview Prevent extra closing tags for components without children
3+
* @author Yannick Croissant
4+
*/
5+
'use strict';
6+
7+
// ------------------------------------------------------------------------------
8+
// Rule Definition
9+
// ------------------------------------------------------------------------------
10+
11+
module.exports = function(context) {
12+
13+
// HTML5 and SVG elements whitelist
14+
var standardElements = [
15+
'a', 'abbr', 'address', 'area', 'article', 'aside', 'audio', 'b', 'base', 'bdi', 'bdo', 'big', 'blockquote', 'body',
16+
'br', 'button', 'canvas', 'caption', 'cite', 'code', 'col', 'colgroup', 'data', 'datalist', 'dd', 'del', 'details',
17+
'dfn', 'dialog', 'div', 'dl', 'dt', 'em', 'embed', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2',
18+
'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hr', 'html', 'i', 'iframe', 'img', 'input', 'ins', 'kbd', 'keygen',
19+
'label', 'legend', 'li', 'link', 'main', 'map', 'mark', 'menu', 'menuitem', 'meta', 'meter', 'nav', 'noscript',
20+
'object', 'ol', 'optgroup', 'option', 'output', 'p', 'param', 'picture', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby',
21+
's', 'samp', 'script', 'section', 'select', 'small', 'source', 'span', 'strong', 'style', 'sub', 'summary', 'sup',
22+
'table', 'tbody', 'td', 'textarea', 'tfoot', 'th', 'thead', 'time', 'title', 'tr', 'track', 'u', 'ul', 'var',
23+
'video', 'wbr', 'circle', 'defs', 'ellipse', 'g', 'line', 'linearGradient', 'mask', 'path', 'pattern', 'polygon',
24+
'polyline', 'radialGradient', 'rect', 'stop', 'svg', 'text', 'tspan'
25+
];
26+
27+
function isComponent(node) {
28+
return node.name && node.name.type === 'XJSIdentifier' && standardElements.indexOf(node.name.name) === -1;
29+
}
30+
31+
function hasChildren(node) {
32+
if (
33+
!node.parent.children.length ||
34+
(node.parent.children[0].type === 'Literal' && !node.parent.children[0].value.trim())
35+
) {
36+
return false;
37+
}
38+
return true;
39+
}
40+
41+
// --------------------------------------------------------------------------
42+
// Public
43+
// --------------------------------------------------------------------------
44+
45+
return {
46+
47+
'XJSOpeningElement': function(node) {
48+
if (!isComponent(node) || node.selfClosing || hasChildren(node)) {
49+
return;
50+
}
51+
context.report(node, 'Empty components are self-closing');
52+
}
53+
};
54+
55+
};

lib/rules/self-closing.js

Whitespace-only changes.

tests/lib/rules/self-closing-comp.js

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/**
2+
* @fileoverview Prevent extra closing tags for components without children
3+
* @author Yannick Croissant
4+
*/
5+
'use strict';
6+
7+
// ------------------------------------------------------------------------------
8+
// Requirements
9+
// ------------------------------------------------------------------------------
10+
11+
var eslint = require('eslint').linter;
12+
var ESLintTester = require('eslint-tester');
13+
14+
// ------------------------------------------------------------------------------
15+
// Tests
16+
// ------------------------------------------------------------------------------
17+
18+
var eslintTester = new ESLintTester(eslint);
19+
eslintTester.addRuleTest('lib/rules/self-closing-comp', {
20+
21+
valid: [
22+
{
23+
code: 'var contentContainer = <div className="content"></div>;',
24+
settings: {
25+
ecmascript: 6,
26+
jsx: true
27+
}
28+
}, {
29+
code: 'var HelloJohn = <Hello name="John" />;',
30+
settings: {
31+
ecmascript: 6,
32+
jsx: true
33+
}
34+
}, {
35+
code: 'var Profile = <Hello name="John"><img src="picture.png" /></Hello>;',
36+
settings: {
37+
ecmascript: 6,
38+
jsx: true
39+
}
40+
}
41+
],
42+
43+
invalid: [
44+
{
45+
code: 'var HelloJohn = <Hello name="John"></Hello>;',
46+
settings: {
47+
ecmascript: 6,
48+
jsx: true
49+
},
50+
errors: [{
51+
message: 'Empty components are self-closing'
52+
}]
53+
}, {
54+
code: 'var HelloJohn = <Hello name="John">\n</Hello>;',
55+
settings: {
56+
ecmascript: 6,
57+
jsx: true
58+
},
59+
errors: [{
60+
message: 'Empty components are self-closing'
61+
}]
62+
}, {
63+
code: 'var HelloJohn = <Hello name="John"> </Hello>;',
64+
settings: {
65+
ecmascript: 6,
66+
jsx: true
67+
},
68+
errors: [{
69+
message: 'Empty components are self-closing'
70+
}]
71+
}
72+
]
73+
});

0 commit comments

Comments
 (0)