Skip to content
This repository has been archived by the owner on Mar 23, 2024. It is now read-only.

Commit

Permalink
requireSpacesInsideParenthesizedExpression: new rule implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
gibson042 committed Apr 10, 2015
1 parent 9096e09 commit 38bbab1
Show file tree
Hide file tree
Showing 3 changed files with 237 additions and 0 deletions.
1 change: 1 addition & 0 deletions lib/config/configuration.js
Expand Up @@ -580,6 +580,7 @@ Configuration.prototype.registerDefaultRules = function() {
this.registerRule(require('../rules/require-spaces-inside-object-brackets'));
this.registerRule(require('../rules/require-spaces-inside-array-brackets'));
this.registerRule(require('../rules/require-spaces-inside-parentheses'));
this.registerRule(require('../rules/require-spaces-inside-parenthesized-expression'));
this.registerRule(require('../rules/disallow-spaces-inside-brackets'));
this.registerRule(require('../rules/disallow-spaces-inside-object-brackets'));
this.registerRule(require('../rules/disallow-spaces-inside-array-brackets'));
Expand Down
120 changes: 120 additions & 0 deletions lib/rules/require-spaces-inside-parenthesized-expression.js
@@ -0,0 +1,120 @@
/**
* Requires space after opening grouping round bracket and before closing.
*
* Types: `Boolean` or `Object`
*
* Values:
* - `true`: always require spaces inside grouping parentheses
* - `Object`:
* - `"allExcept"`: `[ "{", "}", "function" ]` Ignore parenthesized objects and functions
*
* #### Example
*
* ```js
* "requireSpacesInsideParenthesizedExpression": true
*
* // or
*
* "requireSpacesInsideParenthesizedExpression": {
* "allExcept": [ "{", "}" ]
* }
* ```
*
* ##### Valid for mode `true`
*
* ```js
* var x = ( 1 + obj.size ) * ( 2 );
* ```
*
* ##### Valid for mode `{ allExcept": [ "{", "}", "function" ] }`
*
* ```js
* var x = ( options || { x: true }).x;
* var global = (function() { return this; })();
* ```
*
* ##### Invalid
*
* ```js
* var x = (1 + obj.size) * (2);
* ```
*/

var assert = require('assert');
var utils = require('../utils');

module.exports = function() {};

module.exports.prototype = {
configure: function(value) {
var isObject = typeof value === 'object';

var error = this.getOptionName() + ' rule requires string value true or object';

if (isObject) {
assert('allExcept' in value, error);
} else {
assert(value === true, error);
}

this._exceptions = {};

if (isObject) {
(value.allExcept || []).forEach(function(value) {
this._exceptions[value] = true;
}, this);
}
},

getOptionName: function() {
return 'requireSpacesInsideParenthesizedExpression';
},

check: function(file, errors) {
var exceptions = this._exceptions;

file.iterateTokenByValue('(', function(token) {
var nextToken = file.getNextToken(token, {includeComments: true});
var value = nextToken.value;

// Skip empty parentheses and explicit exceptions
if (value === ')' || value in exceptions) {
return;
}

// Skip non-expression parentheses
var type = utils.categorizeOpenParen(token, file);
if (type !== 'ParenthesizedExpression') {
return;
}

errors.assert.whitespaceBetween({
token: token,
nextToken: nextToken,
message: 'Missing space after opening grouping round bracket'
});
});

file.iterateTokenByValue(')', function(token) {
var prevToken = file.getPrevToken(token, {includeComments: true});
var value = prevToken.value;

// Skip empty parentheses and explicit exceptions
if (value === '(' || value in exceptions) {
return;
}

// Skip non-expression parentheses
var type = utils.categorizeCloseParen(token, file);
if (type !== 'ParenthesizedExpression') {
return;
}

errors.assert.whitespaceBetween({
token: prevToken,
nextToken: token,
message: 'Missing space before closing grouping round bracket'
});
});
}
};
116 changes: 116 additions & 0 deletions lib/utils.js
@@ -1,3 +1,4 @@
var assert = require('assert');
var path = require('path');
var Vow = require('vow');

Expand Down Expand Up @@ -74,6 +75,11 @@ var TRAILING_UNDERSCORES_RE = /(^_+|_+$)/g;

var SNAKE_CASE_RE = /^([a-z$][a-z0-9$]+)(_[a-z0-9$]+)+$/i;

var FUNCTION_TYPE_RE = /Function/;
var STATEMENT_TYPE_RE = /Statement$/;
var NO_PAREN_STATEMENT_TYPE_RE = /^ExpressionStatement$|^ReturnStatement$|^ThrowStatement$/;
var ANY_STATEMENT_TYPE_RE = /Statement$|Declaration$/;

/**
* All keywords where spaces are a stylistic choice
* @type {Array}
Expand Down Expand Up @@ -180,6 +186,116 @@ exports.getFunctionNodeFromIIFE = function(node) {
return null;
};

/**
* Returns the type of AST node or ECMAScript production in which the provided
* open parenthesis token is included.
*
* @param {Object} token
* @param {JsFile} file
* @returns {String}
*/
exports.categorizeOpenParen = function(token, file) {
assert(token.value === '(', 'Input token must be a round bracket');
var node = file.getNodeByRange(token.range[0]);
var nodeType = node.type;
var prevToken = file.getPrevToken(token);

// Outermost grouping parenthesis
if (!prevToken) {
return 'ParenthesizedExpression';
}

// Part of a parentheses-bearing statement (if, with, while, switch, etc.)
if (prevToken.type === 'Keyword' && STATEMENT_TYPE_RE.test(nodeType) &&
!NO_PAREN_STATEMENT_TYPE_RE.test(nodeType)) {

return 'Statement';
}

// Part of a function definition (declaration, expression, method, etc.)
if (FUNCTION_TYPE_RE.test(nodeType) &&

// Name is optional for function expressions
(prevToken.type === 'Identifier' || prevToken.value === 'function')) {

return 'Function';
}

// Part of a call expression
var prevNode = file.getNodeByRange(prevToken.range[0]);
if (nodeType === 'CallExpression' &&

// Must not be inside an arguments list or other grouping parentheses
prevToken.value !== ',' && prevToken.value !== '(' &&

// If the callee is parenthesized (e.g., `(foo.bar)()`), prevNode will match node
// Otherwise (e.g., `foo.bar()`), prevToken must be the last token of the callee node
(prevNode === node || prevToken === file.getLastNodeToken(node.callee))) {

return 'CallExpression';
}

// All remaining cases are grouping parentheses
return 'ParenthesizedExpression';
};

/**
* Returns the type of AST node or ECMAScript production in which the provided
* close parenthesis token is included.
*
* @param {Object} token
* @param {JsFile} file
* @returns {String}
*/
exports.categorizeCloseParen = function(token, file) {
assert(token.value === ')', 'Input token must be a round bracket');
var node = file.getNodeByRange(token.range[0]);
var nodeType = node.type;
var nextToken = file.getNextToken(token);

// Terminal statement
if (nextToken.type === 'EOF') {
switch (nodeType) {
case 'DoWhileStatement':
return 'Statement';
case 'CallExpression':
return 'CallExpression';
default:
return 'ParenthesizedExpression';
}
}

// Part of a parentheses-bearing statement (if, with, while, switch, etc.)
if (STATEMENT_TYPE_RE.test(nodeType) && !NO_PAREN_STATEMENT_TYPE_RE.test(nodeType)) {
// Closing parentheses for switch must be followed by "{"
if (nextToken.value === '{') {
return 'Statement';
}

// Closing parentheses for other statements must be followed by a statement
var nextNode = file.getNodeByRange(nextToken.range[0]);
while (nextNode && nextNode.range[0] >= token.range[1]) {
if (ANY_STATEMENT_TYPE_RE.test(nextNode.type)) {
return 'Statement';
}
nextNode = nextNode.parentNode;
}
}

// Part of a function definition (declaration, expression, method, etc.)
if (nextToken.value === '{' && FUNCTION_TYPE_RE.test(nodeType)) {
return 'Function';
}

// Part of a call expression
if (nodeType === 'CallExpression' && nextToken.range[0] >= node.range[1]) {
return 'CallExpression';
}

// All remaining cases are grouping parentheses
return 'ParenthesizedExpression';
};

/**
* Trims leading and trailing underscores
*
Expand Down

0 comments on commit 38bbab1

Please sign in to comment.