Repository: eslint/eslint (js)

Issue:
`object-curly-newline`: new option request
I'm on eslint v2.13.1.

The `multiline: true` option is excellent: it lets me require that when newlines exist, every property must be on its own line.

Combining it with `minProperties` is half useful, however: when it's set to, say, 3, that means that any objects with 3 or more properties _must_ be split onto multiple lines, which is great! Unfortunately, this also means that objects with less than 3 properties _must not_ be split onto multiple lines.

What I'd like instead is something like `{ multiline: true, maxPropertiesBeforeForcedNewline: 2 }` which says that if there are `> 2` properties, the object _must_ be multiline, but if there are `<= 2`, the object may be single or multi, provided that it's consistent with the `multiline: true` option.


Pull request description:
Update: Add `consistent` option to `object-curly-newline` (fixes #6488)
**What is the purpose of this pull request? (put an "X" next to item)**

[ ] Documentation update
[ ] Bug fix ([template](https://raw.githubusercontent.com/eslint/eslint/master/templates/bug-report.md))
[ ] New rule ([template](https://raw.githubusercontent.com/eslint/eslint/master/templates/rule-proposal.md))
[x] Changes an existing rule ([template](https://raw.githubusercontent.com/eslint/eslint/master/templates/rule-change-proposal.md))
[ ] Add autofixing to a rule
[ ] Add a CLI option
[ ] Add something to the core
[ ] Other, please explain:

See https://github.com/eslint/eslint/issues/6488.

**What rule do you want to change?**

`object-curly-newline`.

**Does this change cause the rule to produce more or fewer warnings?**

Fewer.

**How will the change be implemented? (New option, new default behavior, etc.)?**

New option `consistent`.

**Please provide some example code that this change will affect:**

```js
var one = { a: 1 };

var two = { a: 1, b: 2 };

var three = { a: 1, b: 2, c: 3 };

var oneM = {
  a: 1
};

var twoM = {
  a: 1,
  b: 2
};

var threeM = {
  a: 1,
  b: 2,
  c: 3
};

var wat = { a: 1
};

var wat2 = {
a: 1 };

var wat3 = { a: 1,
  b: 2,
  c: 3 };
```

**What does the rule currently do for this code?**

```shell
   5:13  error  Expected a line break after this opening brace   object-curly-newline
   5:32  error  Expected a line break before this closing brace  object-curly-newline
   7:12  error  Unexpected line break after this opening brace   object-curly-newline
   9:1   error  Unexpected line break before this closing brace  object-curly-newline
  23:1   error  Unexpected line break before this closing brace  object-curly-newline
  25:12  error  Unexpected line break after this opening brace   object-curly-newline
  28:12  error  Expected a line break after this opening brace   object-curly-newline
  30:8   error  Expected a line break before this closing brace  object-curly-newline

✖ 8 problems (8 errors, 0 warnings)
```

**What will the rule do after it's changed?**

```shell
   5:13  error  Expected a line break after this opening brace   object-curly-newline
   5:32  error  Expected a line break before this closing brace  object-curly-newline
  23:1   error  Unexpected line break before this closing brace  object-curly-newline
  25:12  error  Unexpected line break after this opening brace   object-curly-newline
  28:12  error  Expected a line break after this opening brace   object-curly-newline
  30:8   error  Expected a line break before this closing brace  object-curly-newline
```

Patch:
diff --git a/docs/rules/object-curly-newline.md b/docs/rules/object-curly-newline.md
index a548f4eac..40f5368e5 100644
--- a/docs/rules/object-curly-newline.md
+++ b/docs/rules/object-curly-newline.md
@@ -16,7 +16,8 @@ This rule has either a string option:
 Or an object option:
 
 * `"multiline": true` (default) requires line breaks if there are line breaks inside properties or between properties
-* `"minProperties"` requires line breaks if the number of properties is more than the given integer
+* `"minProperties"` requires line breaks if the number of properties is more than the given integer. By default, an error will also be reported if an object contains linebreaks and has fewer properties than the given integer. However, the second behavior is disabled if the `consistent` option is set to `true`
+* `"consistent": true` requires that either both curly braces, or neither, directly enclose newlines. Note that enabling this option will also change the behavior of the `minProperties` option. (See `minProperties` above for more information)
 
 You can specify different options for object literals and destructuring assignments:
 
@@ -312,6 +313,86 @@ let {k = function() {
 }} = obj;
 ```
 
+### consistent
+
+Examples of **incorrect** code for this rule with the `{ "consistent": true }` option:
+
+```js
+/*eslint object-curly-newline: ["error", { "consistent": true }]*/
+/*eslint-env es6*/
+
+let a = {foo: 1
+};
+let b = {
+    foo: 1};
+let c = {foo: 1, bar: 2
+};
+let d = {
+    foo: 1, bar: 2};
+let e = {foo: 1,
+    bar: 2};
+let f = {foo: function() {
+    dosomething();
+}};
+
+let {g
+} = obj;
+let {
+    h} = obj;
+let {i, j
+} = obj;
+let {
+    k, l} = obj;
+let {m,
+    n} = obj;
+let {o = function() {
+    dosomething();
+}} = obj;
+```
+
+Examples of **correct** code for this rule with the `{ "consistent": true }` option:
+
+```js
+/*eslint object-curly-newline: ["error", { "consistent": true }]*/
+/*eslint-env es6*/
+
+let a = {};
+let b = {foo: 1};
+let c = {
+    foo: 1
+};
+let d = {
+    foo: 1, bar: 2
+};
+let e = {
+    foo: 1,
+    bar: 2
+};
+let f = {foo: function() {dosomething();}};
+let g = {
+    foo: function() {
+        dosomething();
+    }
+};
+
+let {} = obj;
+let {h} = obj;
+let {i, j} = obj;
+let {
+    k, l
+} = obj;
+let {
+    m,
+    n
+} = obj;
+let {o = function() {dosomething();}} = obj;
+let {
+    p = function() {
+        dosomething();
+    }
+} = obj;
+```
+
 ### ObjectExpression and ObjectPattern
 
 Examples of **incorrect** code for this rule with the `{ "ObjectExpression": "always", "ObjectPattern": "never" }` options:
diff --git a/lib/rules/object-curly-newline.js b/lib/rules/object-curly-newline.js
index a4451154d..b78cb9cfc 100644
--- a/lib/rules/object-curly-newline.js
+++ b/lib/rules/object-curly-newline.js
@@ -30,6 +30,9 @@ const OPTION_VALUE = {
                 minProperties: {
                     type: "integer",
                     minimum: 0
+                },
+                consistent: {
+                    type: "boolean"
                 }
             },
             additionalProperties: false,
@@ -42,11 +45,12 @@ const OPTION_VALUE = {
  * Normalizes a given option value.
  *
  * @param {string|Object|undefined} value - An option value to parse.
- * @returns {{multiline: boolean, minProperties: number}} Normalized option object.
+ * @returns {{multiline: boolean, minProperties: number, consistent: boolean}} Normalized option object.
  */
 function normalizeOptionValue(value) {
     let multiline = false;
     let minProperties = Number.POSITIVE_INFINITY;
+    let consistent = false;
 
     if (value) {
         if (value === "always") {
@@ -56,12 +60,13 @@ function normalizeOptionValue(value) {
         } else {
             multiline = Boolean(value.multiline);
             minProperties = value.minProperties || Number.POSITIVE_INFINITY;
+            consistent = Boolean(value.consistent);
         }
     } else {
         multiline = true;
     }
 
-    return { multiline, minProperties };
+    return { multiline, minProperties, consistent };
 }
 
 /**
@@ -172,7 +177,14 @@ module.exports = {
                     });
                 }
             } else {
-                if (!astUtils.isTokenOnSameLine(openBrace, first)) {
+                const consistent = options.consistent;
+                const hasLineBreakBetweenOpenBraceAndFirst = !astUtils.isTokenOnSameLine(openBrace, first);
+                const hasLineBreakBetweenCloseBraceAndLast = !astUtils.isTokenOnSameLine(last, closeBrace);
+
+                if (
+                    (!consistent && hasLineBreakBetweenOpenBraceAndFirst) ||
+                    (consistent && hasLineBreakBetweenOpenBraceAndFirst && !hasLineBreakBetweenCloseBraceAndLast)
+                ) {
                     context.report({
                         message: "Unexpected line break after this opening brace.",
                         node,
@@ -185,7 +197,10 @@ module.exports = {
                         }
                     });
                 }
-                if (!astUtils.isTokenOnSameLine(last, closeBrace)) {
+                if (
+                    (!consistent && hasLineBreakBetweenCloseBraceAndLast) ||
+                    (consistent && !hasLineBreakBetweenOpenBraceAndFirst && hasLineBreakBetweenCloseBraceAndLast)
+                ) {
                     context.report({
                         message: "Unexpected line break before this closing brace.",
                         node,


Test patch:
diff --git a/tests/lib/rules/object-curly-newline.js b/tests/lib/rules/object-curly-newline.js
index 4d8859742..05376e0d4 100644
--- a/tests/lib/rules/object-curly-newline.js
+++ b/tests/lib/rules/object-curly-newline.js
@@ -236,6 +236,162 @@ ruleTester.run("object-curly-newline", rule, {
             options: [{ multiline: true, minProperties: 2 }]
         },
 
+        // "consistent" ------------------------------------------
+        {
+            code: [
+                "var b = {",
+                "    a: 1",
+                "};"
+            ].join("\n"),
+            options: [{ multiline: true, consistent: true }]
+        },
+        {
+            code: [
+                "var c = {a: 1, b: 2};"
+            ].join("\n"),
+            options: [{ multiline: true, consistent: true }]
+        },
+        {
+            code: [
+                "var c = {",
+                "    a: 1,",
+                "    b: 2",
+                "};"
+            ].join("\n"),
+
+            options: [{ multiline: true, consistent: true }]
+        },
+        {
+            code: [
+                "var e = {a: function() { dosomething();}};"
+            ].join("\n"),
+            options: [{ multiline: true, consistent: true }]
+        },
+        {
+            code: [
+                "var e = {",
+                "    a: function() { dosomething();}",
+                "};"
+            ].join("\n"),
+            options: [{ multiline: true, consistent: true }]
+        },
+        {
+            code: [
+                "let {} = {a: 1};"
+            ].join("\n"),
+            options: [{ multiline: true, consistent: true }],
+            parserOptions: { ecmaVersion: 6 }
+        },
+        {
+            code: [
+                "let {a} = {a: 1};"
+            ].join("\n"),
+            options: [{ multiline: true, consistent: true }],
+            parserOptions: { ecmaVersion: 6 }
+        },
+        {
+            code: [
+                "let {",
+                "} = {a: 1};"
+            ].join("\n"),
+            options: [{ multiline: true, consistent: true }],
+            parserOptions: { ecmaVersion: 6 }
+        },
+        {
+            code: [
+                "let {",
+                "    a",
+                "} = {a: 1};"
+            ].join("\n"),
+            options: [{ multiline: true, consistent: true }],
+            parserOptions: { ecmaVersion: 6 }
+        },
+        {
+            code: [
+                "let {a, b} = {a: 1, b: 1};"
+            ].join("\n"),
+            options: [{ multiline: true, consistent: true }],
+            parserOptions: { ecmaVersion: 6 }
+        },
+        {
+            code: [
+                "let {",
+                "    a, b",
+                "} = {a: 1, b: 1};"
+            ].join("\n"),
+            options: [{ multiline: true, consistent: true }],
+            parserOptions: { ecmaVersion: 6 }
+        },
+        {
+            code: [
+                "let {k = function() {dosomething();}} = obj;"
+            ].join("\n"),
+            options: [{ multiline: true, consistent: true }],
+            parserOptions: { ecmaVersion: 6 }
+        },
+        {
+            code: [
+                "let {",
+                "    k = function() {",
+                "        dosomething();",
+                "    }",
+                "} = obj;"
+            ].join("\n"),
+            options: [{ multiline: true, consistent: true }],
+            parserOptions: { ecmaVersion: 6 }
+        },
+        {
+            code: [
+                "var c = {a: 1,",
+                "b: 2};"
+            ].join("\n"),
+            options: [{ multiline: false, consistent: true }]
+        },
+        {
+            code: [
+                "let {a,",
+                "b} = {a: 1, b: 1};"
+            ].join("\n"),
+            options: [{ multiline: false, consistent: true }],
+            parserOptions: { ecmaVersion: 6 }
+        },
+
+        // "consistent" and "minProperties" ------------------------------------------
+        {
+            code: [
+                "var c = { a: 1 };"
+            ].join("\n"),
+            options: [{ multiline: true, consistent: true, minProperties: 2 }]
+        },
+        {
+            code: [
+                "var c = {",
+                "a: 1",
+                "};"
+            ].join("\n"),
+            options: [{ multiline: true, consistent: true, minProperties: 2 }]
+        },
+        {
+            code: [
+                "let {a} = {",
+                "a: 1",
+                "};"
+            ].join("\n"),
+            options: [{ multiline: true, consistent: true, minProperties: 2 }],
+            parserOptions: { ecmaVersion: 6 }
+        },
+        {
+            code: [
+                "let {",
+                "a",
+                "} = {",
+                "a: 1",
+                "};"
+            ].join("\n"),
+            options: [{ multiline: true, consistent: true, minProperties: 2 }],
+            parserOptions: { ecmaVersion: 6 }
+        },
+
         // "ObjectExpression" and "ObjectPattern" ---------------------------------------------
         {
             code: [
@@ -659,6 +815,289 @@ ruleTester.run("object-curly-newline", rule, {
             ]
         },
 
+        // "consistent" ------------------------------------------
+        {
+            code: [
+                "var b = {a: 1",
+                "};"
+            ].join("\n"),
+            output: [
+                "var b = {a: 1};"
+            ].join("\n"),
+            options: [{ multiline: true, consistent: true }],
+            errors: [
+                { line: 2, column: 1, message: "Unexpected line break before this closing brace." }
+            ]
+        },
+        {
+            code: [
+                "var b = {",
+                "a: 1};"
+            ].join("\n"),
+            output: [
+                "var b = {a: 1};"
+            ].join("\n"),
+            options: [{ multiline: true, consistent: true }],
+            errors: [
+                { line: 1, column: 9, message: "Unexpected line break after this opening brace." }
+            ]
+        },
+        {
+            code: [
+                "var c = {a: 1, b: 2",
+                "};"
+            ].join("\n"),
+            output: [
+                "var c = {a: 1, b: 2};"
+            ].join("\n"),
+            options: [{ multiline: true, consistent: true }],
+            errors: [
+                { line: 2, column: 1, message: "Unexpected line break before this closing brace." }
+            ]
+        },
+        {
+            code: [
+                "var c = {",
+                "a: 1, b: 2};"
+            ].join("\n"),
+            output: [
+                "var c = {a: 1, b: 2};"
+            ].join("\n"),
+            options: [{ multiline: true, consistent: true }],
+            errors: [
+                { line: 1, column: 9, message: "Unexpected line break after this opening brace." }
+            ]
+        },
+        {
+            code: [
+                "var c = {a: 1,",
+                "b: 2};"
+            ].join("\n"),
+            output: [
+                "var c = {",
+                "a: 1,",
+                "b: 2",
+                "};"
+            ].join("\n"),
+            options: [{ multiline: true, consistent: true }],
+            errors: [
+                { line: 1, column: 9, message: "Expected a line break after this opening brace." },
+                { line: 2, column: 5, message: "Expected a line break before this closing brace." }
+            ]
+        },
+        {
+            code: [
+                "var e = {a: function() {",
+                "dosomething();",
+                "}};"
+            ].join("\n"),
+            output: [
+                "var e = {",
+                "a: function() {",
+                "dosomething();",
+                "}",
+                "};"
+            ].join("\n"),
+            options: [{ multiline: true, consistent: true }],
+            errors: [
+                { line: 1, column: 9, message: "Expected a line break after this opening brace." },
+                { line: 3, column: 2, message: "Expected a line break before this closing brace." }
+            ]
+        },
+        {
+            code: [
+                "let {a",
+                "} = {a: 1}"
+            ].join("\n"),
+            output: [
+                "let {a} = {a: 1}"
+            ].join("\n"),
+            options: [{ multiline: true, consistent: true }],
+            parserOptions: { ecmaVersion: 6 },
+            errors: [
+                { line: 2, column: 1, message: "Unexpected line break before this closing brace." }
+            ]
+        },
+        {
+            code: [
+                "let {",
+                "a} = {a: 1}"
+            ].join("\n"),
+            output: [
+                "let {a} = {a: 1}"
+            ].join("\n"),
+            options: [{ multiline: true, consistent: true }],
+            parserOptions: { ecmaVersion: 6 },
+            errors: [
+                { line: 1, column: 5, message: "Unexpected line break after this opening brace." }
+            ]
+        },
+        {
+            code: [
+                "let {a, b",
+                "} = {a: 1, b: 2}"
+            ].join("\n"),
+            output: [
+                "let {a, b} = {a: 1, b: 2}"
+            ].join("\n"),
+            options: [{ multiline: true, consistent: true }],
+            parserOptions: { ecmaVersion: 6 },
+            errors: [
+                { line: 2, column: 1, message: "Unexpected line break before this closing brace." }
+            ]
+        },
+        {
+            code: [
+                "let {",
+                "a, b} = {a: 1, b: 2}"
+            ].join("\n"),
+            output: [
+                "let {a, b} = {a: 1, b: 2}"
+            ].join("\n"),
+            options: [{ multiline: true, consistent: true }],
+            parserOptions: { ecmaVersion: 6 },
+            errors: [
+                { line: 1, column: 5, message: "Unexpected line break after this opening brace." }
+            ]
+        },
+        {
+            code: [
+                "let {a,",
+                "b} = {a: 1, b: 2}"
+            ].join("\n"),
+            output: [
+                "let {",
+                "a,",
+                "b",
+                "} = {a: 1, b: 2}"
+            ].join("\n"),
+            options: [{ multiline: true, consistent: true }],
+            parserOptions: { ecmaVersion: 6 },
+            errors: [
+                { line: 1, column: 5, message: "Expected a line break after this opening brace." },
+                { line: 2, column: 2, message: "Expected a line break before this closing brace." }
+            ]
+        },
+        {
+            code: [
+                "let {e = function() {",
+                "dosomething();",
+                "}} = a;"
+            ].join("\n"),
+            output: [
+                "let {",
+                "e = function() {",
+                "dosomething();",
+                "}",
+                "} = a;"
+            ].join("\n"),
+            options: [{ multiline: true, consistent: true }],
+            parserOptions: { ecmaVersion: 6 },
+            errors: [
+                { line: 1, column: 5, message: "Expected a line break after this opening brace." },
+                { line: 3, column: 2, message: "Expected a line break before this closing brace." }
+            ]
+        },
+        {
+            code: [
+                "var c = {",
+                "a: 1,",
+                "b: 2};"
+            ].join("\n"),
+            output: [
+                "var c = {a: 1,",
+                "b: 2};"
+            ].join("\n"),
+            options: [{ multiline: false, consistent: true }],
+            errors: [
+                { line: 1, column: 9, message: "Unexpected line break after this opening brace." }
+            ]
+        },
+        {
+            code: [
+                "var c = {a: 1,",
+                "b: 2",
+                "};"
+            ].join("\n"),
+            output: [
+                "var c = {a: 1,",
+                "b: 2};"
+            ].join("\n"),
+            options: [{ multiline: false, consistent: true }],
+            errors: [
+                { line: 3, column: 1, message: "Unexpected line break before this closing brace." }
+            ]
+        },
+        {
+            code: [
+                "let {",
+                "a,",
+                "b} = {a: 1, b: 2};"
+            ].join("\n"),
+            output: [
+                "let {a,",
+                "b} = {a: 1, b: 2};"
+            ].join("\n"),
+            options: [{ multiline: false, consistent: true }],
+            parserOptions: { ecmaVersion: 6 },
+            errors: [
+                { line: 1, column: 5, message: "Unexpected line break after this opening brace." }
+            ]
+        },
+        {
+            code: [
+                "let {a,",
+                "b",
+                "} = {a: 1, b: 2};"
+            ].join("\n"),
+            output: [
+                "let {a,",
+                "b} = {a: 1, b: 2};"
+            ].join("\n"),
+            options: [{ multiline: false, consistent: true }],
+            parserOptions: { ecmaVersion: 6 },
+            errors: [
+                { line: 3, column: 1, message: "Unexpected line break before this closing brace." }
+            ]
+        },
+
+        // "consistent" and "minProperties" ------------------------------------------
+        {
+            code: [
+                "var c = {a: 1, b: 2};"
+            ].join("\n"),
+            output: [
+                "var c = {",
+                "a: 1, b: 2",
+                "};"
+            ].join("\n"),
+            options: [{ multiline: true, consistent: true, minProperties: 2 }],
+            errors: [
+                { line: 1, column: 9, message: "Expected a line break after this opening brace." },
+                { line: 1, column: 20, message: "Expected a line break before this closing brace." }
+            ]
+        },
+        {
+            code: [
+                "let {a, b} = {",
+                "a: 1, b: 2",
+                "};"
+            ].join("\n"),
+            output: [
+                "let {",
+                "a, b",
+                "} = {",
+                "a: 1, b: 2",
+                "};"
+            ].join("\n"),
+            options: [{ multiline: true, consistent: true, minProperties: 2 }],
+            parserOptions: { ecmaVersion: 6 },
+            errors: [
+                { line: 1, column: 5, message: "Expected a line break after this opening brace." },
+                { line: 1, column: 10, message: "Expected a line break before this closing brace." }
+            ]
+        },
+
         // "ObjectExpression" and "ObjectPattern" ---------------------------------------------
         {
             code: [