Skip to content

Commit

Permalink
Add jsx-closing-bracket-location rule (fixes #14, fixes #64)
Browse files Browse the repository at this point in the history
  • Loading branch information
yannickcr committed Aug 26, 2015
1 parent dc895db commit 9948883
Show file tree
Hide file tree
Showing 5 changed files with 408 additions and 2 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ Finally, enable all of the rules that you would like to use.
"rules": {
"react/display-name": 1,
"react/jsx-boolean-value": 1,
"react/jsx-closing-bracket-location": 1,
"react/jsx-curly-spacing": 1,
"react/jsx-max-props-per-line": 1,
"react/jsx-indent-props": 1,
Expand Down Expand Up @@ -74,6 +75,7 @@ Finally, enable all of the rules that you would like to use.

* [display-name](docs/rules/display-name.md): Prevent missing displayName in a React component definition
* [jsx-boolean-value](docs/rules/jsx-boolean-value.md): Enforce boolean attributes notation in JSX
* [jsx-closing-bracket-location](docs/rules/jsx-closing-bracket-location.md): Validate closing bracket location in JSX
* [jsx-curly-spacing](docs/rules/jsx-curly-spacing.md): Enforce or disallow spaces inside of curly braces in JSX attributes
* [jsx-max-props-per-line](docs/rules/jsx-max-props-per-line.md): Limit maximum of props on a single line in JSX
* [jsx-indent-props](docs/rules/jsx-indent-props.md): Validate props indentation in JSX
Expand Down
95 changes: 95 additions & 0 deletions docs/rules/jsx-closing-bracket-location.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Validate closing bracket location in JSX (jsx-closing-bracket-location)

Enforce the closing bracket location for JSX multiline elements.

## Rule Details

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.

The following patterns are considered warnings:

```jsx
<Hello
lastName="Smith"
firstName="John" />;

<Hello
lastName="Smith"
firstName="John"
/>;
```

The following patterns are not considered warnings:

```jsx
<Hello firstName="John" lastName="Smith" />;

<Hello
firstName="John"
lastName="Smith"
/>;
```

## Rule Options

```js
...
"jsx-closing-bracket-location": [<enabled>, { "location": <string> }]
...
```

### `location`

Enforced location for the closing bracket.

* `tag-aligned`: must be aligned with the opening tag.
* `after-props`: must be placed right after the last prop.
* `props-aligned`: must be aligned with the last prop.

Default to `tag-aligned`.

The following patterns are considered warnings:

```jsx
// [1, {location: 'tag-aligned'}]
<Hello
firstName="John"
lastName="Smith"
/>;

// [1, {location: 'after-props'}]
<Hello
firstName="John"
lastName="Smith"
/>;

// [1, {location: 'props-aligned'}]
<Hello
firstName="John"
lastName="Smith" />;
```

The following patterns are not considered warnings:

```jsx
// [1, {location: 'tag-aligned'}]
<Hello
firstName="John"
lastName="Smith"
/>;

// [1, {location: 'after-props'}]
<Hello
firstName="John"
lastName="Smith" />;

// [1, {location: 'props-aligned'}]
<Hello
firstName="John"
lastName="Smith"
/>;
```

## When not to use

If you are not using JSX then you can disable this rule.
6 changes: 4 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ module.exports = {
'jsx-no-duplicate-props': require('./lib/rules/jsx-no-duplicate-props'),
'jsx-max-props-per-line': require('./lib/rules/jsx-max-props-per-line'),
'jsx-no-literals': require('./lib/rules/jsx-no-literals'),
'jsx-indent-props': require('./lib/rules/jsx-indent-props')
'jsx-indent-props': require('./lib/rules/jsx-indent-props'),
'jsx-closing-bracket-location': require('./lib/rules/jsx-closing-bracket-location')
},
rulesConfig: {
'jsx-uses-react': 0,
Expand All @@ -53,6 +54,7 @@ module.exports = {
'jsx-no-duplicate-props': 0,
'jsx-max-props-per-line': 0,
'jsx-no-literals': 0,
'jsx-indent-props': 0
'jsx-indent-props': 0,
'jsx-closing-bracket-location': 0
}
};
104 changes: 104 additions & 0 deletions lib/rules/jsx-closing-bracket-location.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/**
* @fileoverview Validate closing bracket location in JSX
* @author Yannick Croissant
*/
'use strict';

// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = function(context) {

var MESSAGE = 'The closing bracket must be {{location}}';
var MESSAGE_LOCATION = {
'after-props': 'placed after the last prop',
'after-tag': 'placed after the opening tag',
'props-aligned': 'aligned with the last prop',
'tag-aligned': 'aligned with the opening tag'
};

/**
* Get expected location for the closing bracket
* @param {Object} tokens Locations of the opening bracket, closing bracket and last prop
* @return {String} Expected location for the closing bracket
*/
function getExpectedLocation(tokens) {
var location;
// Is always after the opening tag if there is no props
if (typeof tokens.lastProp === 'undefined') {
location = 'after-tag';
// Is always after the last prop if this one is on the same line as the opening bracket
} else if (tokens.opening.line === tokens.lastProp.line) {
location = 'after-props';
// Else use configuration, or default value
} else {
location = context.options[0] && context.options[0].location || 'tag-aligned';
}
return location;
}

/**
* Check if the closing bracket is correctly located
* @param {Object} tokens Locations of the opening bracket, closing bracket and last prop
* @param {String} expectedLocation Expected location for the closing bracket
* @return {Boolean} True if the closing bracket is correctly located, false if not
*/
function hasCorrectLocation(tokens, expectedLocation) {
switch (expectedLocation) {
case 'after-tag':
return tokens.tag.line === tokens.closing.line;
case 'after-props':
return tokens.lastProp.line === tokens.closing.line;
case 'props-aligned':
return tokens.lastProp.column === tokens.closing.column;
case 'tag-aligned':
return tokens.opening.column === tokens.closing.column;
default:
return true;
}
}

/**
* Get the locations of the opening bracket, closing bracket and last prop
* @param {ASTNode} node The node to check
* @return {Object} Locations of the opening bracket, closing bracket and last prop
*/
function getTokensLocations(node) {
var opening = context.getFirstToken(node).loc.start;
var closing = context.getLastTokens(node, node.selfClosing ? 2 : 1)[0].loc.start;
var tag = context.getFirstToken(node.name).loc.start;
var lastProp;
if (node.attributes.length) {
lastProp = context.getFirstToken(node.attributes[node.attributes.length - 1]).loc.start;
}
return {
tag: tag,
opening: opening,
closing: closing,
lastProp: lastProp
};
}

return {
JSXOpeningElement: function(node) {
var tokens = getTokensLocations(node);
var expectedLocation = getExpectedLocation(tokens);
if (hasCorrectLocation(tokens, expectedLocation)) {
return;
}
context.report(node, MESSAGE, {
location: MESSAGE_LOCATION[expectedLocation]
});
}
};

};

module.exports.schema = [{
type: 'object',
properties: {
location: {
enum: ['after-props', 'props-aligned', 'tag-aligned']
}
}
}];

0 comments on commit 9948883

Please sign in to comment.