Skip to content

Commit

Permalink
New: jsx-curly-newline rule
Browse files Browse the repository at this point in the history
  • Loading branch information
golopot committed Apr 14, 2019
1 parent f0c0b4d commit 0ee5616
Show file tree
Hide file tree
Showing 4 changed files with 573 additions and 0 deletions.
125 changes: 125 additions & 0 deletions docs/rules/jsx-curly-newline.md
@@ -0,0 +1,125 @@
# 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 has a string option:

* `consistent` enforces either both of linebreaks around curlies are present, or none are present.
* `multiline` enforces that when the contained expression spans multiple line, require both linebreaks. When the contained expression is single line, disallow both line breaks.
* `multiline-lax` (default) enforces that when the contained expression spans multiple line, require both linebreaks. When the contained expression is single line, disallow inconsistent line break.

### consistent

When `consistent` is set, the following patterns are considered warnings:

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

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

The following patterns are **not** warnings:

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

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

### multiline

When `multiline` 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>
```

### multiline-lax

When `multiline-lax` (default) is set, the following patterns are considered warnings:

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

<div>
{ foo
}
</div>
```
The following patterns are **not** warnings:

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

<div>
{
foo
}
</div>

<div>
{ foo }
</div>

```

## When Not To Use It

You can turn this rule off if you are not concerned with the consistency around the linebreaks inside of JSX attributes or expressions.
1 change: 1 addition & 0 deletions index.js
Expand Up @@ -18,6 +18,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
182 changes: 182 additions & 0 deletions lib/rules/jsx-curly-newline.js
@@ -0,0 +1,182 @@
/**
* @fileoverview enforce consistent line breaks inside jsx curly
*/
'use strict';
const docsUrl = require('../util/docsUrl');

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

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: [
{
enum: ['consistent', 'multiline', 'multiline-lax']
}
],

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

create(context) {
const sourceCode = context.getSourceCode();
const rawOption = context.options[0] || 'multiline-lax';
const multilineOption = rawOption === 'multiline';
const multilineLaxOption = rawOption === 'multiline-lax';

// ----------------------------------------------------------------------
// 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 arguments or parameters in the list
* @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 expressionIsMultiline = expression.loc.start.line !== expression.loc.end.line;

if (multilineLaxOption && !expressionIsMultiline) {
return hasLeftNewline;
}
if (multilineOption || multilineLaxOption) {
return expressionIsMultiline;
}

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()
? // If there is a comment between the { and the first element, don't do a fix.
null
: 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()
? // If there is a comment between the last element and the }, don't do a fix.
null
: fixer.removeRange([
tokenBeforeRightCurly.range[1],
rightCurly.range[0]
]);
}
});
} else if (!hasRightNewline && needsNewlines) {
context.report({
node: rightCurly,
messageId: 'expectedBefore',
fix: fixer => fixer.insertTextBefore(rightCurly, '\n')
});
}
}

/**
* Gets the left curly and right curly tokens of a node.
* @param {ASTNode} node The JSXExpressionContainer node.
* @returns {{leftCurly: Object, rightCurly: Object}} An object contaning left and right curly tokens.
*/
function getCurlyTokens(node) {
return {
leftCurly: sourceCode.getFirstToken(node),
rightCurly: sourceCode.getLastToken(node)
};
}

/**
* Validates the curlys for a JSXExpressionContainer node.
* @param {ASTNode} node The JSXExpressionContainer node.
* @returns {void}
*/
function validateNode(node) {
validateCurlys(
getCurlyTokens(node),
node.expression
);
}

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

return {
JSXExpressionContainer: validateNode
};
}
};

0 comments on commit 0ee5616

Please sign in to comment.