func-call-spacing.js 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. /**
  2. * @fileoverview Rule to control spacing within function calls
  3. * @author Matt DuVall <http://www.mattduvall.com>
  4. * @deprecated in ESLint v8.53.0
  5. */
  6. "use strict";
  7. //------------------------------------------------------------------------------
  8. // Requirements
  9. //------------------------------------------------------------------------------
  10. const astUtils = require("./utils/ast-utils");
  11. //------------------------------------------------------------------------------
  12. // Rule Definition
  13. //------------------------------------------------------------------------------
  14. /** @type {import('../types').Rule.RuleModule} */
  15. module.exports = {
  16. meta: {
  17. deprecated: {
  18. message: "Formatting rules are being moved out of ESLint core.",
  19. url: "https://eslint.org/blog/2023/10/deprecating-formatting-rules/",
  20. deprecatedSince: "8.53.0",
  21. availableUntil: "11.0.0",
  22. replacedBy: [
  23. {
  24. message:
  25. "ESLint Stylistic now maintains deprecated stylistic core rules.",
  26. url: "https://eslint.style/guide/migration",
  27. plugin: {
  28. name: "@stylistic/eslint-plugin",
  29. url: "https://eslint.style",
  30. },
  31. rule: {
  32. name: "function-call-spacing",
  33. url: "https://eslint.style/rules/function-call-spacing",
  34. },
  35. },
  36. ],
  37. },
  38. type: "layout",
  39. docs: {
  40. description:
  41. "Require or disallow spacing between function identifiers and their invocations",
  42. recommended: false,
  43. url: "https://eslint.org/docs/latest/rules/func-call-spacing",
  44. },
  45. fixable: "whitespace",
  46. schema: {
  47. anyOf: [
  48. {
  49. type: "array",
  50. items: [
  51. {
  52. enum: ["never"],
  53. },
  54. ],
  55. minItems: 0,
  56. maxItems: 1,
  57. },
  58. {
  59. type: "array",
  60. items: [
  61. {
  62. enum: ["always"],
  63. },
  64. {
  65. type: "object",
  66. properties: {
  67. allowNewlines: {
  68. type: "boolean",
  69. },
  70. },
  71. additionalProperties: false,
  72. },
  73. ],
  74. minItems: 0,
  75. maxItems: 2,
  76. },
  77. ],
  78. },
  79. messages: {
  80. unexpectedWhitespace:
  81. "Unexpected whitespace between function name and paren.",
  82. unexpectedNewline:
  83. "Unexpected newline between function name and paren.",
  84. missing: "Missing space between function name and paren.",
  85. },
  86. },
  87. create(context) {
  88. const never = context.options[0] !== "always";
  89. const allowNewlines =
  90. !never && context.options[1] && context.options[1].allowNewlines;
  91. const sourceCode = context.sourceCode;
  92. const text = sourceCode.getText();
  93. /**
  94. * Check if open space is present in a function name
  95. * @param {ASTNode} node node to evaluate
  96. * @param {Token} leftToken The last token of the callee. This may be the closing parenthesis that encloses the callee.
  97. * @param {Token} rightToken The first token of the arguments. this is the opening parenthesis that encloses the arguments.
  98. * @returns {void}
  99. * @private
  100. */
  101. function checkSpacing(node, leftToken, rightToken) {
  102. const textBetweenTokens = text
  103. .slice(leftToken.range[1], rightToken.range[0])
  104. .replace(/\/\*.*?\*\//gu, "");
  105. const hasWhitespace = /\s/u.test(textBetweenTokens);
  106. const hasNewline =
  107. hasWhitespace &&
  108. astUtils.LINEBREAK_MATCHER.test(textBetweenTokens);
  109. /*
  110. * never allowNewlines hasWhitespace hasNewline message
  111. * F F F F Missing space between function name and paren.
  112. * F F F T (Invalid `!hasWhitespace && hasNewline`)
  113. * F F T T Unexpected newline between function name and paren.
  114. * F F T F (OK)
  115. * F T T F (OK)
  116. * F T T T (OK)
  117. * F T F T (Invalid `!hasWhitespace && hasNewline`)
  118. * F T F F Missing space between function name and paren.
  119. * T T F F (Invalid `never && allowNewlines`)
  120. * T T F T (Invalid `!hasWhitespace && hasNewline`)
  121. * T T T T (Invalid `never && allowNewlines`)
  122. * T T T F (Invalid `never && allowNewlines`)
  123. * T F T F Unexpected space between function name and paren.
  124. * T F T T Unexpected space between function name and paren.
  125. * T F F T (Invalid `!hasWhitespace && hasNewline`)
  126. * T F F F (OK)
  127. *
  128. * T T Unexpected space between function name and paren.
  129. * F F Missing space between function name and paren.
  130. * F F T Unexpected newline between function name and paren.
  131. */
  132. if (never && hasWhitespace) {
  133. context.report({
  134. node,
  135. loc: {
  136. start: leftToken.loc.end,
  137. end: {
  138. line: rightToken.loc.start.line,
  139. column: rightToken.loc.start.column - 1,
  140. },
  141. },
  142. messageId: "unexpectedWhitespace",
  143. fix(fixer) {
  144. // Don't remove comments.
  145. if (
  146. sourceCode.commentsExistBetween(
  147. leftToken,
  148. rightToken,
  149. )
  150. ) {
  151. return null;
  152. }
  153. // If `?.` exists, it doesn't hide no-unexpected-multiline errors
  154. if (node.optional) {
  155. return fixer.replaceTextRange(
  156. [leftToken.range[1], rightToken.range[0]],
  157. "?.",
  158. );
  159. }
  160. /*
  161. * Only autofix if there is no newline
  162. * https://github.com/eslint/eslint/issues/7787
  163. */
  164. if (hasNewline) {
  165. return null;
  166. }
  167. return fixer.removeRange([
  168. leftToken.range[1],
  169. rightToken.range[0],
  170. ]);
  171. },
  172. });
  173. } else if (!never && !hasWhitespace) {
  174. context.report({
  175. node,
  176. loc: {
  177. start: {
  178. line: leftToken.loc.end.line,
  179. column: leftToken.loc.end.column - 1,
  180. },
  181. end: rightToken.loc.start,
  182. },
  183. messageId: "missing",
  184. fix(fixer) {
  185. if (node.optional) {
  186. return null; // Not sure if inserting a space to either before/after `?.` token.
  187. }
  188. return fixer.insertTextBefore(rightToken, " ");
  189. },
  190. });
  191. } else if (!never && !allowNewlines && hasNewline) {
  192. context.report({
  193. node,
  194. loc: {
  195. start: leftToken.loc.end,
  196. end: rightToken.loc.start,
  197. },
  198. messageId: "unexpectedNewline",
  199. fix(fixer) {
  200. /*
  201. * Only autofix if there is no newline
  202. * https://github.com/eslint/eslint/issues/7787
  203. * But if `?.` exists, it doesn't hide no-unexpected-multiline errors
  204. */
  205. if (!node.optional) {
  206. return null;
  207. }
  208. // Don't remove comments.
  209. if (
  210. sourceCode.commentsExistBetween(
  211. leftToken,
  212. rightToken,
  213. )
  214. ) {
  215. return null;
  216. }
  217. const range = [leftToken.range[1], rightToken.range[0]];
  218. const qdToken = sourceCode.getTokenAfter(leftToken);
  219. if (qdToken.range[0] === leftToken.range[1]) {
  220. return fixer.replaceTextRange(range, "?. ");
  221. }
  222. if (qdToken.range[1] === rightToken.range[0]) {
  223. return fixer.replaceTextRange(range, " ?.");
  224. }
  225. return fixer.replaceTextRange(range, " ?. ");
  226. },
  227. });
  228. }
  229. }
  230. return {
  231. "CallExpression, NewExpression"(node) {
  232. const lastToken = sourceCode.getLastToken(node);
  233. const lastCalleeToken = sourceCode.getLastToken(node.callee);
  234. const parenToken = sourceCode.getFirstTokenBetween(
  235. lastCalleeToken,
  236. lastToken,
  237. astUtils.isOpeningParenToken,
  238. );
  239. const prevToken =
  240. parenToken &&
  241. sourceCode.getTokenBefore(
  242. parenToken,
  243. astUtils.isNotQuestionDotToken,
  244. );
  245. // Parens in NewExpression are optional
  246. if (!(parenToken && parenToken.range[1] < node.range[1])) {
  247. return;
  248. }
  249. checkSpacing(node, prevToken, parenToken);
  250. },
  251. ImportExpression(node) {
  252. const leftToken = sourceCode.getFirstToken(node);
  253. const rightToken = sourceCode.getTokenAfter(leftToken);
  254. checkSpacing(node, leftToken, rightToken);
  255. },
  256. };
  257. },
  258. };