diff --git a/docs/rules/jsx-curly-newline.md b/docs/rules/jsx-curly-newline.md
new file mode 100644
index 0000000000..3c174b7815
--- /dev/null
+++ b/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
+
+ { foo
+ }
+
+
+
+ {
+ foo }
+
+```
+
+The following patterns are **not** warnings:
+
+```jsx
+
+ { foo }
+
+
+
+ {
+ foo
+ }
+
+```
+
+### multiline
+
+When `multiline` is set, the following patterns are considered warnings:
+
+```jsx
+
+ { foo &&
+ foo.bar }
+
+
+
+ {
+ foo
+ }
+
+
+
+ { foo
+ }
+
+```
+
+The following patterns are **not** warnings:
+
+```jsx
+
+ {
+ foo &&
+ foo.bar
+ }
+
+
+
+ { foo }
+
+```
+
+### multiline-lax
+
+When `multiline-lax` (default) is set, the following patterns are considered warnings:
+
+```jsx
+
+ { foo &&
+ foo.bar }
+
+
+
+ { foo
+ }
+
+```
+The following patterns are **not** warnings:
+
+```jsx
+
+ {
+ foo &&
+ foo.bar
+ }
+
+
+
+ {
+ foo
+ }
+
+
+
+ { foo }
+
+
+```
+
+## 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.
diff --git a/index.js b/index.js
index b2d4ccdb90..de964f8251 100644
--- a/index.js
+++ b/index.js
@@ -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'),
diff --git a/lib/rules/jsx-curly-newline.js b/lib/rules/jsx-curly-newline.js
new file mode 100644
index 0000000000..f0d8ae1cea
--- /dev/null
+++ b/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
+ };
+ }
+};
diff --git a/tests/lib/rules/jsx-curly-newline.js b/tests/lib/rules/jsx-curly-newline.js
new file mode 100644
index 0000000000..546ce98df6
--- /dev/null
+++ b/tests/lib/rules/jsx-curly-newline.js
@@ -0,0 +1,265 @@
+/**
+ * @fileoverview enforce consistent line breaks inside jsx curly
+ */
+'use strict';
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const rule = require('../../../lib/rules/jsx-curly-newline');
+const RuleTester = require('eslint').RuleTester;
+
+const parserOptions = {
+ ecmaVersion: 2018,
+ sourceType: 'module',
+ ecmaFeatures: {
+ jsx: true
+ }
+};
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const LEFT_MISSING_ERROR = {messageId: 'expectedAfter', type: 'Punctuator'};
+const LEFT_UNEXPECTED_ERROR = {messageId: 'unexpectedAfter', type: 'Punctuator'};
+const RIGHT_MISSING_ERROR = {messageId: 'expectedBefore', type: 'Punctuator'};
+const RIGHT_UNEXPECTED_ERROR = {messageId: 'unexpectedBefore', type: 'Punctuator'};
+// const EXPECTED_BETWEEN = {messageId: 'expectedBetween', type: 'Identifier'};
+
+const ruleTester = new RuleTester({parserOptions});
+
+ruleTester.run('jsx-curly-newline', rule, {
+
+ valid: [
+
+ // multiline-lax option (default)
+
+ '{foo}
',
+ '',
+ `
+
+ {
+ foo &&
+ foo.bar
+ }
+
`,
+ `
+
+ {
+ foo
+ }
+
`,
+ `
+ `,
+
+ // multiline option
+
+ {
+ code: '{foo}
',
+ options: ['multiline']
+ },
+
+ {
+ code: '',
+ options: ['multiline']
+ },
+
+ {
+ code: `
+
+ {
+ foo &&
+ foo.bar
+ }
+
`,
+
+ options: ['multiline']
+ },
+
+ // consistent option
+
+ {
+ code: '{foo}
',
+ options: ['consistent'],
+ parser: 'babel-eslint'
+ },
+
+ {
+ code: `
+
+ {
+ foo
+ }
+
`,
+ options: ['consistent'],
+ parser: 'babel-eslint'
+ },
+
+ {
+ code: `
+
+ { foo &&
+ foo.bar }
+
`,
+ options: ['consistent'],
+ parser: 'babel-eslint'
+ },
+
+ {
+ code: `
+
+ {
+ foo &&
+ foo.bar
+ }
+
`,
+ options: ['consistent'],
+ parser: 'babel-eslint'
+ }
+ ],
+
+ invalid: [
+
+ // multiline-lax option (default)
+ {
+ code: '{foo\n}
',
+ output: '{foo}
',
+ errors: [RIGHT_UNEXPECTED_ERROR]
+ },
+ {
+ code: '{\nfoo}
',
+ output: '{\nfoo\n}
',
+ errors: [RIGHT_MISSING_ERROR]
+ },
+ {
+ code: `
+
+ { foo &&
+ bar }
+
`,
+ output: `
+
+ {\n foo &&
+ bar \n}
+
`,
+ errors: [LEFT_MISSING_ERROR, RIGHT_MISSING_ERROR]
+ },
+
+ {
+ code: `
+ `,
+ output: `
+ `,
+ errors: [LEFT_MISSING_ERROR]
+ },
+
+ // multiline options
+
+ {
+ code: `
+
+ {\nfoo\n}
+
`,
+ output: `
+
+ {foo}
+
`,
+ options: ['multiline'],
+ errors: [LEFT_UNEXPECTED_ERROR, RIGHT_UNEXPECTED_ERROR]
+ },
+
+ {
+ code: `
+
+ { foo &&
+ foo.bar }
+
`,
+ output: `
+
+ {\n foo &&
+ foo.bar \n}
+
`,
+ options: ['multiline'],
+ errors: [LEFT_MISSING_ERROR, RIGHT_MISSING_ERROR]
+ },
+
+ {
+ code: `
+
+ { foo &&
+ foo.bar
+ }
+
`,
+ output: `
+
+ {\n foo &&
+ foo.bar
+ }
+
`,
+ options: ['multiline'],
+ errors: [LEFT_MISSING_ERROR]
+ },
+
+ {
+ code: `
+
+ { /* not fixed due to comment */
+ foo }
+
`,
+ output: null,
+ options: ['multiline'],
+ errors: [LEFT_UNEXPECTED_ERROR]
+ },
+
+ {
+ code: `
+
+ { foo
+ /* not fixed due to comment */}
+
`,
+ output: null,
+ options: ['multiline'],
+ errors: [RIGHT_UNEXPECTED_ERROR]
+ },
+
+ // conistent option
+
+ {
+ code: `
+
+ { foo &&
+ foo.bar \n}
+
`,
+ output: `
+
+ { foo &&
+ foo.bar}
+
`,
+ options: ['consistent'],
+ errors: [RIGHT_UNEXPECTED_ERROR]
+ },
+ {
+ code: `
+
+ { foo &&
+ bar
+ }
+
`,
+ output: `
+
+ { foo &&
+ bar}
+
`,
+ options: ['consistent'],
+ errors: [RIGHT_UNEXPECTED_ERROR]
+ }
+ ]
+});