object-curly-spacing.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  1. /**
  2. * @fileoverview Disallows or enforces spaces inside of object literals.
  3. * @author Jamund Ferguson
  4. */
  5. "use strict";
  6. const astUtils = require("./utils/ast-utils");
  7. //------------------------------------------------------------------------------
  8. // Rule Definition
  9. //------------------------------------------------------------------------------
  10. module.exports = {
  11. meta: {
  12. type: "layout",
  13. docs: {
  14. description: "enforce consistent spacing inside braces",
  15. category: "Stylistic Issues",
  16. recommended: false,
  17. url: "https://eslint.org/docs/rules/object-curly-spacing"
  18. },
  19. fixable: "whitespace",
  20. schema: [
  21. {
  22. enum: ["always", "never"]
  23. },
  24. {
  25. type: "object",
  26. properties: {
  27. arraysInObjects: {
  28. type: "boolean"
  29. },
  30. objectsInObjects: {
  31. type: "boolean"
  32. }
  33. },
  34. additionalProperties: false
  35. }
  36. ]
  37. },
  38. create(context) {
  39. const spaced = context.options[0] === "always",
  40. sourceCode = context.getSourceCode();
  41. /**
  42. * Determines whether an option is set, relative to the spacing option.
  43. * If spaced is "always", then check whether option is set to false.
  44. * If spaced is "never", then check whether option is set to true.
  45. * @param {Object} option The option to exclude.
  46. * @returns {boolean} Whether or not the property is excluded.
  47. */
  48. function isOptionSet(option) {
  49. return context.options[1] ? context.options[1][option] === !spaced : false;
  50. }
  51. const options = {
  52. spaced,
  53. arraysInObjectsException: isOptionSet("arraysInObjects"),
  54. objectsInObjectsException: isOptionSet("objectsInObjects")
  55. };
  56. //--------------------------------------------------------------------------
  57. // Helpers
  58. //--------------------------------------------------------------------------
  59. /**
  60. * Reports that there shouldn't be a space after the first token
  61. * @param {ASTNode} node The node to report in the event of an error.
  62. * @param {Token} token The token to use for the report.
  63. * @returns {void}
  64. */
  65. function reportNoBeginningSpace(node, token) {
  66. const nextToken = context.getSourceCode().getTokenAfter(token, { includeComments: true });
  67. context.report({
  68. node,
  69. loc: { start: token.loc.end, end: nextToken.loc.start },
  70. message: "There should be no space after '{{token}}'.",
  71. data: {
  72. token: token.value
  73. },
  74. fix(fixer) {
  75. return fixer.removeRange([token.range[1], nextToken.range[0]]);
  76. }
  77. });
  78. }
  79. /**
  80. * Reports that there shouldn't be a space before the last token
  81. * @param {ASTNode} node The node to report in the event of an error.
  82. * @param {Token} token The token to use for the report.
  83. * @returns {void}
  84. */
  85. function reportNoEndingSpace(node, token) {
  86. const previousToken = context.getSourceCode().getTokenBefore(token, { includeComments: true });
  87. context.report({
  88. node,
  89. loc: { start: previousToken.loc.end, end: token.loc.start },
  90. message: "There should be no space before '{{token}}'.",
  91. data: {
  92. token: token.value
  93. },
  94. fix(fixer) {
  95. return fixer.removeRange([previousToken.range[1], token.range[0]]);
  96. }
  97. });
  98. }
  99. /**
  100. * Reports that there should be a space after the first token
  101. * @param {ASTNode} node The node to report in the event of an error.
  102. * @param {Token} token The token to use for the report.
  103. * @returns {void}
  104. */
  105. function reportRequiredBeginningSpace(node, token) {
  106. context.report({
  107. node,
  108. loc: token.loc,
  109. message: "A space is required after '{{token}}'.",
  110. data: {
  111. token: token.value
  112. },
  113. fix(fixer) {
  114. return fixer.insertTextAfter(token, " ");
  115. }
  116. });
  117. }
  118. /**
  119. * Reports that there should be a space before the last token
  120. * @param {ASTNode} node The node to report in the event of an error.
  121. * @param {Token} token The token to use for the report.
  122. * @returns {void}
  123. */
  124. function reportRequiredEndingSpace(node, token) {
  125. context.report({
  126. node,
  127. loc: token.loc,
  128. message: "A space is required before '{{token}}'.",
  129. data: {
  130. token: token.value
  131. },
  132. fix(fixer) {
  133. return fixer.insertTextBefore(token, " ");
  134. }
  135. });
  136. }
  137. /**
  138. * Determines if spacing in curly braces is valid.
  139. * @param {ASTNode} node The AST node to check.
  140. * @param {Token} first The first token to check (should be the opening brace)
  141. * @param {Token} second The second token to check (should be first after the opening brace)
  142. * @param {Token} penultimate The penultimate token to check (should be last before closing brace)
  143. * @param {Token} last The last token to check (should be closing brace)
  144. * @returns {void}
  145. */
  146. function validateBraceSpacing(node, first, second, penultimate, last) {
  147. if (astUtils.isTokenOnSameLine(first, second)) {
  148. const firstSpaced = sourceCode.isSpaceBetweenTokens(first, second);
  149. if (options.spaced && !firstSpaced) {
  150. reportRequiredBeginningSpace(node, first);
  151. }
  152. if (!options.spaced && firstSpaced && second.type !== "Line") {
  153. reportNoBeginningSpace(node, first);
  154. }
  155. }
  156. if (astUtils.isTokenOnSameLine(penultimate, last)) {
  157. const shouldCheckPenultimate = (
  158. options.arraysInObjectsException && astUtils.isClosingBracketToken(penultimate) ||
  159. options.objectsInObjectsException && astUtils.isClosingBraceToken(penultimate)
  160. );
  161. const penultimateType = shouldCheckPenultimate && sourceCode.getNodeByRangeIndex(penultimate.range[0]).type;
  162. const closingCurlyBraceMustBeSpaced = (
  163. options.arraysInObjectsException && penultimateType === "ArrayExpression" ||
  164. options.objectsInObjectsException && (penultimateType === "ObjectExpression" || penultimateType === "ObjectPattern")
  165. ) ? !options.spaced : options.spaced;
  166. const lastSpaced = sourceCode.isSpaceBetweenTokens(penultimate, last);
  167. if (closingCurlyBraceMustBeSpaced && !lastSpaced) {
  168. reportRequiredEndingSpace(node, last);
  169. }
  170. if (!closingCurlyBraceMustBeSpaced && lastSpaced) {
  171. reportNoEndingSpace(node, last);
  172. }
  173. }
  174. }
  175. /**
  176. * Gets '}' token of an object node.
  177. *
  178. * Because the last token of object patterns might be a type annotation,
  179. * this traverses tokens preceded by the last property, then returns the
  180. * first '}' token.
  181. * @param {ASTNode} node The node to get. This node is an
  182. * ObjectExpression or an ObjectPattern. And this node has one or
  183. * more properties.
  184. * @returns {Token} '}' token.
  185. */
  186. function getClosingBraceOfObject(node) {
  187. const lastProperty = node.properties[node.properties.length - 1];
  188. return sourceCode.getTokenAfter(lastProperty, astUtils.isClosingBraceToken);
  189. }
  190. /**
  191. * Reports a given object node if spacing in curly braces is invalid.
  192. * @param {ASTNode} node An ObjectExpression or ObjectPattern node to check.
  193. * @returns {void}
  194. */
  195. function checkForObject(node) {
  196. if (node.properties.length === 0) {
  197. return;
  198. }
  199. const first = sourceCode.getFirstToken(node),
  200. last = getClosingBraceOfObject(node),
  201. second = sourceCode.getTokenAfter(first, { includeComments: true }),
  202. penultimate = sourceCode.getTokenBefore(last, { includeComments: true });
  203. validateBraceSpacing(node, first, second, penultimate, last);
  204. }
  205. /**
  206. * Reports a given import node if spacing in curly braces is invalid.
  207. * @param {ASTNode} node An ImportDeclaration node to check.
  208. * @returns {void}
  209. */
  210. function checkForImport(node) {
  211. if (node.specifiers.length === 0) {
  212. return;
  213. }
  214. let firstSpecifier = node.specifiers[0];
  215. const lastSpecifier = node.specifiers[node.specifiers.length - 1];
  216. if (lastSpecifier.type !== "ImportSpecifier") {
  217. return;
  218. }
  219. if (firstSpecifier.type !== "ImportSpecifier") {
  220. firstSpecifier = node.specifiers[1];
  221. }
  222. const first = sourceCode.getTokenBefore(firstSpecifier),
  223. last = sourceCode.getTokenAfter(lastSpecifier, astUtils.isNotCommaToken),
  224. second = sourceCode.getTokenAfter(first, { includeComments: true }),
  225. penultimate = sourceCode.getTokenBefore(last, { includeComments: true });
  226. validateBraceSpacing(node, first, second, penultimate, last);
  227. }
  228. /**
  229. * Reports a given export node if spacing in curly braces is invalid.
  230. * @param {ASTNode} node An ExportNamedDeclaration node to check.
  231. * @returns {void}
  232. */
  233. function checkForExport(node) {
  234. if (node.specifiers.length === 0) {
  235. return;
  236. }
  237. const firstSpecifier = node.specifiers[0],
  238. lastSpecifier = node.specifiers[node.specifiers.length - 1],
  239. first = sourceCode.getTokenBefore(firstSpecifier),
  240. last = sourceCode.getTokenAfter(lastSpecifier, astUtils.isNotCommaToken),
  241. second = sourceCode.getTokenAfter(first, { includeComments: true }),
  242. penultimate = sourceCode.getTokenBefore(last, { includeComments: true });
  243. validateBraceSpacing(node, first, second, penultimate, last);
  244. }
  245. //--------------------------------------------------------------------------
  246. // Public
  247. //--------------------------------------------------------------------------
  248. return {
  249. // var {x} = y;
  250. ObjectPattern: checkForObject,
  251. // var y = {x: 'y'}
  252. ObjectExpression: checkForObject,
  253. // import {y} from 'x';
  254. ImportDeclaration: checkForImport,
  255. // export {name} from 'yo';
  256. ExportNamedDeclaration: checkForExport
  257. };
  258. }
  259. };