--- /dev/null
+/**
+ * @fileoverview Disallows or enforces spaces inside of parentheses.
+ * @author Jonathan Rajavuori
+ */
+"use strict";
+
+const astUtils = require("./utils/ast-utils");
+
+//------------------------------------------------------------------------------
+// Rule Definition
+//------------------------------------------------------------------------------
+
+module.exports = {
+ meta: {
+ type: "layout",
+
+ docs: {
+ description: "enforce consistent spacing inside parentheses",
+ category: "Stylistic Issues",
+ recommended: false,
+ url: "https://eslint.org/docs/rules/space-in-parens"
+ },
+
+ fixable: "whitespace",
+
+ schema: [
+ {
+ enum: ["always", "never"]
+ },
+ {
+ type: "object",
+ properties: {
+ exceptions: {
+ type: "array",
+ items: {
+ enum: ["{}", "[]", "()", "empty"]
+ },
+ uniqueItems: true
+ }
+ },
+ additionalProperties: false
+ }
+ ],
+
+ messages: {
+ missingOpeningSpace: "There must be a space after this paren.",
+ missingClosingSpace: "There must be a space before this paren.",
+ rejectedOpeningSpace: "There should be no space after this paren.",
+ rejectedClosingSpace: "There should be no space before this paren."
+ }
+ },
+
+ create(context) {
+ const ALWAYS = context.options[0] === "always",
+ exceptionsArrayOptions = (context.options[1] && context.options[1].exceptions) || [],
+ options = {};
+
+ let exceptions;
+
+ if (exceptionsArrayOptions.length) {
+ options.braceException = exceptionsArrayOptions.includes("{}");
+ options.bracketException = exceptionsArrayOptions.includes("[]");
+ options.parenException = exceptionsArrayOptions.includes("()");
+ options.empty = exceptionsArrayOptions.includes("empty");
+ }
+
+ /**
+ * Produces an object with the opener and closer exception values
+ * @returns {Object} `openers` and `closers` exception values
+ * @private
+ */
+ function getExceptions() {
+ const openers = [],
+ closers = [];
+
+ if (options.braceException) {
+ openers.push("{");
+ closers.push("}");
+ }
+
+ if (options.bracketException) {
+ openers.push("[");
+ closers.push("]");
+ }
+
+ if (options.parenException) {
+ openers.push("(");
+ closers.push(")");
+ }
+
+ if (options.empty) {
+ openers.push(")");
+ closers.push("(");
+ }
+
+ return {
+ openers,
+ closers
+ };
+ }
+
+ //--------------------------------------------------------------------------
+ // Helpers
+ //--------------------------------------------------------------------------
+ const sourceCode = context.getSourceCode();
+
+ /**
+ * Determines if a token is one of the exceptions for the opener paren
+ * @param {Object} token The token to check
+ * @returns {boolean} True if the token is one of the exceptions for the opener paren
+ */
+ function isOpenerException(token) {
+ return exceptions.openers.includes(token.value);
+ }
+
+ /**
+ * Determines if a token is one of the exceptions for the closer paren
+ * @param {Object} token The token to check
+ * @returns {boolean} True if the token is one of the exceptions for the closer paren
+ */
+ function isCloserException(token) {
+ return exceptions.closers.includes(token.value);
+ }
+
+ /**
+ * Determines if an opening paren is immediately followed by a required space
+ * @param {Object} openingParenToken The paren token
+ * @param {Object} tokenAfterOpeningParen The token after it
+ * @returns {boolean} True if the opening paren is missing a required space
+ */
+ function openerMissingSpace(openingParenToken, tokenAfterOpeningParen) {
+ if (sourceCode.isSpaceBetweenTokens(openingParenToken, tokenAfterOpeningParen)) {
+ return false;
+ }
+
+ if (!options.empty && astUtils.isClosingParenToken(tokenAfterOpeningParen)) {
+ return false;
+ }
+
+ if (ALWAYS) {
+ return !isOpenerException(tokenAfterOpeningParen);
+ }
+ return isOpenerException(tokenAfterOpeningParen);
+ }
+
+ /**
+ * Determines if an opening paren is immediately followed by a disallowed space
+ * @param {Object} openingParenToken The paren token
+ * @param {Object} tokenAfterOpeningParen The token after it
+ * @returns {boolean} True if the opening paren has a disallowed space
+ */
+ function openerRejectsSpace(openingParenToken, tokenAfterOpeningParen) {
+ if (!astUtils.isTokenOnSameLine(openingParenToken, tokenAfterOpeningParen)) {
+ return false;
+ }
+
+ if (tokenAfterOpeningParen.type === "Line") {
+ return false;
+ }
+
+ if (!sourceCode.isSpaceBetweenTokens(openingParenToken, tokenAfterOpeningParen)) {
+ return false;
+ }
+
+ if (ALWAYS) {
+ return isOpenerException(tokenAfterOpeningParen);
+ }
+ return !isOpenerException(tokenAfterOpeningParen);
+ }
+
+ /**
+ * Determines if a closing paren is immediately preceeded by a required space
+ * @param {Object} tokenBeforeClosingParen The token before the paren
+ * @param {Object} closingParenToken The paren token
+ * @returns {boolean} True if the closing paren is missing a required space
+ */
+ function closerMissingSpace(tokenBeforeClosingParen, closingParenToken) {
+ if (sourceCode.isSpaceBetweenTokens(tokenBeforeClosingParen, closingParenToken)) {
+ return false;
+ }
+
+ if (!options.empty && astUtils.isOpeningParenToken(tokenBeforeClosingParen)) {
+ return false;
+ }
+
+ if (ALWAYS) {
+ return !isCloserException(tokenBeforeClosingParen);
+ }
+ return isCloserException(tokenBeforeClosingParen);
+ }
+
+ /**
+ * Determines if a closer paren is immediately preceeded by a disallowed space
+ * @param {Object} tokenBeforeClosingParen The token before the paren
+ * @param {Object} closingParenToken The paren token
+ * @returns {boolean} True if the closing paren has a disallowed space
+ */
+ function closerRejectsSpace(tokenBeforeClosingParen, closingParenToken) {
+ if (!astUtils.isTokenOnSameLine(tokenBeforeClosingParen, closingParenToken)) {
+ return false;
+ }
+
+ if (!sourceCode.isSpaceBetweenTokens(tokenBeforeClosingParen, closingParenToken)) {
+ return false;
+ }
+
+ if (ALWAYS) {
+ return isCloserException(tokenBeforeClosingParen);
+ }
+ return !isCloserException(tokenBeforeClosingParen);
+ }
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ return {
+ Program: function checkParenSpaces(node) {
+ exceptions = getExceptions();
+ const tokens = sourceCode.tokensAndComments;
+
+ tokens.forEach((token, i) => {
+ const prevToken = tokens[i - 1];
+ const nextToken = tokens[i + 1];
+
+ // if token is not an opening or closing paren token, do nothing
+ if (!astUtils.isOpeningParenToken(token) && !astUtils.isClosingParenToken(token)) {
+ return;
+ }
+
+ // if token is an opening paren and is not followed by a required space
+ if (token.value === "(" && openerMissingSpace(token, nextToken)) {
+ context.report({
+ node,
+ loc: token.loc,
+ messageId: "missingOpeningSpace",
+ fix(fixer) {
+ return fixer.insertTextAfter(token, " ");
+ }
+ });
+ }
+
+ // if token is an opening paren and is followed by a disallowed space
+ if (token.value === "(" && openerRejectsSpace(token, nextToken)) {
+ context.report({
+ node,
+ loc: { start: token.loc.end, end: nextToken.loc.start },
+ messageId: "rejectedOpeningSpace",
+ fix(fixer) {
+ return fixer.removeRange([token.range[1], nextToken.range[0]]);
+ }
+ });
+ }
+
+ // if token is a closing paren and is not preceded by a required space
+ if (token.value === ")" && closerMissingSpace(prevToken, token)) {
+ context.report({
+ node,
+ loc: token.loc,
+ messageId: "missingClosingSpace",
+ fix(fixer) {
+ return fixer.insertTextBefore(token, " ");
+ }
+ });
+ }
+
+ // if token is a closing paren and is preceded by a disallowed space
+ if (token.value === ")" && closerRejectsSpace(prevToken, token)) {
+ context.report({
+ node,
+ loc: { start: prevToken.loc.end, end: token.loc.start },
+ messageId: "rejectedClosingSpace",
+ fix(fixer) {
+ return fixer.removeRange([prevToken.range[1], token.range[0]]);
+ }
+ });
+ }
+ });
+ }
+ };
+ }
+};