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.

282 lines
9.2 KiB

/**
* @fileoverview Disallows or enforces spaces inside of parentheses.
* @author Jonathan Rajavuori
* @copyright 2014 David Clark. All rights reserved.
* @copyright 2014 Jonathan Rajavuori. All rights reserved.
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = function(context) {
var MISSING_SPACE_MESSAGE = "There must be a space inside this paren.",
REJECTED_SPACE_MESSAGE = "There should be no spaces inside this paren.",
exceptionsArray = (context.options.length === 2) ? context.options[1].exceptions : [],
options = {},
rejectedSpaceRegExp,
missingSpaceRegExp,
spaceChecks;
if (exceptionsArray && exceptionsArray.length) {
options.braceException = exceptionsArray.indexOf("{}") !== -1 || false;
options.bracketException = exceptionsArray.indexOf("[]") !== -1 || false;
options.parenException = exceptionsArray.indexOf("()") !== -1 || false;
options.empty = exceptionsArray.indexOf("empty") !== -1 || false;
}
/**
* Used with the `never` option to produce, given the exception options,
* two regular expressions to check for missing and rejected spaces.
* @param {Object} opts The exception options
* @returns {Object} `missingSpace` and `rejectedSpace` regular expressions
* @private
*/
function getNeverChecks(opts) {
var missingSpaceOpeners = [],
missingSpaceClosers = [],
rejectedSpaceOpeners = ["\\s"],
rejectedSpaceClosers = ["\\s"],
missingSpaceCheck,
rejectedSpaceCheck;
// Populate openers and closers
if (opts.braceException) {
missingSpaceOpeners.push("\\{");
missingSpaceClosers.push("\\}");
rejectedSpaceOpeners.push("\\{");
rejectedSpaceClosers.push("\\}");
}
if (opts.bracketException) {
missingSpaceOpeners.push("\\[");
missingSpaceClosers.push("\\]");
rejectedSpaceOpeners.push("\\[");
rejectedSpaceClosers.push("\\]");
}
if (opts.parenException) {
missingSpaceOpeners.push("\\(");
missingSpaceClosers.push("\\)");
rejectedSpaceOpeners.push("\\(");
rejectedSpaceClosers.push("\\)");
}
if (opts.empty) {
missingSpaceOpeners.push("\\)");
missingSpaceClosers.push("\\(");
rejectedSpaceOpeners.push("\\)");
rejectedSpaceClosers.push("\\(");
}
if (missingSpaceOpeners.length) {
missingSpaceCheck = "\\((" + missingSpaceOpeners.join("|") + ")";
if (missingSpaceClosers.length) {
missingSpaceCheck += "|";
}
}
if (missingSpaceClosers.length) {
missingSpaceCheck += "(" + missingSpaceClosers.join("|") + ")\\)";
}
// compose the rejected regexp
rejectedSpaceCheck = "\\( +[^" + rejectedSpaceOpeners.join("") + "]";
rejectedSpaceCheck += "|[^" + rejectedSpaceClosers.join("") + "] +\\)";
return {
// e.g. \((\{)|(\})\) --- where {} is an exception
missingSpace: missingSpaceCheck || ".^",
// e.g. \( +[^ \n\r\{]|[^ \n\r\}] +\) --- where {} is an exception
rejectedSpace: rejectedSpaceCheck
};
}
/**
* Used with the `always` option to produce, given the exception options,
* two regular expressions to check for missing and rejected spaces.
* @param {Object} opts The exception options
* @returns {Object} `missingSpace` and `rejectedSpace` regular expressions
* @private
*/
function getAlwaysChecks(opts) {
var missingSpaceOpeners = ["\\s", "\\)"],
missingSpaceClosers = ["\\s", "\\("],
rejectedSpaceOpeners = [],
rejectedSpaceClosers = [],
missingSpaceCheck,
rejectedSpaceCheck;
// Populate openers and closers
if (opts.braceException) {
missingSpaceOpeners.push("\\{");
missingSpaceClosers.push("\\}");
rejectedSpaceOpeners.push(" \\{");
rejectedSpaceClosers.push("\\} ");
}
if (opts.bracketException) {
missingSpaceOpeners.push("\\[");
missingSpaceClosers.push("\\]");
rejectedSpaceOpeners.push(" \\[");
rejectedSpaceClosers.push("\\] ");
}
if (opts.parenException) {
missingSpaceOpeners.push("\\(");
missingSpaceClosers.push("\\)");
rejectedSpaceOpeners.push(" \\(");
rejectedSpaceClosers.push("\\) ");
}
if (opts.empty) {
rejectedSpaceOpeners.push(" \\)");
rejectedSpaceClosers.push("\\( ");
}
// compose the allowed regexp
missingSpaceCheck = "\\([^" + missingSpaceOpeners.join("") + "]";
missingSpaceCheck += "|[^" + missingSpaceClosers.join("") + "]\\)";
// compose the rejected regexp
if (rejectedSpaceOpeners.length) {
rejectedSpaceCheck = "\\((" + rejectedSpaceOpeners.join("|") + ")";
if (rejectedSpaceClosers.length) {
rejectedSpaceCheck += "|";
}
}
if (rejectedSpaceClosers.length) {
rejectedSpaceCheck += "(" + rejectedSpaceClosers.join("|") + ")\\)";
}
return {
// e.g. \([^ \)\r\n\{]|[^ \(\r\n\}]\) --- where {} is an exception
missingSpace: missingSpaceCheck,
// e.g. \(( \{})|(\} )\) --- where {} is an excpetion
rejectedSpace: rejectedSpaceCheck || ".^"
};
}
spaceChecks = (context.options[0] === "always") ? getAlwaysChecks(options) : getNeverChecks(options);
missingSpaceRegExp = new RegExp(spaceChecks.missingSpace, "mg");
rejectedSpaceRegExp = new RegExp(spaceChecks.rejectedSpace, "mg");
//--------------------------------------------------------------------------
// Helpers
//--------------------------------------------------------------------------
var skipRanges = [];
/**
* Adds the range of a node to the set to be skipped when checking parens
* @param {ASTNode} node The node to skip
* @returns {void}
* @private
*/
function addSkipRange(node) {
skipRanges.push(node.range);
}
/**
* Sorts the skipRanges array. Must be called before shouldSkip
* @returns {void}
* @private
*/
function sortSkipRanges() {
skipRanges.sort(function (a, b) {
return a[0] - b[0];
});
}
/**
* Checks if a certain position in the source should be skipped
* @param {Number} pos The 0-based index in the source
* @returns {boolean} whether the position should be skipped
* @private
*/
function shouldSkip(pos) {
var i, len, range;
for (i = 0, len = skipRanges.length; i < len; i += 1) {
range = skipRanges[i];
if (pos < range[0]) {
break;
} else if (pos < range[1]) {
return true;
}
}
return false;
}
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
return {
"Program:exit": function checkParenSpaces(node) {
var nextMatch,
nextLine,
column,
line = 1,
source = context.getSource(),
pos = 0;
function checkMatch(match, message) {
if (source.charAt(match.index) !== "(") {
// Matched a closing paren pattern
match.index += 1;
}
if (!shouldSkip(match.index)) {
while ((nextLine = source.indexOf("\n", pos)) !== -1 && nextLine < match.index) {
pos = nextLine + 1;
line += 1;
}
column = match.index - pos;
context.report(node, { line: line, column: column }, message);
}
}
sortSkipRanges();
while ((nextMatch = rejectedSpaceRegExp.exec(source)) !== null) {
checkMatch(nextMatch, REJECTED_SPACE_MESSAGE);
}
while ((nextMatch = missingSpaceRegExp.exec(source)) !== null) {
checkMatch(nextMatch, MISSING_SPACE_MESSAGE);
}
},
// These nodes can contain parentheses that this rule doesn't care about
LineComment: addSkipRange,
BlockComment: addSkipRange,
Literal: addSkipRange
};
};
module.exports.schema = [
{
"enum": ["always", "never"]
},
{
"type": "object",
"properties": {
"exceptions": {
"type": "array",
"items": {
"enum": ["{}", "[]", "()", "empty"]
},
"uniqueItems": true
}
},
"additionalProperties": false
}
];