/** * @fileoverview Disallows or enforces spaces inside of parentheses. * @author Jonathan Rajavuori * @deprecated in ESLint v8.53.0 */ 'use strict'; const astUtils = require('./utils/ast-utils'); //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ /** @type {import('../types').Rule.RuleModule} */ module.exports = { meta: { deprecated: { message: 'Formatting rules are being moved out of ESLint core.', url: 'https://eslint.org/blog/2023/10/deprecating-formatting-rules/', deprecatedSince: '8.53.0', availableUntil: '10.0.0', replacedBy: [ { message: 'ESLint Stylistic now maintains deprecated stylistic core rules.', url: 'https://eslint.style/guide/migration', plugin: { name: '@stylistic/eslint-plugin-js', url: 'https://eslint.style/packages/js', }, rule: { name: 'space-in-parens', url: 'https://eslint.style/rules/js/space-in-parens', }, }, ], }, type: 'layout', docs: { description: 'Enforce consistent spacing inside parentheses', recommended: false, url: 'https://eslint.org/docs/latest/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.sourceCode; /** * 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 preceded 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 preceded 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]]); }, }); } }); }, }; }, };