Skip to content

Commit 9948883

Browse files
committed
Add jsx-closing-bracket-location rule (fixes #14, fixes #64)
1 parent dc895db commit 9948883

File tree

5 files changed

+408
-2
lines changed

5 files changed

+408
-2
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ Finally, enable all of the rules that you would like to use.
4444
"rules": {
4545
"react/display-name": 1,
4646
"react/jsx-boolean-value": 1,
47+
"react/jsx-closing-bracket-location": 1,
4748
"react/jsx-curly-spacing": 1,
4849
"react/jsx-max-props-per-line": 1,
4950
"react/jsx-indent-props": 1,
@@ -74,6 +75,7 @@ Finally, enable all of the rules that you would like to use.
7475

7576
* [display-name](docs/rules/display-name.md): Prevent missing displayName in a React component definition
7677
* [jsx-boolean-value](docs/rules/jsx-boolean-value.md): Enforce boolean attributes notation in JSX
78+
* [jsx-closing-bracket-location](docs/rules/jsx-closing-bracket-location.md): Validate closing bracket location in JSX
7779
* [jsx-curly-spacing](docs/rules/jsx-curly-spacing.md): Enforce or disallow spaces inside of curly braces in JSX attributes
7880
* [jsx-max-props-per-line](docs/rules/jsx-max-props-per-line.md): Limit maximum of props on a single line in JSX
7981
* [jsx-indent-props](docs/rules/jsx-indent-props.md): Validate props indentation in JSX
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# Validate closing bracket location in JSX (jsx-closing-bracket-location)
2+
3+
Enforce the closing bracket location for JSX multiline elements.
4+
5+
## Rule Details
6+
7+
This rule checks all JSX multiline elements and verifies the location of the closing bracket. By default this one must be aligned with the opening tag.
8+
9+
The following patterns are considered warnings:
10+
11+
```jsx
12+
<Hello
13+
lastName="Smith"
14+
firstName="John" />;
15+
16+
<Hello
17+
lastName="Smith"
18+
firstName="John"
19+
/>;
20+
```
21+
22+
The following patterns are not considered warnings:
23+
24+
```jsx
25+
<Hello firstName="John" lastName="Smith" />;
26+
27+
<Hello
28+
firstName="John"
29+
lastName="Smith"
30+
/>;
31+
```
32+
33+
## Rule Options
34+
35+
```js
36+
...
37+
"jsx-closing-bracket-location": [<enabled>, { "location": <string> }]
38+
...
39+
```
40+
41+
### `location`
42+
43+
Enforced location for the closing bracket.
44+
45+
* `tag-aligned`: must be aligned with the opening tag.
46+
* `after-props`: must be placed right after the last prop.
47+
* `props-aligned`: must be aligned with the last prop.
48+
49+
Default to `tag-aligned`.
50+
51+
The following patterns are considered warnings:
52+
53+
```jsx
54+
// [1, {location: 'tag-aligned'}]
55+
<Hello
56+
firstName="John"
57+
lastName="Smith"
58+
/>;
59+
60+
// [1, {location: 'after-props'}]
61+
<Hello
62+
firstName="John"
63+
lastName="Smith"
64+
/>;
65+
66+
// [1, {location: 'props-aligned'}]
67+
<Hello
68+
firstName="John"
69+
lastName="Smith" />;
70+
```
71+
72+
The following patterns are not considered warnings:
73+
74+
```jsx
75+
// [1, {location: 'tag-aligned'}]
76+
<Hello
77+
firstName="John"
78+
lastName="Smith"
79+
/>;
80+
81+
// [1, {location: 'after-props'}]
82+
<Hello
83+
firstName="John"
84+
lastName="Smith" />;
85+
86+
// [1, {location: 'props-aligned'}]
87+
<Hello
88+
firstName="John"
89+
lastName="Smith"
90+
/>;
91+
```
92+
93+
## When not to use
94+
95+
If you are not using JSX then you can disable this rule.

index.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ module.exports = {
2626
'jsx-no-duplicate-props': require('./lib/rules/jsx-no-duplicate-props'),
2727
'jsx-max-props-per-line': require('./lib/rules/jsx-max-props-per-line'),
2828
'jsx-no-literals': require('./lib/rules/jsx-no-literals'),
29-
'jsx-indent-props': require('./lib/rules/jsx-indent-props')
29+
'jsx-indent-props': require('./lib/rules/jsx-indent-props'),
30+
'jsx-closing-bracket-location': require('./lib/rules/jsx-closing-bracket-location')
3031
},
3132
rulesConfig: {
3233
'jsx-uses-react': 0,
@@ -53,6 +54,7 @@ module.exports = {
5354
'jsx-no-duplicate-props': 0,
5455
'jsx-max-props-per-line': 0,
5556
'jsx-no-literals': 0,
56-
'jsx-indent-props': 0
57+
'jsx-indent-props': 0,
58+
'jsx-closing-bracket-location': 0
5759
}
5860
};
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/**
2+
* @fileoverview Validate closing bracket location in JSX
3+
* @author Yannick Croissant
4+
*/
5+
'use strict';
6+
7+
// ------------------------------------------------------------------------------
8+
// Rule Definition
9+
// ------------------------------------------------------------------------------
10+
module.exports = function(context) {
11+
12+
var MESSAGE = 'The closing bracket must be {{location}}';
13+
var MESSAGE_LOCATION = {
14+
'after-props': 'placed after the last prop',
15+
'after-tag': 'placed after the opening tag',
16+
'props-aligned': 'aligned with the last prop',
17+
'tag-aligned': 'aligned with the opening tag'
18+
};
19+
20+
/**
21+
* Get expected location for the closing bracket
22+
* @param {Object} tokens Locations of the opening bracket, closing bracket and last prop
23+
* @return {String} Expected location for the closing bracket
24+
*/
25+
function getExpectedLocation(tokens) {
26+
var location;
27+
// Is always after the opening tag if there is no props
28+
if (typeof tokens.lastProp === 'undefined') {
29+
location = 'after-tag';
30+
// Is always after the last prop if this one is on the same line as the opening bracket
31+
} else if (tokens.opening.line === tokens.lastProp.line) {
32+
location = 'after-props';
33+
// Else use configuration, or default value
34+
} else {
35+
location = context.options[0] && context.options[0].location || 'tag-aligned';
36+
}
37+
return location;
38+
}
39+
40+
/**
41+
* Check if the closing bracket is correctly located
42+
* @param {Object} tokens Locations of the opening bracket, closing bracket and last prop
43+
* @param {String} expectedLocation Expected location for the closing bracket
44+
* @return {Boolean} True if the closing bracket is correctly located, false if not
45+
*/
46+
function hasCorrectLocation(tokens, expectedLocation) {
47+
switch (expectedLocation) {
48+
case 'after-tag':
49+
return tokens.tag.line === tokens.closing.line;
50+
case 'after-props':
51+
return tokens.lastProp.line === tokens.closing.line;
52+
case 'props-aligned':
53+
return tokens.lastProp.column === tokens.closing.column;
54+
case 'tag-aligned':
55+
return tokens.opening.column === tokens.closing.column;
56+
default:
57+
return true;
58+
}
59+
}
60+
61+
/**
62+
* Get the locations of the opening bracket, closing bracket and last prop
63+
* @param {ASTNode} node The node to check
64+
* @return {Object} Locations of the opening bracket, closing bracket and last prop
65+
*/
66+
function getTokensLocations(node) {
67+
var opening = context.getFirstToken(node).loc.start;
68+
var closing = context.getLastTokens(node, node.selfClosing ? 2 : 1)[0].loc.start;
69+
var tag = context.getFirstToken(node.name).loc.start;
70+
var lastProp;
71+
if (node.attributes.length) {
72+
lastProp = context.getFirstToken(node.attributes[node.attributes.length - 1]).loc.start;
73+
}
74+
return {
75+
tag: tag,
76+
opening: opening,
77+
closing: closing,
78+
lastProp: lastProp
79+
};
80+
}
81+
82+
return {
83+
JSXOpeningElement: function(node) {
84+
var tokens = getTokensLocations(node);
85+
var expectedLocation = getExpectedLocation(tokens);
86+
if (hasCorrectLocation(tokens, expectedLocation)) {
87+
return;
88+
}
89+
context.report(node, MESSAGE, {
90+
location: MESSAGE_LOCATION[expectedLocation]
91+
});
92+
}
93+
};
94+
95+
};
96+
97+
module.exports.schema = [{
98+
type: 'object',
99+
properties: {
100+
location: {
101+
enum: ['after-props', 'props-aligned', 'tag-aligned']
102+
}
103+
}
104+
}];

0 commit comments

Comments
 (0)