Umama-at-Bluchip/Quick-UI
0
1/**2 * @fileoverview Rule to specify spacing of object literal keys and values3 * @author Brandon Mills4 * @deprecated in ESLint v8.53.05 */6"use strict";7 8//------------------------------------------------------------------------------9// Requirements10//------------------------------------------------------------------------------11 12const astUtils = require("./utils/ast-utils");13const { getGraphemeCount } = require("../shared/string-utils");14 15/**16 * Checks whether a string contains a line terminator as defined in17 * http://www.ecma-international.org/ecma-262/5.1/#sec-7.318 * @param {string} str String to test.19 * @returns {boolean} True if str contains a line terminator.20 */21function containsLineTerminator(str) {22 return astUtils.LINEBREAK_MATCHER.test(str);23}24 25/**26 * Gets the last element of an array.27 * @param {Array} arr An array.28 * @returns {any} Last element of arr.29 */30function last(arr) {31 return arr[arr.length - 1];32}33 34/**35 * Checks whether a node is contained on a single line.36 * @param {ASTNode} node AST Node being evaluated.37 * @returns {boolean} True if the node is a single line.38 */39function isSingleLine(node) {40 return (node.loc.end.line === node.loc.start.line);41}42 43/**44 * Checks whether the properties on a single line.45 * @param {ASTNode[]} properties List of Property AST nodes.46 * @returns {boolean} True if all properties is on a single line.47 */48function isSingleLineProperties(properties) {49 const [firstProp] = properties,50 lastProp = last(properties);51 52 return firstProp.loc.start.line === lastProp.loc.end.line;53}54 55/**56 * Initializes a single option property from the configuration with defaults for undefined values57 * @param {Object} toOptions Object to be initialized58 * @param {Object} fromOptions Object to be initialized from59 * @returns {Object} The object with correctly initialized options and values60 */61function initOptionProperty(toOptions, fromOptions) {62 toOptions.mode = fromOptions.mode || "strict";63 64 // Set value of beforeColon65 if (typeof fromOptions.beforeColon !== "undefined") {66 toOptions.beforeColon = +fromOptions.beforeColon;67 } else {68 toOptions.beforeColon = 0;69 }70 71 // Set value of afterColon72 if (typeof fromOptions.afterColon !== "undefined") {73 toOptions.afterColon = +fromOptions.afterColon;74 } else {75 toOptions.afterColon = 1;76 }77 78 // Set align if exists79 if (typeof fromOptions.align !== "undefined") {80 if (typeof fromOptions.align === "object") {81 toOptions.align = fromOptions.align;82 } else { // "string"83 toOptions.align = {84 on: fromOptions.align,85 mode: toOptions.mode,86 beforeColon: toOptions.beforeColon,87 afterColon: toOptions.afterColon88 };89 }90 }91 92 return toOptions;93}94 95/**96 * Initializes all the option values (singleLine, multiLine and align) from the configuration with defaults for undefined values97 * @param {Object} toOptions Object to be initialized98 * @param {Object} fromOptions Object to be initialized from99 * @returns {Object} The object with correctly initialized options and values100 */101function initOptions(toOptions, fromOptions) {102 if (typeof fromOptions.align === "object") {103 104 // Initialize the alignment configuration105 toOptions.align = initOptionProperty({}, fromOptions.align);106 toOptions.align.on = fromOptions.align.on || "colon";107 toOptions.align.mode = fromOptions.align.mode || "strict";108 109 toOptions.multiLine = initOptionProperty({}, (fromOptions.multiLine || fromOptions));110 toOptions.singleLine = initOptionProperty({}, (fromOptions.singleLine || fromOptions));111 112 } else { // string or undefined113 toOptions.multiLine = initOptionProperty({}, (fromOptions.multiLine || fromOptions));114 toOptions.singleLine = initOptionProperty({}, (fromOptions.singleLine || fromOptions));115 116 // If alignment options are defined in multiLine, pull them out into the general align configuration117 if (toOptions.multiLine.align) {118 toOptions.align = {119 on: toOptions.multiLine.align.on,120 mode: toOptions.multiLine.align.mode || toOptions.multiLine.mode,121 beforeColon: toOptions.multiLine.align.beforeColon,122 afterColon: toOptions.multiLine.align.afterColon123 };124 }125 }126 127 return toOptions;128}129 130//------------------------------------------------------------------------------131// Rule Definition132//------------------------------------------------------------------------------133 134/** @type {import('../shared/types').Rule} */135module.exports = {136 meta: {137 deprecated: true,138 replacedBy: [],139 type: "layout",140 141 docs: {142 description: "Enforce consistent spacing between keys and values in object literal properties",143 recommended: false,144 url: "https://eslint.org/docs/latest/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: false176 }177 ]178 },179 mode: {180 enum: ["strict", "minimum"]181 },182 beforeColon: {183 type: "boolean"184 },185 afterColon: {186 type: "boolean"187 }188 },189 additionalProperties: false190 },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: false208 },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: false234 }235 ]236 },237 mode: {238 enum: ["strict", "minimum"]239 },240 beforeColon: {241 type: "boolean"242 },243 afterColon: {244 type: "boolean"245 }246 },247 additionalProperties: false248 }249 },250 additionalProperties: false251 },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: false269 },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: false284 },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: false302 }303 },304 additionalProperties: false305 }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 * OPTIONS320 * "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.sourceCode;333 334 /**335 * Determines if the given property is key-value property.336 * @param {ASTNode} property Property node to check.337 * @returns {boolean} Whether the property is a key-value property.338 */339 function isKeyValueProperty(property) {340 return !(341 (property.method ||342 property.shorthand ||343 property.kind !== "init" || property.type !== "Property") // Could be "ExperimentalSpreadProperty" or "SpreadElement"344 );345 }346 347 /**348 * Starting from the given node (a property.key node here) looks forward349 * until it finds the colon punctuator and returns it.350 * @param {ASTNode} node The node to start looking from.351 * @returns {ASTNode} The colon punctuator.352 */353 function getNextColon(node) {354 return sourceCode.getTokenAfter(node, astUtils.isColonToken);355 }356 357 /**358 * Starting from the given node (a property.key node here) looks forward359 * until it finds the last token before a colon punctuator and returns it.360 * @param {ASTNode} node The node to start looking from.361 * @returns {ASTNode} The last token before a colon punctuator.362 */363 function getLastTokenBeforeColon(node) {364 const colonToken = getNextColon(node);365 366 return sourceCode.getTokenBefore(colonToken);367 }368 369 /**370 * Starting from the given node (a property.key node here) looks forward371 * until it finds the first token after a colon punctuator and returns it.372 * @param {ASTNode} node The node to start looking from.373 * @returns {ASTNode} The first token after a colon punctuator.374 */375 function getFirstTokenAfterColon(node) {376 const colonToken = getNextColon(node);377 378 return sourceCode.getTokenAfter(colonToken);379 }380 381 /**382 * Checks whether a property is a member of the property group it follows.383 * @param {ASTNode} lastMember The last Property known to be in the group.384 * @param {ASTNode} candidate The next Property that might be in the group.385 * @returns {boolean} True if the candidate property is part of the group.386 */387 function continuesPropertyGroup(lastMember, candidate) {388 const groupEndLine = lastMember.loc.start.line,389 candidateValueStartLine = (isKeyValueProperty(candidate) ? getFirstTokenAfterColon(candidate.key) : candidate).loc.start.line;390 391 if (candidateValueStartLine - groupEndLine <= 1) {392 return true;393 }394 395 /*396 * Check that the first comment is adjacent to the end of the group, the397 * last comment is adjacent to the candidate property, and that successive398 * comments are adjacent to each other.399 */400 const leadingComments = sourceCode.getCommentsBefore(candidate);401 402 if (403 leadingComments.length &&404 leadingComments[0].loc.start.line - groupEndLine <= 1 &&405 candidateValueStartLine - last(leadingComments).loc.end.line <= 1406 ) {407 for (let i = 1; i < leadingComments.length; i++) {408 if (leadingComments[i].loc.start.line - leadingComments[i - 1].loc.end.line > 1) {409 return false;410 }411 }412 return true;413 }414 415 return false;416 }417 418 /**419 * Gets an object literal property's key as the identifier name or string value.420 * @param {ASTNode} property Property node whose key to retrieve.421 * @returns {string} The property's key.422 */423 function getKey(property) {424 const key = property.key;425 426 if (property.computed) {427 return sourceCode.getText().slice(key.range[0], key.range[1]);428 }429 return astUtils.getStaticPropertyName(property);430 }431 432 /**433 * Reports an appropriately-formatted error if spacing is incorrect on one434 * side of the colon.435 * @param {ASTNode} property Key-value pair in an object literal.436 * @param {string} side Side being verified - either "key" or "value".437 * @param {string} whitespace Actual whitespace string.438 * @param {int} expected Expected whitespace length.439 * @param {string} mode Value of the mode as "strict" or "minimum"440 * @returns {void}441 */442 function report(property, side, whitespace, expected, mode) {443 const diff = whitespace.length - expected;444 445 if ((446 diff && mode === "strict" ||447 diff < 0 && mode === "minimum" ||448 diff > 0 && !expected && mode === "minimum") &&449 !(expected && containsLineTerminator(whitespace))450 ) {451 const nextColon = getNextColon(property.key),452 tokenBeforeColon = sourceCode.getTokenBefore(nextColon, { includeComments: true }),453 tokenAfterColon = sourceCode.getTokenAfter(nextColon, { includeComments: true }),454 isKeySide = side === "key",455 isExtra = diff > 0,456 diffAbs = Math.abs(diff),457 spaces = Array(diffAbs + 1).join(" ");458 459 const locStart = isKeySide ? tokenBeforeColon.loc.end : nextColon.loc.start;460 const locEnd = isKeySide ? nextColon.loc.start : tokenAfterColon.loc.start;461 const missingLoc = isKeySide ? tokenBeforeColon.loc : tokenAfterColon.loc;462 const loc = isExtra ? { start: locStart, end: locEnd } : missingLoc;463 464 let fix;465 466 if (isExtra) {467 let range;468 469 // Remove whitespace470 if (isKeySide) {471 range = [tokenBeforeColon.range[1], tokenBeforeColon.range[1] + diffAbs];472 } else {473 range = [tokenAfterColon.range[0] - diffAbs, tokenAfterColon.range[0]];474 }475 fix = function(fixer) {476 return fixer.removeRange(range);477 };478 } else {479 480 // Add whitespace481 if (isKeySide) {482 fix = function(fixer) {483 return fixer.insertTextAfter(tokenBeforeColon, spaces);484 };485 } else {486 fix = function(fixer) {487 return fixer.insertTextBefore(tokenAfterColon, spaces);488 };489 }490 }491 492 let messageId = "";493 494 if (isExtra) {495 messageId = side === "key" ? "extraKey" : "extraValue";496 } else {497 messageId = side === "key" ? "missingKey" : "missingValue";498 }499 500 context.report({501 node: property[side],502 loc,503 messageId,504 data: {505 computed: property.computed ? "computed " : "",506 key: getKey(property)507 },508 fix509 });510 }511 }512 513 /**514 * Gets the number of characters in a key, including quotes around string515 * keys and braces around computed property keys.516 * @param {ASTNode} property Property of on object literal.517 * @returns {int} Width of the key.518 */519 function getKeyWidth(property) {520 const startToken = sourceCode.getFirstToken(property);521 const endToken = getLastTokenBeforeColon(property.key);522 523 return getGraphemeCount(sourceCode.getText().slice(startToken.range[0], endToken.range[1]));524 }525 526 /**527 * Gets the whitespace around the colon in an object literal property.528 * @param {ASTNode} property Property node from an object literal.529 * @returns {Object} Whitespace before and after the property's colon.530 */531 function getPropertyWhitespace(property) {532 const whitespace = /(\s*):(\s*)/u.exec(sourceCode.getText().slice(533 property.key.range[1], property.value.range[0]534 ));535 536 if (whitespace) {537 return {538 beforeColon: whitespace[1],539 afterColon: whitespace[2]540 };541 }542 return null;543 }544 545 /**546 * Creates groups of properties.547 * @param {ASTNode} node ObjectExpression node being evaluated.548 * @returns {Array<ASTNode[]>} Groups of property AST node lists.549 */550 function createGroups(node) {551 if (node.properties.length === 1) {552 return [node.properties];553 }554 555 return node.properties.reduce((groups, property) => {556 const currentGroup = last(groups),557 prev = last(currentGroup);558 559 if (!prev || continuesPropertyGroup(prev, property)) {560 currentGroup.push(property);561 } else {562 groups.push([property]);563 }564 565 return groups;566 }, [567 []568 ]);569 }570 571 /**572 * Verifies correct vertical alignment of a group of properties.573 * @param {ASTNode[]} properties List of Property AST nodes.574 * @returns {void}575 */576 function verifyGroupAlignment(properties) {577 const length = properties.length,578 widths = properties.map(getKeyWidth), // Width of keys, including quotes579 align = alignmentOptions.on; // "value" or "colon"580 let targetWidth = Math.max(...widths),581 beforeColon, afterColon, mode;582 583 if (alignmentOptions && length > 1) { // When aligning values within a group, use the alignment configuration.584 beforeColon = alignmentOptions.beforeColon;585 afterColon = alignmentOptions.afterColon;586 mode = alignmentOptions.mode;587 } else {588 beforeColon = multiLineOptions.beforeColon;589 afterColon = multiLineOptions.afterColon;590 mode = alignmentOptions.mode;591 }592 593 // Conditionally include one space before or after colon594 targetWidth += (align === "colon" ? beforeColon : afterColon);595 596 for (let i = 0; i < length; i++) {597 const property = properties[i];598 const whitespace = getPropertyWhitespace(property);599 600 if (whitespace) { // Object literal getters/setters lack a colon601 const width = widths[i];602 603 if (align === "value") {604 report(property, "key", whitespace.beforeColon, beforeColon, mode);605 report(property, "value", whitespace.afterColon, targetWidth - width, mode);606 } else { // align = "colon"607 report(property, "key", whitespace.beforeColon, targetWidth - width, mode);608 report(property, "value", whitespace.afterColon, afterColon, mode);609 }610 }611 }612 }613 614 /**615 * Verifies spacing of property conforms to specified options.616 * @param {ASTNode} node Property node being evaluated.617 * @param {Object} lineOptions Configured singleLine or multiLine options618 * @returns {void}619 */620 function verifySpacing(node, lineOptions) {621 const actual = getPropertyWhitespace(node);622 623 if (actual) { // Object literal getters/setters lack colons624 report(node, "key", actual.beforeColon, lineOptions.beforeColon, lineOptions.mode);625 report(node, "value", actual.afterColon, lineOptions.afterColon, lineOptions.mode);626 }627 }628 629 /**630 * Verifies spacing of each property in a list.631 * @param {ASTNode[]} properties List of Property AST nodes.632 * @param {Object} lineOptions Configured singleLine or multiLine options633 * @returns {void}634 */635 function verifyListSpacing(properties, lineOptions) {636 const length = properties.length;637 638 for (let i = 0; i < length; i++) {639 verifySpacing(properties[i], lineOptions);640 }641 }642 643 /**644 * Verifies vertical alignment, taking into account groups of properties.645 * @param {ASTNode} node ObjectExpression node being evaluated.646 * @returns {void}647 */648 function verifyAlignment(node) {649 createGroups(node).forEach(group => {650 const properties = group.filter(isKeyValueProperty);651 652 if (properties.length > 0 && isSingleLineProperties(properties)) {653 verifyListSpacing(properties, multiLineOptions);654 } else {655 verifyGroupAlignment(properties);656 }657 });658 }659 660 //--------------------------------------------------------------------------661 // Public API662 //--------------------------------------------------------------------------663 664 if (alignmentOptions) { // Verify vertical alignment665 666 return {667 ObjectExpression(node) {668 if (isSingleLine(node)) {669 verifyListSpacing(node.properties.filter(isKeyValueProperty), singleLineOptions);670 } else {671 verifyAlignment(node);672 }673 }674 };675 676 }677 678 // Obey beforeColon and afterColon in each property as configured679 return {680 Property(node) {681 verifySpacing(node, isSingleLine(node.parent) ? singleLineOptions : multiLineOptions);682 }683 };684 685 686 }687};688 