|
|
|
/**
|
|
|
|
* @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
|
|
|
|
}
|
|
|
|
];
|