diff --git a/CHANGELOG.md b/CHANGELOG.md
index 678e103483..27b1f966ba 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/docs/rules/jsx-curly-newline.md b/docs/rules/jsx-curly-newline.md
new file mode 100644
index 0000000000..ddac9caad7
--- /dev/null
+++ b/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
+
+ { foo
+ }
+
+
+
+ {
+ foo }
+
+
+
+ { foo &&
+ foo.bar
+ }
+
+```
+
+The following patterns are **not** warnings:
+
+```jsx
+
+ { foo }
+
+
+
+ {
+ foo
+ }
+
+```
+
+### never
+
+When `never` or `{ multiline: "forbid", singleline: "forbid" }` 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 }
+
+```
+
+## require
+
+When `{ multiline: "require", singleline: "require" }` 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
+ }
+
+```
+
+
+## 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.
diff --git a/index.js b/index.js
index 82a89f97fd..f628c55ff6 100644
--- a/index.js
+++ b/index.js
@@ -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'),
diff --git a/lib/rules/jsx-curly-newline.js b/lib/rules/jsx-curly-newline.js
new file mode 100644
index 0000000000..2c67f58e9d
--- /dev/null
+++ b/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);
+ }
+ };
+ }
+};
diff --git a/tests/lib/rules/jsx-curly-newline.js b/tests/lib/rules/jsx-curly-newline.js
new file mode 100644
index 0000000000..8b4779cc18
--- /dev/null
+++ b/tests/lib/rules/jsx-curly-newline.js
@@ -0,0 +1,298 @@
+/**
+ * @fileoverview enforce consistent line breaks inside jsx curly
+ */
+
+'use strict';
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const RuleTester = require('eslint').RuleTester;
+const rule = require('../../../lib/rules/jsx-curly-newline');
+
+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 CONSISTENT = ['consistent'];
+const NEVER = ['never'];
+const MULTILINE_REQUIRE = [{singleline: 'consistent', multiline: 'require'}];
+
+const ruleTester = new RuleTester({parserOptions});
+
+ruleTester.run('jsx-curly-newline', rule, {
+
+ valid: [
+
+ // consistent option (default)
+
+ {
+ code: '{foo}
',
+ options: ['consistent']
+ },
+
+ {
+ code: `
+
+ {
+ foo
+ }
+
`,
+ options: CONSISTENT
+ },
+
+ {
+ code: `
+
+ { foo &&
+ foo.bar }
+
`,
+ options: CONSISTENT
+ },
+
+ {
+ code: `
+
+ {
+ foo &&
+ foo.bar
+ }
+
`,
+ options: CONSISTENT
+ },
+
+ {
+ code: `
+ `,
+ options: CONSISTENT
+ },
+
+
+ // {singleline: 'consistent', multiline: 'require'} option
+
+ {
+ code: '{foo}
',
+ options: MULTILINE_REQUIRE
+ },
+ {
+ code: '',
+ options: MULTILINE_REQUIRE
+ },
+ {
+ code: `
+
+ {
+ foo &&
+ foo.bar
+ }
+
`,
+ options: MULTILINE_REQUIRE
+ },
+ {
+ code: `
+
+ {
+ foo
+ }
+
`,
+ options: MULTILINE_REQUIRE
+ },
+
+ // never option
+
+ {
+ code: '{foo}
',
+ options: NEVER
+ },
+
+ {
+ code: '',
+ options: NEVER
+ },
+
+ {
+ code: `
+
+ { foo &&
+ foo.bar }
+
`,
+
+ options: NEVER
+ }
+ ],
+
+ invalid: [
+
+ // conistent option (default)
+
+ {
+ code: `
+
+ { foo \n}
+
`,
+ output: `
+
+ { foo}
+
`,
+ options: CONSISTENT,
+ errors: [RIGHT_UNEXPECTED_ERROR]
+ },
+
+ {
+ 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]
+ },
+
+ // {singleline: 'consistent', multiline: 'require'} option
+ {
+ code: '{foo\n}
',
+ output: '{foo}
',
+ errors: [RIGHT_UNEXPECTED_ERROR],
+ options: MULTILINE_REQUIRE
+ },
+ {
+ code: '{\nfoo}
',
+ output: '{\nfoo\n}
',
+ errors: [RIGHT_MISSING_ERROR],
+ options: MULTILINE_REQUIRE
+ },
+ {
+ code: `
+
+ { foo &&
+ bar }
+
`,
+ output: `
+
+ {\n foo &&
+ bar \n}
+
`,
+ errors: [LEFT_MISSING_ERROR, RIGHT_MISSING_ERROR],
+ options: MULTILINE_REQUIRE
+ },
+ {
+ code: `
+ `,
+ output: `
+ `,
+ errors: [LEFT_MISSING_ERROR],
+ options: MULTILINE_REQUIRE
+ },
+
+ // never options
+
+ {
+ code: `
+
+ {\nfoo\n}
+
`,
+ output: `
+
+ {foo}
+
`,
+ options: NEVER,
+ errors: [LEFT_UNEXPECTED_ERROR, RIGHT_UNEXPECTED_ERROR]
+ },
+
+ {
+ code: `
+
+ {
+ foo &&
+ foo.bar
+ }
+
`,
+ output: `
+
+ {foo &&
+ foo.bar}
+
`,
+ options: NEVER,
+ errors: [LEFT_UNEXPECTED_ERROR, RIGHT_UNEXPECTED_ERROR]
+ },
+
+ {
+ code: `
+
+ { foo &&
+ foo.bar
+ }
+
`,
+ output: `
+
+ { foo &&
+ foo.bar}
+
`,
+ options: NEVER,
+ errors: [RIGHT_UNEXPECTED_ERROR]
+ },
+
+ {
+ code: `
+
+ { /* not fixed due to comment */
+ foo }
+
`,
+ output: null,
+ options: NEVER,
+ errors: [LEFT_UNEXPECTED_ERROR]
+ },
+
+ {
+ code: `
+
+ { foo
+ /* not fixed due to comment */}
+
`,
+ output: null,
+ options: NEVER,
+ errors: [RIGHT_UNEXPECTED_ERROR]
+ }
+ ]
+});