2025-04-19 23:12:19 -04:00

345 lines
10 KiB
JavaScript

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