Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New: jsx-curly-newline rule #2240

Merged
merged 1 commit into from Jun 5, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -2576,3 +2576,4 @@ If you're still not using React 15 you can keep the old behavior by setting the
[`state-in-constructor`]: docs/rules/state-in-constructor.md
[`jsx-props-no-spreading`]: docs/rules/jsx-props-no-spreading.md
[`static-property-placement`]: docs/rules/static-property-placement.md
[`jsx-curly-newline`]: docs/rules/jsx-curly-newline.md
149 changes: 149 additions & 0 deletions docs/rules/jsx-curly-newline.md
@@ -0,0 +1,149 @@
# Enforce linebreaks in curly braces in JSX attributes and expressions. (react/jsx-curly-newline)

Many style guides require or disallow newlines inside of jsx curly expressions.

**Fixable:** This rule is automatically fixable using the `--fix` flag on the command line.

## Rule Details

This rule enforces consistent linebreaks inside of curlies of jsx curly expressions.

## Rule Options

This rule accepts either an object option:

```ts
{
multiline: "consistent" | "forbid" | "require", // default to 'consistent'
singleline: "consistent" | "forbid" | "require", // default to 'consistent'
}
```
Option `multiline` takes effect when the jsx expression inside the curlies occupies multiple lines.

Option `singleline` takes effect when the jsx expression inside the curlies occupies a single line.

* `consistent` enforces either both curly braces have a line break directly inside them, or no line breaks are present.
* `forbid` disallows linebreaks directly inside curly braces.
* `require` enforces the presence of linebreaks directly inside curlies.

or a string option:

* `consistent` (default) is an alias for `{ multiline: "consistent", singleline: "consistent" }`.
* `never` is an alias for `{ multiline: "forbid", singleline: "forbid" }`

or an

### consistent (default)

When `consistent` or `{ multiline: "consistent", singleline: "consistent" }` is set, the following patterns are considered warnings:

```jsx
<div>
{ foo
}
</div>

<div>
{
foo }
</div>

<div>
{ foo &&
foo.bar
}
</div>
```

The following patterns are **not** warnings:

```jsx
<div>
{ foo }
</div>

<div>
{
foo
}
</div>
```

### never

When `never` or `{ multiline: "forbid", singleline: "forbid" }` is set, the following patterns are considered warnings:

```jsx
<div>
{
foo &&
foo.bar
}
</div>

<div>
{
foo
}
</div>

<div>
{ foo
}
</div>
```

The following patterns are **not** warnings:

```jsx
<div>
{ foo &&
foo.bar }
</div>

<div>
{ foo }
</div>
```

## require

When `{ multiline: "require", singleline: "require" }` is set, the following patterns are considered warnings:

```jsx
<div>
{ foo &&
foo.bar }
</div>

<div>
{ foo }
</div>

<div>
{ foo
}
</div>
```

The following patterns are **not** warnings:

```jsx
<div>
{
foo &&
foo.bar
}
</div>

<div>
{
foo
}
</div>
```


## When Not To Use It

You can turn this rule off if you are not concerned with the consistency of padding linebreaks inside of JSX attributes or expressions.
1 change: 1 addition & 0 deletions index.js
Expand Up @@ -19,6 +19,7 @@ const allRules = {
'jsx-closing-bracket-location': require('./lib/rules/jsx-closing-bracket-location'),
'jsx-closing-tag-location': require('./lib/rules/jsx-closing-tag-location'),
'jsx-curly-spacing': require('./lib/rules/jsx-curly-spacing'),
'jsx-curly-newline': require('./lib/rules/jsx-curly-newline'),
'jsx-equals-spacing': require('./lib/rules/jsx-equals-spacing'),
'jsx-filename-extension': require('./lib/rules/jsx-filename-extension'),
'jsx-first-prop-new-line': require('./lib/rules/jsx-first-prop-new-line'),
Expand Down
187 changes: 187 additions & 0 deletions lib/rules/jsx-curly-newline.js
@@ -0,0 +1,187 @@
/**
* @fileoverview enforce consistent line breaks inside jsx curly
*/

'use strict';

const docsUrl = require('../util/docsUrl');

// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------

function getNormalizedOption(context) {
const rawOption = context.options[0] || 'consistent';

if (rawOption === 'consistent') {
return {
multiline: 'consistent',
singleline: 'consistent'
};
}

if (rawOption === 'never') {
return {
multiline: 'forbid',
singleline: 'forbid'
};
}

return {
multiline: rawOption.multiline || 'consistent',
singleline: rawOption.singleline || 'consistent'
};
}

module.exports = {
meta: {
type: 'layout',

docs: {
description: 'enforce consistent line breaks inside jsx curly',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('jsx-curly-newline')
},

fixable: 'whitespace',

schema: [
{
oneOf: [
{
enum: ['consistent', 'never']
},
{
type: 'object',
properties: {
singleline: {enum: ['consistent', 'require', 'forbid']},
multiline: {enum: ['consistent', 'require', 'forbid']}
},
additionalProperties: false
}
]
}
],


messages: {
expectedBefore: 'Expected newline before \'}\'.',
expectedAfter: 'Expected newline after \'{\'.',
unexpectedBefore: 'Unexpected newline before \'{\'.',
unexpectedAfter: 'Unexpected newline after \'}\'.'
}
},

create(context) {
const sourceCode = context.getSourceCode();
const option = getNormalizedOption(context);

// ----------------------------------------------------------------------
// Helpers
// ----------------------------------------------------------------------

/**
* Determines whether two adjacent tokens are on the same line.
* @param {Object} left - The left token object.
* @param {Object} right - The right token object.
* @returns {boolean} Whether or not the tokens are on the same line.
*/
function isTokenOnSameLine(left, right) {
return left.loc.end.line === right.loc.start.line;
}

/**
* Determines whether there should be newlines inside curlys
* @param {ASTNode} expression The expression contained in the curlys
* @param {boolean} hasLeftNewline `true` if the left curly has a newline in the current code.
* @returns {boolean} `true` if there should be newlines inside the function curlys
*/
function shouldHaveNewlines(expression, hasLeftNewline) {
const isMultiline = expression.loc.start.line !== expression.loc.end.line;

switch (isMultiline ? option.multiline : option.singleline) {
case 'forbid': return false;
case 'require': return true;
case 'consistent':
default: return hasLeftNewline;
}
}

/**
* Validates curlys
* @param {Object} curlys An object with keys `leftParen` for the left paren token, and `rightParen` for the right paren token
* @param {ASTNode} expression The expression inside the curly
* @returns {void}
*/
function validateCurlys(curlys, expression) {
const leftCurly = curlys.leftCurly;
const rightCurly = curlys.rightCurly;
const tokenAfterLeftCurly = sourceCode.getTokenAfter(leftCurly);
const tokenBeforeRightCurly = sourceCode.getTokenBefore(rightCurly);
const hasLeftNewline = !isTokenOnSameLine(leftCurly, tokenAfterLeftCurly);
const hasRightNewline = !isTokenOnSameLine(tokenBeforeRightCurly, rightCurly);
const needsNewlines = shouldHaveNewlines(expression, hasLeftNewline);

if (hasLeftNewline && !needsNewlines) {
context.report({
node: leftCurly,
messageId: 'unexpectedAfter',
fix(fixer) {
return sourceCode
.getText()
.slice(leftCurly.range[1], tokenAfterLeftCurly.range[0])
.trim() ?
null : // If there is a comment between the { and the first element, don't do a fix.
fixer.removeRange([leftCurly.range[1], tokenAfterLeftCurly.range[0]]);
}
});
} else if (!hasLeftNewline && needsNewlines) {
context.report({
node: leftCurly,
messageId: 'expectedAfter',
fix: fixer => fixer.insertTextAfter(leftCurly, '\n')
});
}

if (hasRightNewline && !needsNewlines) {
context.report({
node: rightCurly,
messageId: 'unexpectedBefore',
fix(fixer) {
return sourceCode
.getText()
.slice(tokenBeforeRightCurly.range[1], rightCurly.range[0])
.trim() ?
null : // If there is a comment between the last element and the }, don't do a fix.
fixer.removeRange([
tokenBeforeRightCurly.range[1],
rightCurly.range[0]
]);
}
});
} else if (!hasRightNewline && needsNewlines) {
context.report({
node: rightCurly,
messageId: 'expectedBefore',
fix: fixer => fixer.insertTextBefore(rightCurly, '\n')
});
}
}


// ----------------------------------------------------------------------
// Public
// ----------------------------------------------------------------------

return {
JSXExpressionContainer(node) {
const curlyTokens = {
leftCurly: sourceCode.getFirstToken(node),
rightCurly: sourceCode.getLastToken(node)
};
validateCurlys(curlyTokens, node.expression);
}
};
}
};