Repository: eslint/eslint (JavaScript)

Issue:
Proposal: minimum number of keys option for sort-keys
<!--
    ESLint adheres to the [JS Foundation Code of Conduct](https://js.foundation/community/code-of-conduct).

    This template is for requesting a rule change. If you are here for another reason, please see below:

    1. To report a bug: https://eslint.org/docs/developer-guide/contributing/reporting-bugs
    2. To propose a new rule: https://eslint.org/docs/developer-guide/contributing/new-rules
    3. To request a change that is not a bug fix, rule change, or new rule: https://eslint.org/docs/developer-guide/contributing/changes
    4. If you have any questions, please stop by our chatroom: https://gitter.im/eslint/eslint

    Note that leaving sections blank will make it difficult for us to troubleshoot and we may have to close the issue.
-->

**Background**

My team is undergoing an exercise where we re-evaluate our enabled lint rules to identify any pain points.

The most unpopular so far is `sort-keys`, as the value of this rule is only apparent for object literals with a large number of keys (the extreme case being an object containing something like the dependency list in `package.json`, where it's extremely useful that the keys are sorted).

For objects containing 2 or 3 keys, it becomes more of a chore, especially considering the fact that this rule [is not auto-fixable by design](https://github.com/eslint/eslint/issues/7714). 

---

**What rule do you want to change?**
[sort-keys](https://eslint.org/docs/rules/sort-keys)

**Does this change cause the rule to produce more or fewer warnings?**
Fewer when the option is set as it would allow the user to provide a minimum number of keys that an object literal should have in order for this rule to take effect. By default, the minimum number of keys would be 2.

**How will the change be implemented? (New option, new default behavior, etc.)?**
New option -- I'm thinking `minKeys` is consistent with the naming of other options throughout ESLint, but happy to hear other suggestions. 

**Please provide some example code that this change will affect:**

<!-- Put your code examples here -->
```js
/* eslint sort-keys: ["error", { minKeys: 5 }] */

const styles = {
  position: 'relative',
  color: 'blue'
};

const styles2 = {
  fontSize: 16,
  position: 'relative',
  color: 'blue',
  zIndex: 10,
  display: 'block',
};
```

**What does the rule currently do for this code?**
ESLint produces 3 errors, one error for the property ordering of `styles` and two errors for the property ordering of `styles2`.

**What will the rule do after it's changed?**
With `minKeys` set to 5, only `styles2` will produce lint errors, so there will be a total of 2 lint errors.

**Are you willing to submit a pull request to implement this change?**
Yes, happy to open a PR if the proposal reaches consensus


Discussion:
This seems reasonable. I'll support. :+1:

Unfortunately, it looks like there wasn't enough interest from the team
or community to implement this change. While we wish we'd be able to
accommodate everyone's requests, we do need to prioritize. We've found
that issues failing to reach accepted status after 21 days tend to
never be accepted, and as such, we [close those issues](https://eslint.org/docs/maintainer-guide/issues#when-to-close-an-issue).
This doesn't mean the idea isn't interesting or useful, just that it's
not something the team can commit to.

Thanks for contributing to ESLint and we appreciate your understanding.

[//]: # (auto-close)

I'd like to have this implemented, too! 😊 Waiting for the feature to be merged!
I'll champion this.  I think this is worth another look, given the large support from the team (3 thumbs up here and 1 on the corresponding PR) and the existence of the PR.



Patch:
diff --git a/docs/rules/sort-keys.md b/docs/rules/sort-keys.md
index e6c5155688e3..199d7bbc8a8e 100644
--- a/docs/rules/sort-keys.md
+++ b/docs/rules/sort-keys.md
@@ -61,7 +61,7 @@ let obj = {b: 1, ...c, a: 2};
 
 ```json
 {
-    "sort-keys": ["error", "asc", {"caseSensitive": true, "natural": false}]
+    "sort-keys": ["error", "asc", {"caseSensitive": true, "natural": false, "minKeys": 2}]
 }
 ```
 
@@ -70,9 +70,10 @@ The 1st option is `"asc"` or `"desc"`.
 * `"asc"` (default) - enforce properties to be in ascending order.
 * `"desc"` - enforce properties to be in descending order.
 
-The 2nd option is an object which has 2 properties.
+The 2nd option is an object which has 3 properties.
 
 * `caseSensitive` - if `true`, enforce properties to be in case-sensitive order. Default is `true`.
+* `minKeys` - Specifies the minimum number of keys that an object should have in order for the object's unsorted keys to produce an error. Default is `2`, which means by default all objects with unsorted keys will result in lint errors.
 * `natural` - if `true`, enforce properties to be in natural order. Default is `false`. Natural Order compares strings containing combination of letters and numbers in the way a human being would sort. It basically sorts numerically, instead of sorting alphabetically. So the number 10 comes after the number 3 in Natural Sorting.
 
 Example for a list:
@@ -167,6 +168,52 @@ Examples of **correct** code for the `{natural: true}` option:
 let obj = {1: a, 2: b, 10: c};
 ```
 
+### minKeys
+
+Examples of **incorrect** code for the `{minKeys: 4}` option:
+
+```js
+/*eslint sort-keys: ["error", "asc", {minKeys: 4}]*/
+/*eslint-env es6*/
+
+// 4 keys
+let obj = {
+    b: 2,
+    a: 1, // not sorted correctly (should be 1st key)
+    c: 3,
+    d: 4,
+};
+
+// 5 keys
+let obj = {
+    2: 'a',
+    1: 'b', // not sorted correctly (should be 1st key)
+    3: 'c',
+    4: 'd',
+    5: 'e',
+};
+```
+
+Examples of **correct** code for the `{minKeys: 4}` option:
+
+```js
+/*eslint sort-keys: ["error", "asc", {minKeys: 4}]*//
+/*eslint-env es6*/
+
+// 3 keys
+let obj = {
+    b: 2,
+    a: 1,
+    c: 3,
+};
+
+// 2 keys
+let obj = {
+    2: 'b',
+    1: 'a',
+};
+```
+
 ## When Not To Use It
 
 If you don't want to notify about properties' order, then it's safe to disable this rule.
diff --git a/lib/rules/sort-keys.js b/lib/rules/sort-keys.js
index fd5dd8a13bce..9e17650b3e02 100644
--- a/lib/rules/sort-keys.js
+++ b/lib/rules/sort-keys.js
@@ -96,6 +96,11 @@ module.exports = {
                     natural: {
                         type: "boolean",
                         default: false
+                    },
+                    minKeys: {
+                        type: "integer",
+                        minimum: 2,
+                        default: 2
                     }
                 },
                 additionalProperties: false
@@ -110,6 +115,7 @@ module.exports = {
         const options = context.options[1];
         const insensitive = options && options.caseSensitive === false;
         const natual = options && options.natural;
+        const minKeys = options && options.minKeys;
         const isValidOrder = isValidOrders[
             order + (insensitive ? "I" : "") + (natual ? "N" : "")
         ];
@@ -118,10 +124,11 @@ module.exports = {
         let stack = null;
 
         return {
-            ObjectExpression() {
+            ObjectExpression(node) {
                 stack = {
                     upper: stack,
-                    prevName: null
+                    prevName: null,
+                    numKeys: node.properties.length
                 };
             },
 
@@ -141,11 +148,12 @@ module.exports = {
                 }
 
                 const prevName = stack.prevName;
+                const numKeys = stack.numKeys;
                 const thisName = getPropertyName(node);
 
                 stack.prevName = thisName || prevName;
 
-                if (!prevName || !thisName) {
+                if (!prevName || !thisName || numKeys < minKeys) {
                     return;
                 }
 


Test patch:
diff --git a/tests/lib/rules/sort-keys.js b/tests/lib/rules/sort-keys.js
index 1ae091fad5b8..61c55ab7fe48 100644
--- a/tests/lib/rules/sort-keys.js
+++ b/tests/lib/rules/sort-keys.js
@@ -65,6 +65,9 @@ ruleTester.run("sort-keys", rule, {
         { code: "var obj = {1:1, '11':2, 2:4, A:3}", options: ["asc"] },
         { code: "var obj = {'#':1, 'Z':2, À:3, è:4}", options: ["asc"] },
 
+        // asc, minKeys should ignore unsorted keys when number of keys is less than minKeys
+        { code: "var obj = {a:1, c:2, b:3}", options: ["asc", { minKeys: 4 }] },
+
         // asc, insensitive
         { code: "var obj = {_:2, a:1, b:3} // asc, insensitive", options: ["asc", { caseSensitive: false }] },
         { code: "var obj = {a:1, b:3, c:2}", options: ["asc", { caseSensitive: false }] },
@@ -75,6 +78,9 @@ ruleTester.run("sort-keys", rule, {
         { code: "var obj = {1:1, '11':2, 2:4, A:3}", options: ["asc", { caseSensitive: false }] },
         { code: "var obj = {'#':1, 'Z':2, À:3, è:4}", options: ["asc", { caseSensitive: false }] },
 
+        // asc, insensitive, minKeys should ignore unsorted keys when number of keys is less than minKeys
+        { code: "var obj = {$:1, A:3, _:2, a:4}", options: ["asc", { caseSensitive: false, minKeys: 5 }] },
+
         // asc, natural
         { code: "var obj = {_:2, a:1, b:3} // asc, natural", options: ["asc", { natural: true }] },
         { code: "var obj = {a:1, b:3, c:2}", options: ["asc", { natural: true }] },
@@ -84,6 +90,9 @@ ruleTester.run("sort-keys", rule, {
         { code: "var obj = {1:1, 2:4, '11':2, A:3}", options: ["asc", { natural: true }] },
         { code: "var obj = {'#':1, 'Z':2, À:3, è:4}", options: ["asc", { natural: true }] },
 
+        // asc, natural, minKeys should ignore unsorted keys when number of keys is less than minKeys
+        { code: "var obj = {b_:1, a:2, b:3}", options: ["asc", { natural: true, minKeys: 4 }] },
+
         // asc, natural, insensitive
         { code: "var obj = {_:2, a:1, b:3} // asc, natural, insensitive", options: ["asc", { natural: true, caseSensitive: false }] },
         { code: "var obj = {a:1, b:3, c:2}", options: ["asc", { natural: true, caseSensitive: false }] },
@@ -94,6 +103,9 @@ ruleTester.run("sort-keys", rule, {
         { code: "var obj = {1:1, 2:4, '11':2, A:3}", options: ["asc", { natural: true, caseSensitive: false }] },
         { code: "var obj = {'#':1, 'Z':2, À:3, è:4}", options: ["asc", { natural: true, caseSensitive: false }] },
 
+        // asc, natural, insensitive, minKeys should ignore unsorted keys when number of keys is less than minKeys
+        { code: "var obj = {a:1, _:2, b:3}", options: ["asc", { natural: true, caseSensitive: false, minKeys: 4 }] },
+
         // desc
         { code: "var obj = {b:3, a:1, _:2} // desc", options: ["desc"] },
         { code: "var obj = {c:2, b:3, a:1}", options: ["desc"] },
@@ -103,6 +115,9 @@ ruleTester.run("sort-keys", rule, {
         { code: "var obj = {A:3, 2:4, '11':2, 1:1}", options: ["desc"] },
         { code: "var obj = {è:4, À:3, 'Z':2, '#':1}", options: ["desc"] },
 
+        // desc, minKeys should ignore unsorted keys when number of keys is less than minKeys
+        { code: "var obj = {a:1, c:2, b:3}", options: ["desc", { minKeys: 4 }] },
+
         // desc, insensitive
         { code: "var obj = {b:3, a:1, _:2} // desc, insensitive", options: ["desc", { caseSensitive: false }] },
         { code: "var obj = {c:2, b:3, a:1}", options: ["desc", { caseSensitive: false }] },
@@ -113,6 +128,9 @@ ruleTester.run("sort-keys", rule, {
         { code: "var obj = {A:3, 2:4, '11':2, 1:1}", options: ["desc", { caseSensitive: false }] },
         { code: "var obj = {è:4, À:3, 'Z':2, '#':1}", options: ["desc", { caseSensitive: false }] },
 
+        // desc, insensitive, minKeys should ignore unsorted keys when number of keys is less than minKeys
+        { code: "var obj = {$:1, _:2, A:3, a:4}", options: ["desc", { caseSensitive: false, minKeys: 5 }] },
+
         // desc, natural
         { code: "var obj = {b:3, a:1, _:2} // desc, natural", options: ["desc", { natural: true }] },
         { code: "var obj = {c:2, b:3, a:1}", options: ["desc", { natural: true }] },
@@ -122,6 +140,9 @@ ruleTester.run("sort-keys", rule, {
         { code: "var obj = {A:3, '11':2, 2:4, 1:1}", options: ["desc", { natural: true }] },
         { code: "var obj = {è:4, À:3, 'Z':2, '#':1}", options: ["desc", { natural: true }] },
 
+        // desc, natural, minKeys should ignore unsorted keys when number of keys is less than minKeys
+        { code: "var obj = {b_:1, a:2, b:3}", options: ["desc", { natural: true, minKeys: 4 }] },
+
         // desc, natural, insensitive
         { code: "var obj = {b:3, a:1, _:2} // desc, natural, insensitive", options: ["desc", { natural: true, caseSensitive: false }] },
         { code: "var obj = {c:2, b:3, a:1}", options: ["desc", { natural: true, caseSensitive: false }] },
@@ -130,7 +151,10 @@ ruleTester.run("sort-keys", rule, {
         { code: "var obj = {C:2, c:3, b_:1}", options: ["desc", { natural: true, caseSensitive: false }] },
         { code: "var obj = {a:4, A:3, _:2, $:1}", options: ["desc", { natural: true, caseSensitive: false }] },
         { code: "var obj = {A:3, '11':2, 2:4, 1:1}", options: ["desc", { natural: true, caseSensitive: false }] },
-        { code: "var obj = {è:4, À:3, 'Z':2, '#':1}", options: ["desc", { natural: true, caseSensitive: false }] }
+        { code: "var obj = {è:4, À:3, 'Z':2, '#':1}", options: ["desc", { natural: true, caseSensitive: false }] },
+
+        // desc, natural, insensitive, minKeys should ignore unsorted keys when number of keys is less than minKeys
+        { code: "var obj = {a:1, _:2, b:3}", options: ["desc", { natural: true, caseSensitive: false, minKeys: 4 }] }
     ],
     invalid: [
 
@@ -278,6 +302,15 @@ ruleTester.run("sort-keys", rule, {
             ]
         },
 
+        // asc, minKeys should error when number of keys is greater than or equal to minKeys
+        {
+            code: "var obj = {a:1, _:2, b:3}",
+            options: ["asc", { minKeys: 3 }],
+            errors: [
+                "Expected object keys to be in ascending order. '_' should be before 'a'."
+            ]
+        },
+
         // asc, insensitive
         {
             code: "var obj = {a:1, _:2, b:3} // asc, insensitive",
@@ -322,6 +355,15 @@ ruleTester.run("sort-keys", rule, {
             ]
         },
 
+        // asc, insensitive, minKeys should error when number of keys is greater than or equal to minKeys
+        {
+            code: "var obj = {a:1, _:2, b:3}",
+            options: ["asc", { caseSensitive: false, minKeys: 3 }],
+            errors: [
+                "Expected object keys to be in insensitive ascending order. '_' should be before 'a'."
+            ]
+        },
+
         // asc, natural
         {
             code: "var obj = {a:1, _:2, b:3} // asc, natural",
@@ -373,6 +415,15 @@ ruleTester.run("sort-keys", rule, {
             ]
         },
 
+        // asc, natural, minKeys should error when number of keys is greater than or equal to minKeys
+        {
+            code: "var obj = {a:1, _:2, b:3}",
+            options: ["asc", { natural: true, minKeys: 2 }],
+            errors: [
+                "Expected object keys to be in natural ascending order. '_' should be before 'a'."
+            ]
+        },
+
         // asc, natural, insensitive
         {
             code: "var obj = {a:1, _:2, b:3} // asc, natural, insensitive",
@@ -417,6 +468,15 @@ ruleTester.run("sort-keys", rule, {
             ]
         },
 
+        // asc, natural, insensitive, minKeys should error when number of keys is greater than or equal to minKeys
+        {
+            code: "var obj = {a:1, _:2, b:3}",
+            options: ["asc", { natural: true, caseSensitive: false, minKeys: 3 }],
+            errors: [
+                "Expected object keys to be in natural insensitive ascending order. '_' should be before 'a'."
+            ]
+        },
+
         // desc
         {
             code: "var obj = {a:1, _:2, b:3} // desc",
@@ -471,6 +531,15 @@ ruleTester.run("sort-keys", rule, {
             ]
         },
 
+        // desc, minKeys should error when number of keys is greater than or equal to minKeys
+        {
+            code: "var obj = {a:1, _:2, b:3}",
+            options: ["desc", { minKeys: 3 }],
+            errors: [
+                "Expected object keys to be in descending order. 'b' should be before '_'."
+            ]
+        },
+
         // desc, insensitive
         {
             code: "var obj = {a:1, _:2, b:3} // desc, insensitive",
@@ -525,6 +594,15 @@ ruleTester.run("sort-keys", rule, {
             ]
         },
 
+        // desc, insensitive should error when number of keys is greater than or equal to minKeys
+        {
+            code: "var obj = {a:1, _:2, b:3}",
+            options: ["desc", { caseSensitive: false, minKeys: 2 }],
+            errors: [
+                "Expected object keys to be in insensitive descending order. 'b' should be before '_'."
+            ]
+        },
+
         // desc, natural
         {
             code: "var obj = {a:1, _:2, b:3} // desc, natural",
@@ -580,6 +658,15 @@ ruleTester.run("sort-keys", rule, {
             ]
         },
 
+        // desc, natural should error when number of keys is greater than or equal to minKeys
+        {
+            code: "var obj = {a:1, _:2, b:3}",
+            options: ["desc", { natural: true, minKeys: 3 }],
+            errors: [
+                "Expected object keys to be in natural descending order. 'b' should be before '_'."
+            ]
+        },
+
         // desc, natural, insensitive
         {
             code: "var obj = {a:1, _:2, b:3} // desc, natural, insensitive",
@@ -633,6 +720,15 @@ ruleTester.run("sort-keys", rule, {
                 "Expected object keys to be in natural insensitive descending order. 'À' should be before '#'.",
                 "Expected object keys to be in natural insensitive descending order. 'è' should be before 'Z'."
             ]
+        },
+
+        // desc, natural, insensitive should error when number of keys is greater than or equal to minKeys
+        {
+            code: "var obj = {a:1, _:2, b:3}",
+            options: ["desc", { natural: true, caseSensitive: false, minKeys: 2 }],
+            errors: [
+                "Expected object keys to be in natural insensitive descending order. 'b' should be before '_'."
+            ]
         }
     ]
 });