space-in-parens.js 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374
  1. /**
  2. * @fileoverview Disallows or enforces spaces inside of parentheses.
  3. * @author Jonathan Rajavuori
  4. * @deprecated in ESLint v8.53.0
  5. */
  6. "use strict";
  7. const astUtils = require("./utils/ast-utils");
  8. //------------------------------------------------------------------------------
  9. // Rule Definition
  10. //------------------------------------------------------------------------------
  11. /** @type {import('../types').Rule.RuleModule} */
  12. module.exports = {
  13. meta: {
  14. deprecated: {
  15. message: "Formatting rules are being moved out of ESLint core.",
  16. url: "https://eslint.org/blog/2023/10/deprecating-formatting-rules/",
  17. deprecatedSince: "8.53.0",
  18. availableUntil: "11.0.0",
  19. replacedBy: [
  20. {
  21. message:
  22. "ESLint Stylistic now maintains deprecated stylistic core rules.",
  23. url: "https://eslint.style/guide/migration",
  24. plugin: {
  25. name: "@stylistic/eslint-plugin",
  26. url: "https://eslint.style",
  27. },
  28. rule: {
  29. name: "space-in-parens",
  30. url: "https://eslint.style/rules/space-in-parens",
  31. },
  32. },
  33. ],
  34. },
  35. type: "layout",
  36. docs: {
  37. description: "Enforce consistent spacing inside parentheses",
  38. recommended: false,
  39. url: "https://eslint.org/docs/latest/rules/space-in-parens",
  40. },
  41. fixable: "whitespace",
  42. schema: [
  43. {
  44. enum: ["always", "never"],
  45. },
  46. {
  47. type: "object",
  48. properties: {
  49. exceptions: {
  50. type: "array",
  51. items: {
  52. enum: ["{}", "[]", "()", "empty"],
  53. },
  54. uniqueItems: true,
  55. },
  56. },
  57. additionalProperties: false,
  58. },
  59. ],
  60. messages: {
  61. missingOpeningSpace: "There must be a space after this paren.",
  62. missingClosingSpace: "There must be a space before this paren.",
  63. rejectedOpeningSpace: "There should be no space after this paren.",
  64. rejectedClosingSpace: "There should be no space before this paren.",
  65. },
  66. },
  67. create(context) {
  68. const ALWAYS = context.options[0] === "always",
  69. exceptionsArrayOptions =
  70. (context.options[1] && context.options[1].exceptions) || [],
  71. options = {};
  72. let exceptions;
  73. if (exceptionsArrayOptions.length) {
  74. options.braceException = exceptionsArrayOptions.includes("{}");
  75. options.bracketException = exceptionsArrayOptions.includes("[]");
  76. options.parenException = exceptionsArrayOptions.includes("()");
  77. options.empty = exceptionsArrayOptions.includes("empty");
  78. }
  79. /**
  80. * Produces an object with the opener and closer exception values
  81. * @returns {Object} `openers` and `closers` exception values
  82. * @private
  83. */
  84. function getExceptions() {
  85. const openers = [],
  86. closers = [];
  87. if (options.braceException) {
  88. openers.push("{");
  89. closers.push("}");
  90. }
  91. if (options.bracketException) {
  92. openers.push("[");
  93. closers.push("]");
  94. }
  95. if (options.parenException) {
  96. openers.push("(");
  97. closers.push(")");
  98. }
  99. if (options.empty) {
  100. openers.push(")");
  101. closers.push("(");
  102. }
  103. return {
  104. openers,
  105. closers,
  106. };
  107. }
  108. //--------------------------------------------------------------------------
  109. // Helpers
  110. //--------------------------------------------------------------------------
  111. const sourceCode = context.sourceCode;
  112. /**
  113. * Determines if a token is one of the exceptions for the opener paren
  114. * @param {Object} token The token to check
  115. * @returns {boolean} True if the token is one of the exceptions for the opener paren
  116. */
  117. function isOpenerException(token) {
  118. return exceptions.openers.includes(token.value);
  119. }
  120. /**
  121. * Determines if a token is one of the exceptions for the closer paren
  122. * @param {Object} token The token to check
  123. * @returns {boolean} True if the token is one of the exceptions for the closer paren
  124. */
  125. function isCloserException(token) {
  126. return exceptions.closers.includes(token.value);
  127. }
  128. /**
  129. * Determines if an opening paren is immediately followed by a required space
  130. * @param {Object} openingParenToken The paren token
  131. * @param {Object} tokenAfterOpeningParen The token after it
  132. * @returns {boolean} True if the opening paren is missing a required space
  133. */
  134. function openerMissingSpace(openingParenToken, tokenAfterOpeningParen) {
  135. if (
  136. sourceCode.isSpaceBetweenTokens(
  137. openingParenToken,
  138. tokenAfterOpeningParen,
  139. )
  140. ) {
  141. return false;
  142. }
  143. if (
  144. !options.empty &&
  145. astUtils.isClosingParenToken(tokenAfterOpeningParen)
  146. ) {
  147. return false;
  148. }
  149. if (ALWAYS) {
  150. return !isOpenerException(tokenAfterOpeningParen);
  151. }
  152. return isOpenerException(tokenAfterOpeningParen);
  153. }
  154. /**
  155. * Determines if an opening paren is immediately followed by a disallowed space
  156. * @param {Object} openingParenToken The paren token
  157. * @param {Object} tokenAfterOpeningParen The token after it
  158. * @returns {boolean} True if the opening paren has a disallowed space
  159. */
  160. function openerRejectsSpace(openingParenToken, tokenAfterOpeningParen) {
  161. if (
  162. !astUtils.isTokenOnSameLine(
  163. openingParenToken,
  164. tokenAfterOpeningParen,
  165. )
  166. ) {
  167. return false;
  168. }
  169. if (tokenAfterOpeningParen.type === "Line") {
  170. return false;
  171. }
  172. if (
  173. !sourceCode.isSpaceBetweenTokens(
  174. openingParenToken,
  175. tokenAfterOpeningParen,
  176. )
  177. ) {
  178. return false;
  179. }
  180. if (ALWAYS) {
  181. return isOpenerException(tokenAfterOpeningParen);
  182. }
  183. return !isOpenerException(tokenAfterOpeningParen);
  184. }
  185. /**
  186. * Determines if a closing paren is immediately preceded by a required space
  187. * @param {Object} tokenBeforeClosingParen The token before the paren
  188. * @param {Object} closingParenToken The paren token
  189. * @returns {boolean} True if the closing paren is missing a required space
  190. */
  191. function closerMissingSpace(
  192. tokenBeforeClosingParen,
  193. closingParenToken,
  194. ) {
  195. if (
  196. sourceCode.isSpaceBetweenTokens(
  197. tokenBeforeClosingParen,
  198. closingParenToken,
  199. )
  200. ) {
  201. return false;
  202. }
  203. if (
  204. !options.empty &&
  205. astUtils.isOpeningParenToken(tokenBeforeClosingParen)
  206. ) {
  207. return false;
  208. }
  209. if (ALWAYS) {
  210. return !isCloserException(tokenBeforeClosingParen);
  211. }
  212. return isCloserException(tokenBeforeClosingParen);
  213. }
  214. /**
  215. * Determines if a closer paren is immediately preceded by a disallowed space
  216. * @param {Object} tokenBeforeClosingParen The token before the paren
  217. * @param {Object} closingParenToken The paren token
  218. * @returns {boolean} True if the closing paren has a disallowed space
  219. */
  220. function closerRejectsSpace(
  221. tokenBeforeClosingParen,
  222. closingParenToken,
  223. ) {
  224. if (
  225. !astUtils.isTokenOnSameLine(
  226. tokenBeforeClosingParen,
  227. closingParenToken,
  228. )
  229. ) {
  230. return false;
  231. }
  232. if (
  233. !sourceCode.isSpaceBetweenTokens(
  234. tokenBeforeClosingParen,
  235. closingParenToken,
  236. )
  237. ) {
  238. return false;
  239. }
  240. if (ALWAYS) {
  241. return isCloserException(tokenBeforeClosingParen);
  242. }
  243. return !isCloserException(tokenBeforeClosingParen);
  244. }
  245. //--------------------------------------------------------------------------
  246. // Public
  247. //--------------------------------------------------------------------------
  248. return {
  249. Program: function checkParenSpaces(node) {
  250. exceptions = getExceptions();
  251. const tokens = sourceCode.tokensAndComments;
  252. tokens.forEach((token, i) => {
  253. const prevToken = tokens[i - 1];
  254. const nextToken = tokens[i + 1];
  255. // if token is not an opening or closing paren token, do nothing
  256. if (
  257. !astUtils.isOpeningParenToken(token) &&
  258. !astUtils.isClosingParenToken(token)
  259. ) {
  260. return;
  261. }
  262. // if token is an opening paren and is not followed by a required space
  263. if (
  264. token.value === "(" &&
  265. openerMissingSpace(token, nextToken)
  266. ) {
  267. context.report({
  268. node,
  269. loc: token.loc,
  270. messageId: "missingOpeningSpace",
  271. fix(fixer) {
  272. return fixer.insertTextAfter(token, " ");
  273. },
  274. });
  275. }
  276. // if token is an opening paren and is followed by a disallowed space
  277. if (
  278. token.value === "(" &&
  279. openerRejectsSpace(token, nextToken)
  280. ) {
  281. context.report({
  282. node,
  283. loc: {
  284. start: token.loc.end,
  285. end: nextToken.loc.start,
  286. },
  287. messageId: "rejectedOpeningSpace",
  288. fix(fixer) {
  289. return fixer.removeRange([
  290. token.range[1],
  291. nextToken.range[0],
  292. ]);
  293. },
  294. });
  295. }
  296. // if token is a closing paren and is not preceded by a required space
  297. if (
  298. token.value === ")" &&
  299. closerMissingSpace(prevToken, token)
  300. ) {
  301. context.report({
  302. node,
  303. loc: token.loc,
  304. messageId: "missingClosingSpace",
  305. fix(fixer) {
  306. return fixer.insertTextBefore(token, " ");
  307. },
  308. });
  309. }
  310. // if token is a closing paren and is preceded by a disallowed space
  311. if (
  312. token.value === ")" &&
  313. closerRejectsSpace(prevToken, token)
  314. ) {
  315. context.report({
  316. node,
  317. loc: {
  318. start: prevToken.loc.end,
  319. end: token.loc.start,
  320. },
  321. messageId: "rejectedClosingSpace",
  322. fix(fixer) {
  323. return fixer.removeRange([
  324. prevToken.range[1],
  325. token.range[0],
  326. ]);
  327. },
  328. });
  329. }
  330. });
  331. },
  332. };
  333. },
  334. };