.gitignore added
[dotfiles/.git] / .config / coc / extensions / node_modules / coc-prettier / node_modules / eslint / lib / rules / key-spacing.js
1 /**
2  * @fileoverview Rule to specify spacing of object literal keys and values
3  * @author Brandon Mills
4  */
5 "use strict";
6
7 //------------------------------------------------------------------------------
8 // Requirements
9 //------------------------------------------------------------------------------
10
11 const astUtils = require("./utils/ast-utils");
12
13 //------------------------------------------------------------------------------
14 // Helpers
15 //------------------------------------------------------------------------------
16
17 /**
18  * Checks whether a string contains a line terminator as defined in
19  * http://www.ecma-international.org/ecma-262/5.1/#sec-7.3
20  * @param {string} str String to test.
21  * @returns {boolean} True if str contains a line terminator.
22  */
23 function containsLineTerminator(str) {
24     return astUtils.LINEBREAK_MATCHER.test(str);
25 }
26
27 /**
28  * Gets the last element of an array.
29  * @param {Array} arr An array.
30  * @returns {any} Last element of arr.
31  */
32 function last(arr) {
33     return arr[arr.length - 1];
34 }
35
36 /**
37  * Checks whether a node is contained on a single line.
38  * @param {ASTNode} node AST Node being evaluated.
39  * @returns {boolean} True if the node is a single line.
40  */
41 function isSingleLine(node) {
42     return (node.loc.end.line === node.loc.start.line);
43 }
44
45 /**
46  * Checks whether the properties on a single line.
47  * @param {ASTNode[]} properties List of Property AST nodes.
48  * @returns {boolean} True if all properties is on a single line.
49  */
50 function isSingleLineProperties(properties) {
51     const [firstProp] = properties,
52         lastProp = last(properties);
53
54     return firstProp.loc.start.line === lastProp.loc.end.line;
55 }
56
57 /**
58  * Initializes a single option property from the configuration with defaults for undefined values
59  * @param {Object} toOptions Object to be initialized
60  * @param {Object} fromOptions Object to be initialized from
61  * @returns {Object} The object with correctly initialized options and values
62  */
63 function initOptionProperty(toOptions, fromOptions) {
64     toOptions.mode = fromOptions.mode || "strict";
65
66     // Set value of beforeColon
67     if (typeof fromOptions.beforeColon !== "undefined") {
68         toOptions.beforeColon = +fromOptions.beforeColon;
69     } else {
70         toOptions.beforeColon = 0;
71     }
72
73     // Set value of afterColon
74     if (typeof fromOptions.afterColon !== "undefined") {
75         toOptions.afterColon = +fromOptions.afterColon;
76     } else {
77         toOptions.afterColon = 1;
78     }
79
80     // Set align if exists
81     if (typeof fromOptions.align !== "undefined") {
82         if (typeof fromOptions.align === "object") {
83             toOptions.align = fromOptions.align;
84         } else { // "string"
85             toOptions.align = {
86                 on: fromOptions.align,
87                 mode: toOptions.mode,
88                 beforeColon: toOptions.beforeColon,
89                 afterColon: toOptions.afterColon
90             };
91         }
92     }
93
94     return toOptions;
95 }
96
97 /**
98  * Initializes all the option values (singleLine, multiLine and align) from the configuration with defaults for undefined values
99  * @param {Object} toOptions Object to be initialized
100  * @param {Object} fromOptions Object to be initialized from
101  * @returns {Object} The object with correctly initialized options and values
102  */
103 function initOptions(toOptions, fromOptions) {
104     if (typeof fromOptions.align === "object") {
105
106         // Initialize the alignment configuration
107         toOptions.align = initOptionProperty({}, fromOptions.align);
108         toOptions.align.on = fromOptions.align.on || "colon";
109         toOptions.align.mode = fromOptions.align.mode || "strict";
110
111         toOptions.multiLine = initOptionProperty({}, (fromOptions.multiLine || fromOptions));
112         toOptions.singleLine = initOptionProperty({}, (fromOptions.singleLine || fromOptions));
113
114     } else { // string or undefined
115         toOptions.multiLine = initOptionProperty({}, (fromOptions.multiLine || fromOptions));
116         toOptions.singleLine = initOptionProperty({}, (fromOptions.singleLine || fromOptions));
117
118         // If alignment options are defined in multiLine, pull them out into the general align configuration
119         if (toOptions.multiLine.align) {
120             toOptions.align = {
121                 on: toOptions.multiLine.align.on,
122                 mode: toOptions.multiLine.align.mode || toOptions.multiLine.mode,
123                 beforeColon: toOptions.multiLine.align.beforeColon,
124                 afterColon: toOptions.multiLine.align.afterColon
125             };
126         }
127     }
128
129     return toOptions;
130 }
131
132 //------------------------------------------------------------------------------
133 // Rule Definition
134 //------------------------------------------------------------------------------
135
136 module.exports = {
137     meta: {
138         type: "layout",
139
140         docs: {
141             description: "enforce consistent spacing between keys and values in object literal properties",
142             category: "Stylistic Issues",
143             recommended: false,
144             url: "https://eslint.org/docs/rules/key-spacing"
145         },
146
147         fixable: "whitespace",
148
149         schema: [{
150             anyOf: [
151                 {
152                     type: "object",
153                     properties: {
154                         align: {
155                             anyOf: [
156                                 {
157                                     enum: ["colon", "value"]
158                                 },
159                                 {
160                                     type: "object",
161                                     properties: {
162                                         mode: {
163                                             enum: ["strict", "minimum"]
164                                         },
165                                         on: {
166                                             enum: ["colon", "value"]
167                                         },
168                                         beforeColon: {
169                                             type: "boolean"
170                                         },
171                                         afterColon: {
172                                             type: "boolean"
173                                         }
174                                     },
175                                     additionalProperties: false
176                                 }
177                             ]
178                         },
179                         mode: {
180                             enum: ["strict", "minimum"]
181                         },
182                         beforeColon: {
183                             type: "boolean"
184                         },
185                         afterColon: {
186                             type: "boolean"
187                         }
188                     },
189                     additionalProperties: false
190                 },
191                 {
192                     type: "object",
193                     properties: {
194                         singleLine: {
195                             type: "object",
196                             properties: {
197                                 mode: {
198                                     enum: ["strict", "minimum"]
199                                 },
200                                 beforeColon: {
201                                     type: "boolean"
202                                 },
203                                 afterColon: {
204                                     type: "boolean"
205                                 }
206                             },
207                             additionalProperties: false
208                         },
209                         multiLine: {
210                             type: "object",
211                             properties: {
212                                 align: {
213                                     anyOf: [
214                                         {
215                                             enum: ["colon", "value"]
216                                         },
217                                         {
218                                             type: "object",
219                                             properties: {
220                                                 mode: {
221                                                     enum: ["strict", "minimum"]
222                                                 },
223                                                 on: {
224                                                     enum: ["colon", "value"]
225                                                 },
226                                                 beforeColon: {
227                                                     type: "boolean"
228                                                 },
229                                                 afterColon: {
230                                                     type: "boolean"
231                                                 }
232                                             },
233                                             additionalProperties: false
234                                         }
235                                     ]
236                                 },
237                                 mode: {
238                                     enum: ["strict", "minimum"]
239                                 },
240                                 beforeColon: {
241                                     type: "boolean"
242                                 },
243                                 afterColon: {
244                                     type: "boolean"
245                                 }
246                             },
247                             additionalProperties: false
248                         }
249                     },
250                     additionalProperties: false
251                 },
252                 {
253                     type: "object",
254                     properties: {
255                         singleLine: {
256                             type: "object",
257                             properties: {
258                                 mode: {
259                                     enum: ["strict", "minimum"]
260                                 },
261                                 beforeColon: {
262                                     type: "boolean"
263                                 },
264                                 afterColon: {
265                                     type: "boolean"
266                                 }
267                             },
268                             additionalProperties: false
269                         },
270                         multiLine: {
271                             type: "object",
272                             properties: {
273                                 mode: {
274                                     enum: ["strict", "minimum"]
275                                 },
276                                 beforeColon: {
277                                     type: "boolean"
278                                 },
279                                 afterColon: {
280                                     type: "boolean"
281                                 }
282                             },
283                             additionalProperties: false
284                         },
285                         align: {
286                             type: "object",
287                             properties: {
288                                 mode: {
289                                     enum: ["strict", "minimum"]
290                                 },
291                                 on: {
292                                     enum: ["colon", "value"]
293                                 },
294                                 beforeColon: {
295                                     type: "boolean"
296                                 },
297                                 afterColon: {
298                                     type: "boolean"
299                                 }
300                             },
301                             additionalProperties: false
302                         }
303                     },
304                     additionalProperties: false
305                 }
306             ]
307         }],
308         messages: {
309             extraKey: "Extra space after {{computed}}key '{{key}}'.",
310             extraValue: "Extra space before value for {{computed}}key '{{key}}'.",
311             missingKey: "Missing space after {{computed}}key '{{key}}'.",
312             missingValue: "Missing space before value for {{computed}}key '{{key}}'."
313         }
314     },
315
316     create(context) {
317
318         /**
319          * OPTIONS
320          * "key-spacing": [2, {
321          *     beforeColon: false,
322          *     afterColon: true,
323          *     align: "colon" // Optional, or "value"
324          * }
325          */
326         const options = context.options[0] || {},
327             ruleOptions = initOptions({}, options),
328             multiLineOptions = ruleOptions.multiLine,
329             singleLineOptions = ruleOptions.singleLine,
330             alignmentOptions = ruleOptions.align || null;
331
332         const sourceCode = context.getSourceCode();
333
334         /**
335          * Checks whether a property is a member of the property group it follows.
336          * @param {ASTNode} lastMember The last Property known to be in the group.
337          * @param {ASTNode} candidate The next Property that might be in the group.
338          * @returns {boolean} True if the candidate property is part of the group.
339          */
340         function continuesPropertyGroup(lastMember, candidate) {
341             const groupEndLine = lastMember.loc.start.line,
342                 candidateStartLine = candidate.loc.start.line;
343
344             if (candidateStartLine - groupEndLine <= 1) {
345                 return true;
346             }
347
348             /*
349              * Check that the first comment is adjacent to the end of the group, the
350              * last comment is adjacent to the candidate property, and that successive
351              * comments are adjacent to each other.
352              */
353             const leadingComments = sourceCode.getCommentsBefore(candidate);
354
355             if (
356                 leadingComments.length &&
357                 leadingComments[0].loc.start.line - groupEndLine <= 1 &&
358                 candidateStartLine - last(leadingComments).loc.end.line <= 1
359             ) {
360                 for (let i = 1; i < leadingComments.length; i++) {
361                     if (leadingComments[i].loc.start.line - leadingComments[i - 1].loc.end.line > 1) {
362                         return false;
363                     }
364                 }
365                 return true;
366             }
367
368             return false;
369         }
370
371         /**
372          * Determines if the given property is key-value property.
373          * @param {ASTNode} property Property node to check.
374          * @returns {boolean} Whether the property is a key-value property.
375          */
376         function isKeyValueProperty(property) {
377             return !(
378                 (property.method ||
379                 property.shorthand ||
380                 property.kind !== "init" || property.type !== "Property") // Could be "ExperimentalSpreadProperty" or "SpreadElement"
381             );
382         }
383
384         /**
385          * Starting from the given a node (a property.key node here) looks forward
386          * until it finds the last token before a colon punctuator and returns it.
387          * @param {ASTNode} node The node to start looking from.
388          * @returns {ASTNode} The last token before a colon punctuator.
389          */
390         function getLastTokenBeforeColon(node) {
391             const colonToken = sourceCode.getTokenAfter(node, astUtils.isColonToken);
392
393             return sourceCode.getTokenBefore(colonToken);
394         }
395
396         /**
397          * Starting from the given a node (a property.key node here) looks forward
398          * until it finds the colon punctuator and returns it.
399          * @param {ASTNode} node The node to start looking from.
400          * @returns {ASTNode} The colon punctuator.
401          */
402         function getNextColon(node) {
403             return sourceCode.getTokenAfter(node, astUtils.isColonToken);
404         }
405
406         /**
407          * Gets an object literal property's key as the identifier name or string value.
408          * @param {ASTNode} property Property node whose key to retrieve.
409          * @returns {string} The property's key.
410          */
411         function getKey(property) {
412             const key = property.key;
413
414             if (property.computed) {
415                 return sourceCode.getText().slice(key.range[0], key.range[1]);
416             }
417             return astUtils.getStaticPropertyName(property);
418         }
419
420         /**
421          * Reports an appropriately-formatted error if spacing is incorrect on one
422          * side of the colon.
423          * @param {ASTNode} property Key-value pair in an object literal.
424          * @param {string} side Side being verified - either "key" or "value".
425          * @param {string} whitespace Actual whitespace string.
426          * @param {int} expected Expected whitespace length.
427          * @param {string} mode Value of the mode as "strict" or "minimum"
428          * @returns {void}
429          */
430         function report(property, side, whitespace, expected, mode) {
431             const diff = whitespace.length - expected,
432                 nextColon = getNextColon(property.key),
433                 tokenBeforeColon = sourceCode.getTokenBefore(nextColon, { includeComments: true }),
434                 tokenAfterColon = sourceCode.getTokenAfter(nextColon, { includeComments: true }),
435                 isKeySide = side === "key",
436                 isExtra = diff > 0,
437                 diffAbs = Math.abs(diff),
438                 spaces = Array(diffAbs + 1).join(" ");
439
440             const locStart = isKeySide ? tokenBeforeColon.loc.end : nextColon.loc.start;
441             const locEnd = isKeySide ? nextColon.loc.start : tokenAfterColon.loc.start;
442             const missingLoc = isKeySide ? tokenBeforeColon.loc : tokenAfterColon.loc;
443             const loc = isExtra ? { start: locStart, end: locEnd } : missingLoc;
444
445             if ((
446                 diff && mode === "strict" ||
447                 diff < 0 && mode === "minimum" ||
448                 diff > 0 && !expected && mode === "minimum") &&
449                 !(expected && containsLineTerminator(whitespace))
450             ) {
451                 let fix;
452
453                 if (isExtra) {
454                     let range;
455
456                     // Remove whitespace
457                     if (isKeySide) {
458                         range = [tokenBeforeColon.range[1], tokenBeforeColon.range[1] + diffAbs];
459                     } else {
460                         range = [tokenAfterColon.range[0] - diffAbs, tokenAfterColon.range[0]];
461                     }
462                     fix = function(fixer) {
463                         return fixer.removeRange(range);
464                     };
465                 } else {
466
467                     // Add whitespace
468                     if (isKeySide) {
469                         fix = function(fixer) {
470                             return fixer.insertTextAfter(tokenBeforeColon, spaces);
471                         };
472                     } else {
473                         fix = function(fixer) {
474                             return fixer.insertTextBefore(tokenAfterColon, spaces);
475                         };
476                     }
477                 }
478
479                 let messageId = "";
480
481                 if (isExtra) {
482                     messageId = side === "key" ? "extraKey" : "extraValue";
483                 } else {
484                     messageId = side === "key" ? "missingKey" : "missingValue";
485                 }
486
487                 context.report({
488                     node: property[side],
489                     loc,
490                     messageId,
491                     data: {
492                         computed: property.computed ? "computed " : "",
493                         key: getKey(property)
494                     },
495                     fix
496                 });
497             }
498         }
499
500         /**
501          * Gets the number of characters in a key, including quotes around string
502          * keys and braces around computed property keys.
503          * @param {ASTNode} property Property of on object literal.
504          * @returns {int} Width of the key.
505          */
506         function getKeyWidth(property) {
507             const startToken = sourceCode.getFirstToken(property);
508             const endToken = getLastTokenBeforeColon(property.key);
509
510             return endToken.range[1] - startToken.range[0];
511         }
512
513         /**
514          * Gets the whitespace around the colon in an object literal property.
515          * @param {ASTNode} property Property node from an object literal.
516          * @returns {Object} Whitespace before and after the property's colon.
517          */
518         function getPropertyWhitespace(property) {
519             const whitespace = /(\s*):(\s*)/u.exec(sourceCode.getText().slice(
520                 property.key.range[1], property.value.range[0]
521             ));
522
523             if (whitespace) {
524                 return {
525                     beforeColon: whitespace[1],
526                     afterColon: whitespace[2]
527                 };
528             }
529             return null;
530         }
531
532         /**
533          * Creates groups of properties.
534          * @param  {ASTNode} node ObjectExpression node being evaluated.
535          * @returns {Array.<ASTNode[]>} Groups of property AST node lists.
536          */
537         function createGroups(node) {
538             if (node.properties.length === 1) {
539                 return [node.properties];
540             }
541
542             return node.properties.reduce((groups, property) => {
543                 const currentGroup = last(groups),
544                     prev = last(currentGroup);
545
546                 if (!prev || continuesPropertyGroup(prev, property)) {
547                     currentGroup.push(property);
548                 } else {
549                     groups.push([property]);
550                 }
551
552                 return groups;
553             }, [
554                 []
555             ]);
556         }
557
558         /**
559          * Verifies correct vertical alignment of a group of properties.
560          * @param {ASTNode[]} properties List of Property AST nodes.
561          * @returns {void}
562          */
563         function verifyGroupAlignment(properties) {
564             const length = properties.length,
565                 widths = properties.map(getKeyWidth), // Width of keys, including quotes
566                 align = alignmentOptions.on; // "value" or "colon"
567             let targetWidth = Math.max(...widths),
568                 beforeColon, afterColon, mode;
569
570             if (alignmentOptions && length > 1) { // When aligning values within a group, use the alignment configuration.
571                 beforeColon = alignmentOptions.beforeColon;
572                 afterColon = alignmentOptions.afterColon;
573                 mode = alignmentOptions.mode;
574             } else {
575                 beforeColon = multiLineOptions.beforeColon;
576                 afterColon = multiLineOptions.afterColon;
577                 mode = alignmentOptions.mode;
578             }
579
580             // Conditionally include one space before or after colon
581             targetWidth += (align === "colon" ? beforeColon : afterColon);
582
583             for (let i = 0; i < length; i++) {
584                 const property = properties[i];
585                 const whitespace = getPropertyWhitespace(property);
586
587                 if (whitespace) { // Object literal getters/setters lack a colon
588                     const width = widths[i];
589
590                     if (align === "value") {
591                         report(property, "key", whitespace.beforeColon, beforeColon, mode);
592                         report(property, "value", whitespace.afterColon, targetWidth - width, mode);
593                     } else { // align = "colon"
594                         report(property, "key", whitespace.beforeColon, targetWidth - width, mode);
595                         report(property, "value", whitespace.afterColon, afterColon, mode);
596                     }
597                 }
598             }
599         }
600
601         /**
602          * Verifies spacing of property conforms to specified options.
603          * @param  {ASTNode} node Property node being evaluated.
604          * @param {Object} lineOptions Configured singleLine or multiLine options
605          * @returns {void}
606          */
607         function verifySpacing(node, lineOptions) {
608             const actual = getPropertyWhitespace(node);
609
610             if (actual) { // Object literal getters/setters lack colons
611                 report(node, "key", actual.beforeColon, lineOptions.beforeColon, lineOptions.mode);
612                 report(node, "value", actual.afterColon, lineOptions.afterColon, lineOptions.mode);
613             }
614         }
615
616         /**
617          * Verifies spacing of each property in a list.
618          * @param {ASTNode[]} properties List of Property AST nodes.
619          * @param {Object} lineOptions Configured singleLine or multiLine options
620          * @returns {void}
621          */
622         function verifyListSpacing(properties, lineOptions) {
623             const length = properties.length;
624
625             for (let i = 0; i < length; i++) {
626                 verifySpacing(properties[i], lineOptions);
627             }
628         }
629
630         /**
631          * Verifies vertical alignment, taking into account groups of properties.
632          * @param  {ASTNode} node ObjectExpression node being evaluated.
633          * @returns {void}
634          */
635         function verifyAlignment(node) {
636             createGroups(node).forEach(group => {
637                 const properties = group.filter(isKeyValueProperty);
638
639                 if (properties.length > 0 && isSingleLineProperties(properties)) {
640                     verifyListSpacing(properties, multiLineOptions);
641                 } else {
642                     verifyGroupAlignment(properties);
643                 }
644             });
645         }
646
647         //--------------------------------------------------------------------------
648         // Public API
649         //--------------------------------------------------------------------------
650
651         if (alignmentOptions) { // Verify vertical alignment
652
653             return {
654                 ObjectExpression(node) {
655                     if (isSingleLine(node)) {
656                         verifyListSpacing(node.properties.filter(isKeyValueProperty), singleLineOptions);
657                     } else {
658                         verifyAlignment(node);
659                     }
660                 }
661             };
662
663         }
664
665         // Obey beforeColon and afterColon in each property as configured
666         return {
667             Property(node) {
668                 verifySpacing(node, isSingleLine(node.parent) ? singleLineOptions : multiLineOptions);
669             }
670         };
671
672
673     }
674 };