minor adjustment to readme
[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 properies 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
418             return property.key.name || property.key.value;
419         }
420
421         /**
422          * Reports an appropriately-formatted error if spacing is incorrect on one
423          * side of the colon.
424          * @param {ASTNode} property Key-value pair in an object literal.
425          * @param {string} side Side being verified - either "key" or "value".
426          * @param {string} whitespace Actual whitespace string.
427          * @param {int} expected Expected whitespace length.
428          * @param {string} mode Value of the mode as "strict" or "minimum"
429          * @returns {void}
430          */
431         function report(property, side, whitespace, expected, mode) {
432             const diff = whitespace.length - expected,
433                 nextColon = getNextColon(property.key),
434                 tokenBeforeColon = sourceCode.getTokenBefore(nextColon, { includeComments: true }),
435                 tokenAfterColon = sourceCode.getTokenAfter(nextColon, { includeComments: true }),
436                 isKeySide = side === "key",
437                 locStart = isKeySide ? tokenBeforeColon.loc.start : tokenAfterColon.loc.start,
438                 isExtra = diff > 0,
439                 diffAbs = Math.abs(diff),
440                 spaces = Array(diffAbs + 1).join(" ");
441
442             if ((
443                 diff && mode === "strict" ||
444                 diff < 0 && mode === "minimum" ||
445                 diff > 0 && !expected && mode === "minimum") &&
446                 !(expected && containsLineTerminator(whitespace))
447             ) {
448                 let fix;
449
450                 if (isExtra) {
451                     let range;
452
453                     // Remove whitespace
454                     if (isKeySide) {
455                         range = [tokenBeforeColon.range[1], tokenBeforeColon.range[1] + diffAbs];
456                     } else {
457                         range = [tokenAfterColon.range[0] - diffAbs, tokenAfterColon.range[0]];
458                     }
459                     fix = function(fixer) {
460                         return fixer.removeRange(range);
461                     };
462                 } else {
463
464                     // Add whitespace
465                     if (isKeySide) {
466                         fix = function(fixer) {
467                             return fixer.insertTextAfter(tokenBeforeColon, spaces);
468                         };
469                     } else {
470                         fix = function(fixer) {
471                             return fixer.insertTextBefore(tokenAfterColon, spaces);
472                         };
473                     }
474                 }
475
476                 let messageId = "";
477
478                 if (isExtra) {
479                     messageId = side === "key" ? "extraKey" : "extraValue";
480                 } else {
481                     messageId = side === "key" ? "missingKey" : "missingValue";
482                 }
483
484                 context.report({
485                     node: property[side],
486                     loc: locStart,
487                     messageId,
488                     data: {
489                         computed: property.computed ? "computed " : "",
490                         key: getKey(property)
491                     },
492                     fix
493                 });
494             }
495         }
496
497         /**
498          * Gets the number of characters in a key, including quotes around string
499          * keys and braces around computed property keys.
500          * @param {ASTNode} property Property of on object literal.
501          * @returns {int} Width of the key.
502          */
503         function getKeyWidth(property) {
504             const startToken = sourceCode.getFirstToken(property);
505             const endToken = getLastTokenBeforeColon(property.key);
506
507             return endToken.range[1] - startToken.range[0];
508         }
509
510         /**
511          * Gets the whitespace around the colon in an object literal property.
512          * @param {ASTNode} property Property node from an object literal.
513          * @returns {Object} Whitespace before and after the property's colon.
514          */
515         function getPropertyWhitespace(property) {
516             const whitespace = /(\s*):(\s*)/u.exec(sourceCode.getText().slice(
517                 property.key.range[1], property.value.range[0]
518             ));
519
520             if (whitespace) {
521                 return {
522                     beforeColon: whitespace[1],
523                     afterColon: whitespace[2]
524                 };
525             }
526             return null;
527         }
528
529         /**
530          * Creates groups of properties.
531          * @param  {ASTNode} node ObjectExpression node being evaluated.
532          * @returns {Array.<ASTNode[]>} Groups of property AST node lists.
533          */
534         function createGroups(node) {
535             if (node.properties.length === 1) {
536                 return [node.properties];
537             }
538
539             return node.properties.reduce((groups, property) => {
540                 const currentGroup = last(groups),
541                     prev = last(currentGroup);
542
543                 if (!prev || continuesPropertyGroup(prev, property)) {
544                     currentGroup.push(property);
545                 } else {
546                     groups.push([property]);
547                 }
548
549                 return groups;
550             }, [
551                 []
552             ]);
553         }
554
555         /**
556          * Verifies correct vertical alignment of a group of properties.
557          * @param {ASTNode[]} properties List of Property AST nodes.
558          * @returns {void}
559          */
560         function verifyGroupAlignment(properties) {
561             const length = properties.length,
562                 widths = properties.map(getKeyWidth), // Width of keys, including quotes
563                 align = alignmentOptions.on; // "value" or "colon"
564             let targetWidth = Math.max(...widths),
565                 beforeColon, afterColon, mode;
566
567             if (alignmentOptions && length > 1) { // When aligning values within a group, use the alignment configuration.
568                 beforeColon = alignmentOptions.beforeColon;
569                 afterColon = alignmentOptions.afterColon;
570                 mode = alignmentOptions.mode;
571             } else {
572                 beforeColon = multiLineOptions.beforeColon;
573                 afterColon = multiLineOptions.afterColon;
574                 mode = alignmentOptions.mode;
575             }
576
577             // Conditionally include one space before or after colon
578             targetWidth += (align === "colon" ? beforeColon : afterColon);
579
580             for (let i = 0; i < length; i++) {
581                 const property = properties[i];
582                 const whitespace = getPropertyWhitespace(property);
583
584                 if (whitespace) { // Object literal getters/setters lack a colon
585                     const width = widths[i];
586
587                     if (align === "value") {
588                         report(property, "key", whitespace.beforeColon, beforeColon, mode);
589                         report(property, "value", whitespace.afterColon, targetWidth - width, mode);
590                     } else { // align = "colon"
591                         report(property, "key", whitespace.beforeColon, targetWidth - width, mode);
592                         report(property, "value", whitespace.afterColon, afterColon, mode);
593                     }
594                 }
595             }
596         }
597
598         /**
599          * Verifies spacing of property conforms to specified options.
600          * @param  {ASTNode} node Property node being evaluated.
601          * @param {Object} lineOptions Configured singleLine or multiLine options
602          * @returns {void}
603          */
604         function verifySpacing(node, lineOptions) {
605             const actual = getPropertyWhitespace(node);
606
607             if (actual) { // Object literal getters/setters lack colons
608                 report(node, "key", actual.beforeColon, lineOptions.beforeColon, lineOptions.mode);
609                 report(node, "value", actual.afterColon, lineOptions.afterColon, lineOptions.mode);
610             }
611         }
612
613         /**
614          * Verifies spacing of each property in a list.
615          * @param {ASTNode[]} properties List of Property AST nodes.
616          * @param {Object} lineOptions Configured singleLine or multiLine options
617          * @returns {void}
618          */
619         function verifyListSpacing(properties, lineOptions) {
620             const length = properties.length;
621
622             for (let i = 0; i < length; i++) {
623                 verifySpacing(properties[i], lineOptions);
624             }
625         }
626
627         /**
628          * Verifies vertical alignment, taking into account groups of properties.
629          * @param  {ASTNode} node ObjectExpression node being evaluated.
630          * @returns {void}
631          */
632         function verifyAlignment(node) {
633             createGroups(node).forEach(group => {
634                 const properties = group.filter(isKeyValueProperty);
635
636                 if (properties.length > 0 && isSingleLineProperties(properties)) {
637                     verifyListSpacing(properties, multiLineOptions);
638                 } else {
639                     verifyGroupAlignment(properties);
640                 }
641             });
642         }
643
644         //--------------------------------------------------------------------------
645         // Public API
646         //--------------------------------------------------------------------------
647
648         if (alignmentOptions) { // Verify vertical alignment
649
650             return {
651                 ObjectExpression(node) {
652                     if (isSingleLine(node)) {
653                         verifyListSpacing(node.properties.filter(isKeyValueProperty), singleLineOptions);
654                     } else {
655                         verifyAlignment(node);
656                     }
657                 }
658             };
659
660         }
661
662         // Obey beforeColon and afterColon in each property as configured
663         return {
664             Property(node) {
665                 verifySpacing(node, isSingleLine(node.parent) ? singleLineOptions : multiLineOptions);
666             }
667         };
668
669
670     }
671 };