mirror of https://github.com/lukechilds/node.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
325 lines
10 KiB
325 lines
10 KiB
/**
|
|
* @fileoverview Rule to specify spacing of object literal keys and values
|
|
* @author Brandon Mills
|
|
* @copyright 2014 Brandon Mills. All rights reserved.
|
|
*/
|
|
"use strict";
|
|
|
|
//------------------------------------------------------------------------------
|
|
// Helpers
|
|
//------------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Checks whether a string contains a line terminator as defined in
|
|
* http://www.ecma-international.org/ecma-262/5.1/#sec-7.3
|
|
* @param {string} str String to test.
|
|
* @returns {boolean} True if str contains a line terminator.
|
|
*/
|
|
function containsLineTerminator(str) {
|
|
return /[\n\r\u2028\u2029]/.test(str);
|
|
}
|
|
|
|
/**
|
|
* Gets the last element of an array.
|
|
* @param {Array} arr An array.
|
|
* @returns {any} Last element of arr.
|
|
*/
|
|
function last(arr) {
|
|
return arr[arr.length - 1];
|
|
}
|
|
|
|
/**
|
|
* Checks whether a property is a member of the property group it follows.
|
|
* @param {ASTNode} lastMember The last Property known to be in the group.
|
|
* @param {ASTNode} candidate The next Property that might be in the group.
|
|
* @returns {boolean} True if the candidate property is part of the group.
|
|
*/
|
|
function continuesPropertyGroup(lastMember, candidate) {
|
|
var groupEndLine = lastMember.loc.start.line,
|
|
candidateStartLine = candidate.loc.start.line,
|
|
comments, i;
|
|
|
|
if (candidateStartLine - groupEndLine <= 1) {
|
|
return true;
|
|
}
|
|
|
|
// Check that the first comment is adjacent to the end of the group, the
|
|
// last comment is adjacent to the candidate property, and that successive
|
|
// comments are adjacent to each other.
|
|
comments = candidate.leadingComments;
|
|
if (
|
|
comments &&
|
|
comments[0].loc.start.line - groupEndLine <= 1 &&
|
|
candidateStartLine - last(comments).loc.end.line <= 1
|
|
) {
|
|
for (i = 1; i < comments.length; i++) {
|
|
if (comments[i].loc.start.line - comments[i - 1].loc.end.line > 1) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Checks whether a node is contained on a single line.
|
|
* @param {ASTNode} node AST Node being evaluated.
|
|
* @returns {boolean} True if the node is a single line.
|
|
*/
|
|
function isSingleLine(node) {
|
|
return (node.loc.end.line === node.loc.start.line);
|
|
}
|
|
|
|
//------------------------------------------------------------------------------
|
|
// Rule Definition
|
|
//------------------------------------------------------------------------------
|
|
|
|
var messages = {
|
|
key: "{{error}} space after {{computed}}key \"{{key}}\".",
|
|
value: "{{error}} space before value for {{computed}}key \"{{key}}\"."
|
|
};
|
|
|
|
module.exports = function(context) {
|
|
|
|
/**
|
|
* OPTIONS
|
|
* "key-spacing": [2, {
|
|
* beforeColon: false,
|
|
* afterColon: true,
|
|
* align: "colon" // Optional, or "value"
|
|
* }
|
|
*/
|
|
|
|
var options = context.options[0] || {},
|
|
align = options.align,
|
|
beforeColon = +!!options.beforeColon, // Defaults to false
|
|
afterColon = +!(options.afterColon === false); // Defaults to true
|
|
|
|
/**
|
|
* Gets an object literal property's key as the identifier name or string value.
|
|
* @param {ASTNode} property Property node whose key to retrieve.
|
|
* @returns {string} The property's key.
|
|
*/
|
|
function getKey(property) {
|
|
var key = property.key;
|
|
|
|
if (property.computed) {
|
|
return context.getSource().slice(key.range[0], key.range[1]);
|
|
}
|
|
|
|
return property.key.name || property.key.value;
|
|
}
|
|
|
|
/**
|
|
* Reports an appropriately-formatted error if spacing is incorrect on one
|
|
* side of the colon.
|
|
* @param {ASTNode} property Key-value pair in an object literal.
|
|
* @param {string} side Side being verified - either "key" or "value".
|
|
* @param {string} whitespace Actual whitespace string.
|
|
* @param {int} expected Expected whitespace length.
|
|
* @returns {void}
|
|
*/
|
|
function report(property, side, whitespace, expected) {
|
|
var diff = whitespace.length - expected,
|
|
key = property.key,
|
|
firstTokenAfterColon = context.getTokenAfter(key, 1),
|
|
location = side === "key" ? key.loc.start : firstTokenAfterColon.loc.start;
|
|
|
|
if (diff && !(expected && containsLineTerminator(whitespace))) {
|
|
context.report(property[side], location, messages[side], {
|
|
error: diff > 0 ? "Extra" : "Missing",
|
|
computed: property.computed ? "computed " : "",
|
|
key: getKey(property)
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the number of characters in a key, including quotes around string
|
|
* keys and braces around computed property keys.
|
|
* @param {ASTNode} property Property of on object literal.
|
|
* @returns {int} Width of the key.
|
|
*/
|
|
function getKeyWidth(property) {
|
|
var key = property.key,
|
|
startToken, endToken;
|
|
|
|
// [computed]: value
|
|
if (property.computed) {
|
|
startToken = context.getTokenBefore(key);
|
|
endToken = context.getTokenAfter(key);
|
|
return endToken.range[1] - startToken.range[0];
|
|
}
|
|
|
|
// name: value
|
|
if (key.type === "Identifier") {
|
|
return key.name.length;
|
|
}
|
|
|
|
// "literal": value
|
|
// 42: value
|
|
if (key.type === "Literal") {
|
|
return key.raw.length;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the whitespace around the colon in an object literal property.
|
|
* @param {ASTNode} property Property node from an object literal.
|
|
* @returns {Object} Whitespace before and after the property's colon.
|
|
*/
|
|
function getPropertyWhitespace(property) {
|
|
var whitespace = /(\s*):(\s*)/.exec(context.getSource().slice(
|
|
property.key.range[1], property.value.range[0]
|
|
));
|
|
|
|
if (whitespace) {
|
|
return {
|
|
beforeColon: whitespace[1],
|
|
afterColon: whitespace[2]
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates groups of properties.
|
|
* @param {ASTNode} node ObjectExpression node being evaluated.
|
|
* @returns {Array.<ASTNode[]>} Groups of property AST node lists.
|
|
*/
|
|
function createGroups(node) {
|
|
if (node.properties.length === 1) {
|
|
return [node.properties];
|
|
}
|
|
|
|
return node.properties.reduce(function(groups, property) {
|
|
var currentGroup = last(groups),
|
|
prev = last(currentGroup);
|
|
|
|
if (!prev || continuesPropertyGroup(prev, property)) {
|
|
currentGroup.push(property);
|
|
} else {
|
|
groups.push([property]);
|
|
}
|
|
|
|
return groups;
|
|
}, [[]]);
|
|
}
|
|
|
|
/**
|
|
* Verifies correct vertical alignment of a group of properties.
|
|
* @param {ASTNode[]} properties List of Property AST nodes.
|
|
* @returns {void}
|
|
*/
|
|
function verifyGroupAlignment(properties) {
|
|
var length = properties.length,
|
|
widths = properties.map(getKeyWidth), // Width of keys, including quotes
|
|
targetWidth = Math.max.apply(null, widths),
|
|
i, property, whitespace, width;
|
|
|
|
// Conditionally include one space before or after colon
|
|
targetWidth += (align === "colon" ? beforeColon : afterColon);
|
|
|
|
for (i = 0; i < length; i++) {
|
|
property = properties[i];
|
|
whitespace = getPropertyWhitespace(property);
|
|
|
|
if (!whitespace) {
|
|
continue; // Object literal getters/setters lack a colon
|
|
}
|
|
|
|
width = widths[i];
|
|
|
|
if (align === "value") {
|
|
report(property, "key", whitespace.beforeColon, beforeColon);
|
|
report(property, "value", whitespace.afterColon, targetWidth - width);
|
|
} else { // align = "colon"
|
|
report(property, "key", whitespace.beforeColon, targetWidth - width);
|
|
report(property, "value", whitespace.afterColon, afterColon);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Verifies vertical alignment, taking into account groups of properties.
|
|
* @param {ASTNode} node ObjectExpression node being evaluated.
|
|
* @returns {void}
|
|
*/
|
|
function verifyAlignment(node) {
|
|
createGroups(node).forEach(function(group) {
|
|
verifyGroupAlignment(group);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Verifies spacing of property conforms to specified options.
|
|
* @param {ASTNode} node Property node being evaluated.
|
|
* @returns {void}
|
|
*/
|
|
function verifySpacing(node) {
|
|
var whitespace = getPropertyWhitespace(node);
|
|
if (whitespace) { // Object literal getters/setters lack colons
|
|
report(node, "key", whitespace.beforeColon, beforeColon);
|
|
report(node, "value", whitespace.afterColon, afterColon);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Verifies spacing of each property in a list.
|
|
* @param {ASTNode[]} properties List of Property AST nodes.
|
|
* @returns {void}
|
|
*/
|
|
function verifyListSpacing(properties) {
|
|
var length = properties.length;
|
|
|
|
for (var i = 0; i < length; i++) {
|
|
verifySpacing(properties[i]);
|
|
}
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
// Public API
|
|
//--------------------------------------------------------------------------
|
|
|
|
if (align) { // Verify vertical alignment
|
|
|
|
return {
|
|
"ObjectExpression": function(node) {
|
|
if (isSingleLine(node)) {
|
|
verifyListSpacing(node.properties);
|
|
} else {
|
|
verifyAlignment(node);
|
|
}
|
|
}
|
|
};
|
|
|
|
} else { // Strictly obey beforeColon and afterColon in each property
|
|
|
|
return {
|
|
"Property": function (node) {
|
|
verifySpacing(node);
|
|
}
|
|
};
|
|
|
|
}
|
|
|
|
};
|
|
|
|
module.exports.schema = [
|
|
{
|
|
"type": "object",
|
|
"properties": {
|
|
"align": {
|
|
"enum": ["colon", "value"]
|
|
},
|
|
"beforeColon": {
|
|
"type": "boolean"
|
|
},
|
|
"afterColon": {
|
|
"type": "boolean"
|
|
}
|
|
},
|
|
"additionalProperties": false
|
|
}
|
|
];
|
|
|