space-unary-ops.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  1. /**
  2. * @fileoverview This rule should require or disallow spaces before or after unary operations.
  3. * @author Marcin Kumorek
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Requirements
  8. //------------------------------------------------------------------------------
  9. const astUtils = require("./utils/ast-utils");
  10. //------------------------------------------------------------------------------
  11. // Rule Definition
  12. //------------------------------------------------------------------------------
  13. /** @type {import('../shared/types').Rule} */
  14. module.exports = {
  15. meta: {
  16. type: "layout",
  17. docs: {
  18. description: "Enforce consistent spacing before or after unary operators",
  19. recommended: false,
  20. url: "https://eslint.org/docs/rules/space-unary-ops"
  21. },
  22. fixable: "whitespace",
  23. schema: [
  24. {
  25. type: "object",
  26. properties: {
  27. words: {
  28. type: "boolean",
  29. default: true
  30. },
  31. nonwords: {
  32. type: "boolean",
  33. default: false
  34. },
  35. overrides: {
  36. type: "object",
  37. additionalProperties: {
  38. type: "boolean"
  39. }
  40. }
  41. },
  42. additionalProperties: false
  43. }
  44. ],
  45. messages: {
  46. unexpectedBefore: "Unexpected space before unary operator '{{operator}}'.",
  47. unexpectedAfter: "Unexpected space after unary operator '{{operator}}'.",
  48. unexpectedAfterWord: "Unexpected space after unary word operator '{{word}}'.",
  49. wordOperator: "Unary word operator '{{word}}' must be followed by whitespace.",
  50. operator: "Unary operator '{{operator}}' must be followed by whitespace.",
  51. beforeUnaryExpressions: "Space is required before unary expressions '{{token}}'."
  52. }
  53. },
  54. create(context) {
  55. const options = context.options[0] || { words: true, nonwords: false };
  56. const sourceCode = context.getSourceCode();
  57. //--------------------------------------------------------------------------
  58. // Helpers
  59. //--------------------------------------------------------------------------
  60. /**
  61. * Check if the node is the first "!" in a "!!" convert to Boolean expression
  62. * @param {ASTnode} node AST node
  63. * @returns {boolean} Whether or not the node is first "!" in "!!"
  64. */
  65. function isFirstBangInBangBangExpression(node) {
  66. return node && node.type === "UnaryExpression" && node.argument.operator === "!" &&
  67. node.argument && node.argument.type === "UnaryExpression" && node.argument.operator === "!";
  68. }
  69. /**
  70. * Checks if an override exists for a given operator.
  71. * @param {string} operator Operator
  72. * @returns {boolean} Whether or not an override has been provided for the operator
  73. */
  74. function overrideExistsForOperator(operator) {
  75. return options.overrides && Object.prototype.hasOwnProperty.call(options.overrides, operator);
  76. }
  77. /**
  78. * Gets the value that the override was set to for this operator
  79. * @param {string} operator Operator
  80. * @returns {boolean} Whether or not an override enforces a space with this operator
  81. */
  82. function overrideEnforcesSpaces(operator) {
  83. return options.overrides[operator];
  84. }
  85. /**
  86. * Verify Unary Word Operator has spaces after the word operator
  87. * @param {ASTnode} node AST node
  88. * @param {Object} firstToken first token from the AST node
  89. * @param {Object} secondToken second token from the AST node
  90. * @param {string} word The word to be used for reporting
  91. * @returns {void}
  92. */
  93. function verifyWordHasSpaces(node, firstToken, secondToken, word) {
  94. if (secondToken.range[0] === firstToken.range[1]) {
  95. context.report({
  96. node,
  97. messageId: "wordOperator",
  98. data: {
  99. word
  100. },
  101. fix(fixer) {
  102. return fixer.insertTextAfter(firstToken, " ");
  103. }
  104. });
  105. }
  106. }
  107. /**
  108. * Verify Unary Word Operator doesn't have spaces after the word operator
  109. * @param {ASTnode} node AST node
  110. * @param {Object} firstToken first token from the AST node
  111. * @param {Object} secondToken second token from the AST node
  112. * @param {string} word The word to be used for reporting
  113. * @returns {void}
  114. */
  115. function verifyWordDoesntHaveSpaces(node, firstToken, secondToken, word) {
  116. if (astUtils.canTokensBeAdjacent(firstToken, secondToken)) {
  117. if (secondToken.range[0] > firstToken.range[1]) {
  118. context.report({
  119. node,
  120. messageId: "unexpectedAfterWord",
  121. data: {
  122. word
  123. },
  124. fix(fixer) {
  125. return fixer.removeRange([firstToken.range[1], secondToken.range[0]]);
  126. }
  127. });
  128. }
  129. }
  130. }
  131. /**
  132. * Check Unary Word Operators for spaces after the word operator
  133. * @param {ASTnode} node AST node
  134. * @param {Object} firstToken first token from the AST node
  135. * @param {Object} secondToken second token from the AST node
  136. * @param {string} word The word to be used for reporting
  137. * @returns {void}
  138. */
  139. function checkUnaryWordOperatorForSpaces(node, firstToken, secondToken, word) {
  140. if (overrideExistsForOperator(word)) {
  141. if (overrideEnforcesSpaces(word)) {
  142. verifyWordHasSpaces(node, firstToken, secondToken, word);
  143. } else {
  144. verifyWordDoesntHaveSpaces(node, firstToken, secondToken, word);
  145. }
  146. } else if (options.words) {
  147. verifyWordHasSpaces(node, firstToken, secondToken, word);
  148. } else {
  149. verifyWordDoesntHaveSpaces(node, firstToken, secondToken, word);
  150. }
  151. }
  152. /**
  153. * Verifies YieldExpressions satisfy spacing requirements
  154. * @param {ASTnode} node AST node
  155. * @returns {void}
  156. */
  157. function checkForSpacesAfterYield(node) {
  158. const tokens = sourceCode.getFirstTokens(node, 3),
  159. word = "yield";
  160. if (!node.argument || node.delegate) {
  161. return;
  162. }
  163. checkUnaryWordOperatorForSpaces(node, tokens[0], tokens[1], word);
  164. }
  165. /**
  166. * Verifies AwaitExpressions satisfy spacing requirements
  167. * @param {ASTNode} node AwaitExpression AST node
  168. * @returns {void}
  169. */
  170. function checkForSpacesAfterAwait(node) {
  171. const tokens = sourceCode.getFirstTokens(node, 3);
  172. checkUnaryWordOperatorForSpaces(node, tokens[0], tokens[1], "await");
  173. }
  174. /**
  175. * Verifies UnaryExpression, UpdateExpression and NewExpression have spaces before or after the operator
  176. * @param {ASTnode} node AST node
  177. * @param {Object} firstToken First token in the expression
  178. * @param {Object} secondToken Second token in the expression
  179. * @returns {void}
  180. */
  181. function verifyNonWordsHaveSpaces(node, firstToken, secondToken) {
  182. if (node.prefix) {
  183. if (isFirstBangInBangBangExpression(node)) {
  184. return;
  185. }
  186. if (firstToken.range[1] === secondToken.range[0]) {
  187. context.report({
  188. node,
  189. messageId: "operator",
  190. data: {
  191. operator: firstToken.value
  192. },
  193. fix(fixer) {
  194. return fixer.insertTextAfter(firstToken, " ");
  195. }
  196. });
  197. }
  198. } else {
  199. if (firstToken.range[1] === secondToken.range[0]) {
  200. context.report({
  201. node,
  202. messageId: "beforeUnaryExpressions",
  203. data: {
  204. token: secondToken.value
  205. },
  206. fix(fixer) {
  207. return fixer.insertTextBefore(secondToken, " ");
  208. }
  209. });
  210. }
  211. }
  212. }
  213. /**
  214. * Verifies UnaryExpression, UpdateExpression and NewExpression don't have spaces before or after the operator
  215. * @param {ASTnode} node AST node
  216. * @param {Object} firstToken First token in the expression
  217. * @param {Object} secondToken Second token in the expression
  218. * @returns {void}
  219. */
  220. function verifyNonWordsDontHaveSpaces(node, firstToken, secondToken) {
  221. if (node.prefix) {
  222. if (secondToken.range[0] > firstToken.range[1]) {
  223. context.report({
  224. node,
  225. messageId: "unexpectedAfter",
  226. data: {
  227. operator: firstToken.value
  228. },
  229. fix(fixer) {
  230. if (astUtils.canTokensBeAdjacent(firstToken, secondToken)) {
  231. return fixer.removeRange([firstToken.range[1], secondToken.range[0]]);
  232. }
  233. return null;
  234. }
  235. });
  236. }
  237. } else {
  238. if (secondToken.range[0] > firstToken.range[1]) {
  239. context.report({
  240. node,
  241. messageId: "unexpectedBefore",
  242. data: {
  243. operator: secondToken.value
  244. },
  245. fix(fixer) {
  246. return fixer.removeRange([firstToken.range[1], secondToken.range[0]]);
  247. }
  248. });
  249. }
  250. }
  251. }
  252. /**
  253. * Verifies UnaryExpression, UpdateExpression and NewExpression satisfy spacing requirements
  254. * @param {ASTnode} node AST node
  255. * @returns {void}
  256. */
  257. function checkForSpaces(node) {
  258. const tokens = node.type === "UpdateExpression" && !node.prefix
  259. ? sourceCode.getLastTokens(node, 2)
  260. : sourceCode.getFirstTokens(node, 2);
  261. const firstToken = tokens[0];
  262. const secondToken = tokens[1];
  263. if ((node.type === "NewExpression" || node.prefix) && firstToken.type === "Keyword") {
  264. checkUnaryWordOperatorForSpaces(node, firstToken, secondToken, firstToken.value);
  265. return;
  266. }
  267. const operator = node.prefix ? tokens[0].value : tokens[1].value;
  268. if (overrideExistsForOperator(operator)) {
  269. if (overrideEnforcesSpaces(operator)) {
  270. verifyNonWordsHaveSpaces(node, firstToken, secondToken);
  271. } else {
  272. verifyNonWordsDontHaveSpaces(node, firstToken, secondToken);
  273. }
  274. } else if (options.nonwords) {
  275. verifyNonWordsHaveSpaces(node, firstToken, secondToken);
  276. } else {
  277. verifyNonWordsDontHaveSpaces(node, firstToken, secondToken);
  278. }
  279. }
  280. //--------------------------------------------------------------------------
  281. // Public
  282. //--------------------------------------------------------------------------
  283. return {
  284. UnaryExpression: checkForSpaces,
  285. UpdateExpression: checkForSpaces,
  286. NewExpression: checkForSpaces,
  287. YieldExpression: checkForSpacesAfterYield,
  288. AwaitExpression: checkForSpacesAfterAwait
  289. };
  290. }
  291. };