Skip to content

Commit

Permalink
Merge pull request #218 from evocateur/jsx-closing-bracket-split
Browse files Browse the repository at this point in the history
Add option to jsx-closing-bracket-location to configure different styles for self-closing and non-empty tags (fixes #208)
  • Loading branch information
yannickcr committed Sep 16, 2015
2 parents 75f8a27 + d6743ee commit 67a6eea
Show file tree
Hide file tree
Showing 3 changed files with 242 additions and 17 deletions.
75 changes: 65 additions & 10 deletions docs/rules/jsx-closing-bracket-location.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,22 @@ The following patterns are not considered warnings:

## Rule Options

There are two ways to configure this rule.

The first form is a string shortcut corresponding to the `location` values specified below. If omitted, it defaults to `"tag-aligned"`.

```js
"jsx-closing-bracket-location": <enabled> // -> [<enabled>, "tag-aligned"]
"jsx-closing-bracket-location": [<enabled>, "<location>"]
```

The second form allows you to distinguish between non-empty and self-closing tags. Both properties are optional, and both default to `"tag-aligned"`.

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

### `location`
Expand All @@ -46,48 +58,91 @@ Enforced location for the closing bracket.
* `after-props`: must be placed right after the last prop.
* `props-aligned`: must be aligned with the last prop.

Default to `tag-aligned`.
Defaults to `tag-aligned`.

For backward compatibility, you may pass an object `{ "location": <location> }` that is equivalent to the first string shortcut form.

The following patterns are considered warnings:

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

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

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

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

// 'jsx-closing-bracket-location': [1, 'props-aligned']
<Hello
firstName="John"
lastName="Smith" />;

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

The following patterns are not considered warnings:

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

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

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

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

// 'jsx-closing-bracket-location': [1, {selfClosing: 'props-aligned', nonEmpty: 'after-props'}]
<Hello
firstName="John"
lastName="Smith"
/>;

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

## When not to use
Expand Down
61 changes: 54 additions & 7 deletions lib/rules/jsx-closing-bracket-location.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,33 @@ module.exports = function(context) {
'props-aligned': 'aligned with the last prop',
'tag-aligned': 'aligned with the opening tag'
};
var DEFAULT_LOCATION = 'tag-aligned';

var config = context.options[0];
var options = {
nonEmpty: DEFAULT_LOCATION,
selfClosing: DEFAULT_LOCATION
};

if (typeof config === 'string') {
// simple shorthand [1, 'something']
options.nonEmpty = config;
options.selfClosing = config;
} else if (typeof config === 'object') {
// [1, {location: 'something'}] (back-compat)
if (config.hasOwnProperty('location') && typeof config.location === 'string') {
options.nonEmpty = config.location;
options.selfClosing = config.location;
}
// [1, {nonEmpty: 'something'}]
if (config.hasOwnProperty('nonEmpty') && typeof config.nonEmpty === 'string') {
options.nonEmpty = config.nonEmpty;
}
// [1, {selfClosing: 'something'}]
if (config.hasOwnProperty('selfClosing') && typeof config.selfClosing === 'string') {
options.selfClosing = config.selfClosing;
}
}

/**
* Get expected location for the closing bracket
Expand All @@ -30,9 +57,9 @@ module.exports = function(context) {
// 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 use configuration dependent on selfClosing property
} else {
location = context.options[0] && context.options[0].location || 'tag-aligned';
location = tokens.selfClosing ? options.selfClosing : options.nonEmpty;
}
return location;
}
Expand Down Expand Up @@ -79,7 +106,8 @@ module.exports = function(context) {
tag: tag,
opening: opening,
closing: closing,
lastProp: lastProp
lastProp: lastProp,
selfClosing: node.selfClosing
};
}

Expand All @@ -99,10 +127,29 @@ module.exports = function(context) {
};

module.exports.schema = [{
type: 'object',
properties: {
location: {
oneOf: [
{
enum: ['after-props', 'props-aligned', 'tag-aligned']
},
{
type: 'object',
properties: {
location: {
enum: ['after-props', 'props-aligned', 'tag-aligned']
}
},
additionalProperties: false
}, {
type: 'object',
properties: {
nonEmpty: {
enum: ['after-props', 'props-aligned', 'tag-aligned']
},
selfClosing: {
enum: ['after-props', 'props-aligned', 'tag-aligned']
}
},
additionalProperties: false
}
}
]
}];
123 changes: 123 additions & 0 deletions tests/lib/rules/jsx-closing-bracket-location.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ ruleTester.run('jsx-closing-bracket-location', rule, {
'<App foo />'
].join('\n'),
ecmaFeatures: {jsx: true}
}, {
code: [
'<App ',
' foo',
'/>'
].join('\n'),
ecmaFeatures: {jsx: true}
}, {
code: [
'<App foo />'
Expand All @@ -44,6 +51,21 @@ ruleTester.run('jsx-closing-bracket-location', rule, {
].join('\n'),
options: [{location: 'tag-aligned'}],
ecmaFeatures: {jsx: true}
}, {
code: [
'<App ',
' foo />'
].join('\n'),
options: ['after-props'],
ecmaFeatures: {jsx: true}
}, {
code: [
'<App ',
' foo',
' />'
].join('\n'),
options: ['props-aligned'],
ecmaFeatures: {jsx: true}
}, {
code: [
'<App ',
Expand Down Expand Up @@ -117,6 +139,59 @@ ruleTester.run('jsx-closing-bracket-location', rule, {
].join('\n'),
options: [{location: 'tag-aligned'}],
ecmaFeatures: {jsx: true}
}, {
code: [
'<Provider store>',
' <App',
' foo />',
'</Provider>'
].join('\n'),
options: [{selfClosing: 'after-props'}],
ecmaFeatures: {jsx: true}
}, {
code: [
'<Provider ',
' store',
'>',
' <App',
' foo />',
'</Provider>'
].join('\n'),
options: [{selfClosing: 'after-props'}],
ecmaFeatures: {jsx: true}
}, {
code: [
'<Provider ',
' store>',
' <App ',
' foo',
' />',
'</Provider>'
].join('\n'),
options: [{nonEmpty: 'after-props'}],
ecmaFeatures: {jsx: true}
}, {
code: [
'<Provider store>',
' <App ',
' foo',
' />',
'</Provider>'
].join('\n'),
options: [{selfClosing: 'props-aligned'}],
ecmaFeatures: {jsx: true}
}, {
code: [
'<Provider',
' store',
' >',
' <App ',
' foo',
' />',
'</Provider>'
].join('\n'),
options: [{nonEmpty: 'props-aligned'}],
ecmaFeatures: {jsx: true}
}],

invalid: [{
Expand Down Expand Up @@ -228,5 +303,53 @@ ruleTester.run('jsx-closing-bracket-location', rule, {
options: [{location: 'tag-aligned'}],
ecmaFeatures: {jsx: true},
errors: MESSAGE_TAG_ALIGNED
}, {
code: [
'<Provider ',
' store>', // <--
' <App ',
' foo',
' />',
'</Provider>'
].join('\n'),
options: [{selfClosing: 'props-aligned'}],
ecmaFeatures: {jsx: true},
errors: MESSAGE_TAG_ALIGNED
}, {
code: [
'<Provider',
' store',
' >',
' <App ',
' foo',
' />', // <--
'</Provider>'
].join('\n'),
options: [{nonEmpty: 'props-aligned'}],
ecmaFeatures: {jsx: true},
errors: MESSAGE_TAG_ALIGNED
}, {
code: [
'<Provider ',
' store>', // <--
' <App',
' foo />',
'</Provider>'
].join('\n'),
options: [{selfClosing: 'after-props'}],
ecmaFeatures: {jsx: true},
errors: MESSAGE_TAG_ALIGNED
}, {
code: [
'<Provider ',
' store>',
' <App ',
' foo',
' />', // <--
'</Provider>'
].join('\n'),
options: [{nonEmpty: 'after-props'}],
ecmaFeatures: {jsx: true},
errors: MESSAGE_TAG_ALIGNED
}]
});

0 comments on commit 67a6eea

Please sign in to comment.