1 | /**
|
2 | * @fileoverview Rule to require object keys to be sorted
|
3 | * @author Toru Nagashima
|
4 | */
|
5 |
|
6 | ;
|
7 |
|
8 | //------------------------------------------------------------------------------
|
9 | // Requirements
|
10 | //------------------------------------------------------------------------------
|
11 |
|
12 | const astUtils = require("./utils/ast-utils"),
|
13 | naturalCompare = require("natural-compare");
|
14 |
|
15 | //------------------------------------------------------------------------------
|
16 | // Helpers
|
17 | //------------------------------------------------------------------------------
|
18 |
|
19 | /**
|
20 | * Gets the property name of the given `Property` node.
|
21 | *
|
22 | * - If the property's key is an `Identifier` node, this returns the key's name
|
23 | * whether it's a computed property or not.
|
24 | * - If the property has a static name, this returns the static name.
|
25 | * - Otherwise, this returns null.
|
26 | * @param {ASTNode} node The `Property` node to get.
|
27 | * @returns {string|null} The property name or null.
|
28 | * @private
|
29 | */
|
30 | function getPropertyName(node) {
|
31 | const staticName = astUtils.getStaticPropertyName(node);
|
32 |
|
33 | if (staticName !== null) {
|
34 | return staticName;
|
35 | }
|
36 |
|
37 | return node.key.name || null;
|
38 | }
|
39 |
|
40 | /**
|
41 | * Functions which check that the given 2 names are in specific order.
|
42 | *
|
43 | * Postfix `I` is meant insensitive.
|
44 | * Postfix `N` is meant natural.
|
45 | * @private
|
46 | */
|
47 | const isValidOrders = {
|
48 | asc(a, b) {
|
49 | return a <= b;
|
50 | },
|
51 | ascI(a, b) {
|
52 | return a.toLowerCase() <= b.toLowerCase();
|
53 | },
|
54 | ascN(a, b) {
|
55 | return naturalCompare(a, b) <= 0;
|
56 | },
|
57 | ascIN(a, b) {
|
58 | return naturalCompare(a.toLowerCase(), b.toLowerCase()) <= 0;
|
59 | },
|
60 | desc(a, b) {
|
61 | return isValidOrders.asc(b, a);
|
62 | },
|
63 | descI(a, b) {
|
64 | return isValidOrders.ascI(b, a);
|
65 | },
|
66 | descN(a, b) {
|
67 | return isValidOrders.ascN(b, a);
|
68 | },
|
69 | descIN(a, b) {
|
70 | return isValidOrders.ascIN(b, a);
|
71 | }
|
72 | };
|
73 |
|
74 | //------------------------------------------------------------------------------
|
75 | // Rule Definition
|
76 | //------------------------------------------------------------------------------
|
77 |
|
78 | module.exports = {
|
79 | meta: {
|
80 | type: "suggestion",
|
81 |
|
82 | docs: {
|
83 | description: "require object keys to be sorted",
|
84 | category: "Stylistic Issues",
|
85 | recommended: false,
|
86 | url: "https://eslint.org/docs/rules/sort-keys"
|
87 | },
|
88 |
|
89 | schema: [
|
90 | {
|
91 | enum: ["asc", "desc"]
|
92 | },
|
93 | {
|
94 | type: "object",
|
95 | properties: {
|
96 | caseSensitive: {
|
97 | type: "boolean",
|
98 | default: true
|
99 | },
|
100 | natural: {
|
101 | type: "boolean",
|
102 | default: false
|
103 | },
|
104 | minKeys: {
|
105 | type: "integer",
|
106 | minimum: 2,
|
107 | default: 2
|
108 | }
|
109 | },
|
110 | additionalProperties: false
|
111 | }
|
112 | ],
|
113 |
|
114 | messages: {
|
115 | sortKeys: "Expected object keys to be in {{natural}}{{insensitive}}{{order}}ending order. '{{thisName}}' should be before '{{prevName}}'."
|
116 | }
|
117 | },
|
118 |
|
119 | create(context) {
|
120 |
|
121 | // Parse options.
|
122 | const order = context.options[0] || "asc";
|
123 | const options = context.options[1];
|
124 | const insensitive = options && options.caseSensitive === false;
|
125 | const natural = options && options.natural;
|
126 | const minKeys = options && options.minKeys;
|
127 | const isValidOrder = isValidOrders[
|
128 | order + (insensitive ? "I" : "") + (natural ? "N" : "")
|
129 | ];
|
130 |
|
131 | // The stack to save the previous property's name for each object literals.
|
132 | let stack = null;
|
133 |
|
134 | return {
|
135 | ObjectExpression(node) {
|
136 | stack = {
|
137 | upper: stack,
|
138 | prevName: null,
|
139 | numKeys: node.properties.length
|
140 | };
|
141 | },
|
142 |
|
143 | "ObjectExpression:exit"() {
|
144 | stack = stack.upper;
|
145 | },
|
146 |
|
147 | SpreadElement(node) {
|
148 | if (node.parent.type === "ObjectExpression") {
|
149 | stack.prevName = null;
|
150 | }
|
151 | },
|
152 |
|
153 | Property(node) {
|
154 | if (node.parent.type === "ObjectPattern") {
|
155 | return;
|
156 | }
|
157 |
|
158 | const prevName = stack.prevName;
|
159 | const numKeys = stack.numKeys;
|
160 | const thisName = getPropertyName(node);
|
161 |
|
162 | if (thisName !== null) {
|
163 | stack.prevName = thisName;
|
164 | }
|
165 |
|
166 | if (prevName === null || thisName === null || numKeys < minKeys) {
|
167 | return;
|
168 | }
|
169 |
|
170 | if (!isValidOrder(prevName, thisName)) {
|
171 | context.report({
|
172 | node,
|
173 | loc: node.key.loc,
|
174 | messageId: "sortKeys",
|
175 | data: {
|
176 | thisName,
|
177 | prevName,
|
178 | order,
|
179 | insensitive: insensitive ? "insensitive " : "",
|
180 | natural: natural ? "natural " : ""
|
181 | }
|
182 | });
|
183 | }
|
184 | }
|
185 | };
|
186 | }
|
187 | };
|