UNPKG

14.6 kBJavaScriptView Raw
1/**
2 * @fileoverview Rule to specify spacing of object literal keys and values
3 * @author Brandon Mills
4 * @copyright 2014 Brandon Mills. All rights reserved.
5 */
6"use strict";
7
8//------------------------------------------------------------------------------
9// Helpers
10//------------------------------------------------------------------------------
11
12/**
13 * Checks whether a string contains a line terminator as defined in
14 * http://www.ecma-international.org/ecma-262/5.1/#sec-7.3
15 * @param {string} str String to test.
16 * @returns {boolean} True if str contains a line terminator.
17 */
18function containsLineTerminator(str) {
19 return /[\n\r\u2028\u2029]/.test(str);
20}
21
22/**
23 * Gets the last element of an array.
24 * @param {Array} arr An array.
25 * @returns {any} Last element of arr.
26 */
27function last(arr) {
28 return arr[arr.length - 1];
29}
30
31/**
32 * Checks whether a property is a member of the property group it follows.
33 * @param {ASTNode} lastMember The last Property known to be in the group.
34 * @param {ASTNode} candidate The next Property that might be in the group.
35 * @returns {boolean} True if the candidate property is part of the group.
36 */
37function continuesPropertyGroup(lastMember, candidate) {
38 var groupEndLine = lastMember.loc.start.line,
39 candidateStartLine = candidate.loc.start.line,
40 comments, i;
41
42 if (candidateStartLine - groupEndLine <= 1) {
43 return true;
44 }
45
46 // Check that the first comment is adjacent to the end of the group, the
47 // last comment is adjacent to the candidate property, and that successive
48 // comments are adjacent to each other.
49 comments = candidate.leadingComments;
50 if (
51 comments &&
52 comments[0].loc.start.line - groupEndLine <= 1 &&
53 candidateStartLine - last(comments).loc.end.line <= 1
54 ) {
55 for (i = 1; i < comments.length; i++) {
56 if (comments[i].loc.start.line - comments[i - 1].loc.end.line > 1) {
57 return false;
58 }
59 }
60 return true;
61 }
62
63 return false;
64}
65
66/**
67 * Checks whether a node is contained on a single line.
68 * @param {ASTNode} node AST Node being evaluated.
69 * @returns {boolean} True if the node is a single line.
70 */
71function isSingleLine(node) {
72 return (node.loc.end.line === node.loc.start.line);
73}
74
75/** Sets option values from the configured options with defaults
76 * @param {Object} toOptions Object to be initialized
77 * @param {Object} fromOptions Object to be initialized from
78 * @returns {Object} The object with correctly initialized options and values
79 */
80function initOptions(toOptions, fromOptions) {
81 toOptions.mode = fromOptions.mode || "strict";
82
83 // Set align if exists - multiLine case
84 if (typeof fromOptions.align !== "undefined") {
85 toOptions.align = fromOptions.align;
86 }
87
88 // Set value of beforeColon
89 if (typeof fromOptions.beforeColon !== "undefined") {
90 toOptions.beforeColon = +fromOptions.beforeColon;
91 } else {
92 toOptions.beforeColon = 0;
93 }
94
95 // Set value of afterColon
96 if (typeof fromOptions.afterColon !== "undefined") {
97 toOptions.afterColon = +fromOptions.afterColon;
98 } else {
99 toOptions.afterColon = 1;
100 }
101
102 return toOptions;
103}
104
105//------------------------------------------------------------------------------
106// Rule Definition
107//------------------------------------------------------------------------------
108
109var messages = {
110 key: "{{error}} space after {{computed}}key '{{key}}'.",
111 value: "{{error}} space before value for {{computed}}key '{{key}}'."
112};
113
114module.exports = function(context) {
115
116 /**
117 * OPTIONS
118 * "key-spacing": [2, {
119 * beforeColon: false,
120 * afterColon: true,
121 * align: "colon" // Optional, or "value"
122 * }
123 */
124
125 var options = context.options[0] || {},
126 multiLineOptions = initOptions({}, (options.multiLine || options)),
127 singleLineOptions = initOptions({}, (options.singleLine || options));
128
129 /**
130 * Determines if the given property is key-value property.
131 * @param {ASTNode} property Property node to check.
132 * @returns {Boolean} Whether the property is a key-value property.
133 */
134 function isKeyValueProperty(property) {
135 return !(
136 property.method ||
137 property.shorthand ||
138 property.kind !== "init" ||
139 property.type !== "Property" // Could be "ExperimentalSpreadProperty" or "SpreadProperty"
140 );
141 }
142
143 /**
144 * Starting from the given a node (a property.key node here) looks forward
145 * until it finds the last token before a colon punctuator and returns it.
146 * @param {ASTNode} node The node to start looking from.
147 * @returns {ASTNode} The last token before a colon punctuator.
148 */
149 function getLastTokenBeforeColon(node) {
150 var prevNode;
151
152 while (node && (node.type !== "Punctuator" || node.value !== ":")) {
153 prevNode = node;
154 node = context.getTokenAfter(node);
155 }
156
157 return prevNode;
158 }
159
160 /**
161 * Starting from the given a node (a property.key node here) looks forward
162 * until it finds the colon punctuator and returns it.
163 * @param {ASTNode} node The node to start looking from.
164 * @returns {ASTNode} The colon punctuator.
165 */
166 function getNextColon(node) {
167
168 while (node && (node.type !== "Punctuator" || node.value !== ":")) {
169 node = context.getTokenAfter(node);
170 }
171
172 return node;
173 }
174
175 /**
176 * Gets an object literal property's key as the identifier name or string value.
177 * @param {ASTNode} property Property node whose key to retrieve.
178 * @returns {string} The property's key.
179 */
180 function getKey(property) {
181 var key = property.key;
182
183 if (property.computed) {
184 return context.getSource().slice(key.range[0], key.range[1]);
185 }
186
187 return property.key.name || property.key.value;
188 }
189
190 /**
191 * Reports an appropriately-formatted error if spacing is incorrect on one
192 * side of the colon.
193 * @param {ASTNode} property Key-value pair in an object literal.
194 * @param {string} side Side being verified - either "key" or "value".
195 * @param {string} whitespace Actual whitespace string.
196 * @param {int} expected Expected whitespace length.
197 * @param {string} mode Value of the mode as "strict" or "minimum"
198 * @returns {void}
199 */
200 function report(property, side, whitespace, expected, mode) {
201 var diff = whitespace.length - expected,
202 key = property.key,
203 firstTokenAfterColon = context.getTokenAfter(getNextColon(key)),
204 location = side === "key" ? key.loc.start : firstTokenAfterColon.loc.start;
205
206 if ((diff && mode === "strict" || diff < 0 && mode === "minimum") &&
207 !(expected && containsLineTerminator(whitespace))
208 ) {
209 context.report(property[side], location, messages[side], {
210 error: diff > 0 ? "Extra" : "Missing",
211 computed: property.computed ? "computed " : "",
212 key: getKey(property)
213 });
214 }
215 }
216
217 /**
218 * Gets the number of characters in a key, including quotes around string
219 * keys and braces around computed property keys.
220 * @param {ASTNode} property Property of on object literal.
221 * @returns {int} Width of the key.
222 */
223 function getKeyWidth(property) {
224 var startToken, endToken;
225
226 startToken = context.getFirstToken(property);
227 endToken = getLastTokenBeforeColon(property.key);
228
229 return endToken.range[1] - startToken.range[0];
230 }
231
232 /**
233 * Gets the whitespace around the colon in an object literal property.
234 * @param {ASTNode} property Property node from an object literal.
235 * @returns {Object} Whitespace before and after the property's colon.
236 */
237 function getPropertyWhitespace(property) {
238 var whitespace = /(\s*):(\s*)/.exec(context.getSource().slice(
239 property.key.range[1], property.value.range[0]
240 ));
241
242 if (whitespace) {
243 return {
244 beforeColon: whitespace[1],
245 afterColon: whitespace[2]
246 };
247 }
248 return null;
249 }
250
251 /**
252 * Creates groups of properties.
253 * @param {ASTNode} node ObjectExpression node being evaluated.
254 * @returns {Array.<ASTNode[]>} Groups of property AST node lists.
255 */
256 function createGroups(node) {
257 if (node.properties.length === 1) {
258 return [node.properties];
259 }
260
261 return node.properties.reduce(function(groups, property) {
262 var currentGroup = last(groups),
263 prev = last(currentGroup);
264
265 if (!prev || continuesPropertyGroup(prev, property)) {
266 currentGroup.push(property);
267 } else {
268 groups.push([property]);
269 }
270
271 return groups;
272 }, [
273 []
274 ]);
275 }
276
277 /**
278 * Verifies correct vertical alignment of a group of properties.
279 * @param {ASTNode[]} properties List of Property AST nodes.
280 * @returns {void}
281 */
282 function verifyGroupAlignment(properties) {
283 var length = properties.length,
284 widths = properties.map(getKeyWidth), // Width of keys, including quotes
285 targetWidth = Math.max.apply(null, widths),
286 i, property, whitespace, width,
287 align = multiLineOptions.align,
288 beforeColon = multiLineOptions.beforeColon,
289 afterColon = multiLineOptions.afterColon,
290 mode = multiLineOptions.mode;
291
292 // Conditionally include one space before or after colon
293 targetWidth += (align === "colon" ? beforeColon : afterColon);
294
295 for (i = 0; i < length; i++) {
296 property = properties[i];
297 whitespace = getPropertyWhitespace(property);
298 if (whitespace) { // Object literal getters/setters lack a colon
299 width = widths[i];
300
301 if (align === "value") {
302 report(property, "key", whitespace.beforeColon, beforeColon, mode);
303 report(property, "value", whitespace.afterColon, targetWidth - width, mode);
304 } else { // align = "colon"
305 report(property, "key", whitespace.beforeColon, targetWidth - width, mode);
306 report(property, "value", whitespace.afterColon, afterColon, mode);
307 }
308 }
309 }
310 }
311
312 /**
313 * Verifies vertical alignment, taking into account groups of properties.
314 * @param {ASTNode} node ObjectExpression node being evaluated.
315 * @returns {void}
316 */
317 function verifyAlignment(node) {
318 createGroups(node).forEach(function(group) {
319 verifyGroupAlignment(group.filter(isKeyValueProperty));
320 });
321 }
322
323 /**
324 * Verifies spacing of property conforms to specified options.
325 * @param {ASTNode} node Property node being evaluated.
326 * @param {Object} lineOptions Configured singleLine or multiLine options
327 * @returns {void}
328 */
329 function verifySpacing(node, lineOptions) {
330 var actual = getPropertyWhitespace(node);
331 if (actual) { // Object literal getters/setters lack colons
332 report(node, "key", actual.beforeColon, lineOptions.beforeColon, lineOptions.mode);
333 report(node, "value", actual.afterColon, lineOptions.afterColon, lineOptions.mode);
334 }
335 }
336
337 /**
338 * Verifies spacing of each property in a list.
339 * @param {ASTNode[]} properties List of Property AST nodes.
340 * @returns {void}
341 */
342 function verifyListSpacing(properties) {
343 var length = properties.length;
344
345 for (var i = 0; i < length; i++) {
346 verifySpacing(properties[i], singleLineOptions);
347 }
348 }
349
350 //--------------------------------------------------------------------------
351 // Public API
352 //--------------------------------------------------------------------------
353
354 if (multiLineOptions.align) { // Verify vertical alignment
355
356 return {
357 "ObjectExpression": function(node) {
358 if (isSingleLine(node)) {
359 verifyListSpacing(node.properties);
360 } else {
361 verifyAlignment(node);
362 }
363 }
364 };
365
366 } else { // Obey beforeColon and afterColon in each property as configured
367
368 return {
369 "Property": function(node) {
370 verifySpacing(node, isSingleLine(node) ? singleLineOptions : multiLineOptions);
371 }
372 };
373
374 }
375
376};
377
378module.exports.schema = [{
379 "anyOf": [
380 {
381 "type": "object",
382 "properties": {
383 "align": {
384 "enum": ["colon", "value"]
385 },
386 "mode": {
387 "enum": ["strict", "minimum"]
388 },
389 "beforeColon": {
390 "type": "boolean"
391 },
392 "afterColon": {
393 "type": "boolean"
394 }
395 },
396 "additionalProperties": false
397 },
398 {
399 "type": "object",
400 "properties": {
401 "singleLine": {
402 "type": "object",
403 "properties": {
404 "mode": {
405 "enum": ["strict", "minimum"]
406 },
407 "beforeColon": {
408 "type": "boolean"
409 },
410 "afterColon": {
411 "type": "boolean"
412 }
413 },
414 "additionalProperties": false
415 },
416 "multiLine": {
417 "type": "object",
418 "properties": {
419 "align": {
420 "enum": ["colon", "value"]
421 },
422 "mode": {
423 "enum": ["strict", "minimum"]
424 },
425 "beforeColon": {
426 "type": "boolean"
427 },
428 "afterColon": {
429 "type": "boolean"
430 }
431 },
432 "additionalProperties": false
433 }
434 },
435 "additionalProperties": false
436 }
437 ]
438}];