object-curly-even-spacing.js 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. 'use strict'
  2. /**
  3. * @fileoverview Disallows or enforces spaces inside of object literals.
  4. * @author Jamund Ferguson
  5. * @copyright 2014 Brandyn Bennett. All rights reserved.
  6. * @copyright 2014 Michael Ficarra. No rights reserved.
  7. * @copyright 2014 Vignesh Anand. All rights reserved.
  8. * @copyright 2015 Jamund Ferguson. All rights reserved.
  9. */
  10. // ------------------------------------------------------------------------------
  11. // Rule Definition
  12. // ------------------------------------------------------------------------------
  13. module.exports = {
  14. meta: {
  15. type: 'layout',
  16. docs: {
  17. url: 'https://github.com/standard/eslint-plugin-standard#rules-explanations'
  18. }
  19. },
  20. create: function (context) {
  21. const spaced = context.options[0] === 'always'
  22. const either = context.options[0] === 'either'
  23. /**
  24. * Determines whether an option is set, relative to the spacing option.
  25. * If spaced is "always", then check whether option is set to false.
  26. * If spaced is "never", then check whether option is set to true.
  27. * @param {Object} option - The option to exclude.
  28. * @returns {boolean} Whether or not the property is excluded.
  29. */
  30. function isOptionSet (option) {
  31. return context.options[1] != null ? context.options[1][option] === !spaced : false
  32. }
  33. const options = {
  34. spaced,
  35. either,
  36. arraysInObjectsException: isOptionSet('arraysInObjects'),
  37. objectsInObjectsException: isOptionSet('objectsInObjects')
  38. }
  39. // --------------------------------------------------------------------------
  40. // Helpers
  41. // --------------------------------------------------------------------------
  42. /**
  43. * Determines whether two adjacent tokens are have whitespace between them.
  44. * @param {Object} left - The left token object.
  45. * @param {Object} right - The right token object.
  46. * @returns {boolean} Whether or not there is space between the tokens.
  47. */
  48. function isSpaced (left, right) {
  49. return left.range[1] < right.range[0]
  50. }
  51. /**
  52. * Determines whether two adjacent tokens are on the same line.
  53. * @param {Object} left - The left token object.
  54. * @param {Object} right - The right token object.
  55. * @returns {boolean} Whether or not the tokens are on the same line.
  56. */
  57. function isSameLine (left, right) {
  58. return left.loc.start.line === right.loc.start.line
  59. }
  60. /**
  61. * Reports that there shouldn't be a space after the first token
  62. * @param {ASTNode} node - The node to report in the event of an error.
  63. * @param {Token} token - The token to use for the report.
  64. * @returns {void}
  65. */
  66. function reportNoBeginningSpace (node, token) {
  67. context.report(node, token.loc.start,
  68. "There should be no space after '" + token.value + "'")
  69. }
  70. /**
  71. * Reports that there shouldn't be a space before the last token
  72. * @param {ASTNode} node - The node to report in the event of an error.
  73. * @param {Token} token - The token to use for the report.
  74. * @returns {void}
  75. */
  76. function reportNoEndingSpace (node, token) {
  77. context.report(node, token.loc.start,
  78. "There should be no space before '" + token.value + "'")
  79. }
  80. /**
  81. * Reports that there should be a space after the first token
  82. * @param {ASTNode} node - The node to report in the event of an error.
  83. * @param {Token} token - The token to use for the report.
  84. * @returns {void}
  85. */
  86. function reportRequiredBeginningSpace (node, token) {
  87. context.report(node, token.loc.start,
  88. "A space is required after '" + token.value + "'")
  89. }
  90. /**
  91. * Reports that there should be a space before the last token
  92. * @param {ASTNode} node - The node to report in the event of an error.
  93. * @param {Token} token - The token to use for the report.
  94. * @returns {void}
  95. */
  96. function reportRequiredEndingSpace (node, token) {
  97. context.report(node, token.loc.start,
  98. "A space is required before '" + token.value + "'")
  99. }
  100. /**
  101. * Checks if a start and end brace in a node are spaced evenly
  102. * and not too long (>1 space)
  103. * @param node
  104. * @param start
  105. * @param end
  106. * @returns {boolean}
  107. */
  108. function isEvenlySpacedAndNotTooLong (node, start, end) {
  109. const expectedSpace = start[1].range[0] - start[0].range[1]
  110. const endSpace = end[1].range[0] - end[0].range[1]
  111. return endSpace === expectedSpace && endSpace <= 1
  112. }
  113. /**
  114. * Determines if spacing in curly braces is valid.
  115. * @param {ASTNode} node The AST node to check.
  116. * @param {Token} first The first token to check (should be the opening brace)
  117. * @param {Token} second The second token to check (should be first after the opening brace)
  118. * @param {Token} penultimate The penultimate token to check (should be last before closing brace)
  119. * @param {Token} last The last token to check (should be closing brace)
  120. * @returns {void}
  121. */
  122. function validateBraceSpacing (node, first, second, penultimate, last) {
  123. const closingCurlyBraceMustBeSpaced =
  124. (options.arraysInObjectsException && penultimate.value === ']') ||
  125. (options.objectsInObjectsException && penultimate.value === '}')
  126. ? !options.spaced
  127. : options.spaced
  128. // we only care about evenly spaced things
  129. if (options.either) {
  130. // newlines at any point means return
  131. if (!isSameLine(first, last)) {
  132. return
  133. }
  134. // confirm that the object expression/literal is spaced evenly
  135. if (!isEvenlySpacedAndNotTooLong(node, [first, second], [penultimate, last])) {
  136. context.report(node, 'Expected consistent spacing')
  137. }
  138. return
  139. }
  140. // { and key are on same line
  141. if (isSameLine(first, second)) {
  142. if (options.spaced && !isSpaced(first, second)) {
  143. reportRequiredBeginningSpace(node, first)
  144. }
  145. if (!options.spaced && isSpaced(first, second)) {
  146. reportNoBeginningSpace(node, first)
  147. }
  148. }
  149. // final key and } ore on the same line
  150. if (isSameLine(penultimate, last)) {
  151. if (closingCurlyBraceMustBeSpaced && !isSpaced(penultimate, last)) {
  152. reportRequiredEndingSpace(node, last)
  153. }
  154. if (!closingCurlyBraceMustBeSpaced && isSpaced(penultimate, last)) {
  155. reportNoEndingSpace(node, last)
  156. }
  157. }
  158. }
  159. // --------------------------------------------------------------------------
  160. // Public
  161. // --------------------------------------------------------------------------
  162. return {
  163. // var {x} = y
  164. ObjectPattern: function (node) {
  165. if (node.properties.length === 0) {
  166. return
  167. }
  168. const firstSpecifier = node.properties[0]
  169. const lastSpecifier = node.properties[node.properties.length - 1]
  170. const first = context.getTokenBefore(firstSpecifier)
  171. const second = context.getFirstToken(firstSpecifier)
  172. let penultimate = context.getLastToken(lastSpecifier)
  173. let last = context.getTokenAfter(lastSpecifier)
  174. // support trailing commas
  175. if (last.value === ',') {
  176. penultimate = last
  177. last = context.getTokenAfter(last)
  178. }
  179. validateBraceSpacing(node, first, second, penultimate, last)
  180. },
  181. // import {y} from 'x'
  182. ImportDeclaration: function (node) {
  183. const firstSpecifier = node.specifiers[0]
  184. const lastSpecifier = node.specifiers[node.specifiers.length - 1]
  185. // don't do anything for namespace or default imports
  186. if (firstSpecifier && lastSpecifier && firstSpecifier.type === 'ImportSpecifier' && lastSpecifier.type === 'ImportSpecifier') {
  187. const first = context.getTokenBefore(firstSpecifier)
  188. const second = context.getFirstToken(firstSpecifier)
  189. const penultimate = context.getLastToken(lastSpecifier)
  190. const last = context.getTokenAfter(lastSpecifier)
  191. validateBraceSpacing(node, first, second, penultimate, last)
  192. }
  193. },
  194. // export {name} from 'yo'
  195. ExportNamedDeclaration: function (node) {
  196. if (!node.specifiers.length) {
  197. return
  198. }
  199. const firstSpecifier = node.specifiers[0]
  200. const lastSpecifier = node.specifiers[node.specifiers.length - 1]
  201. const first = context.getTokenBefore(firstSpecifier)
  202. const second = context.getFirstToken(firstSpecifier)
  203. const penultimate = context.getLastToken(lastSpecifier)
  204. const last = context.getTokenAfter(lastSpecifier)
  205. validateBraceSpacing(node, first, second, penultimate, last)
  206. },
  207. // var y = {x: 'y'}
  208. ObjectExpression: function (node) {
  209. if (node.properties.length === 0) {
  210. return
  211. }
  212. const first = context.getFirstToken(node)
  213. const second = context.getFirstToken(node, 1)
  214. const penultimate = context.getLastToken(node, 1)
  215. const last = context.getLastToken(node)
  216. validateBraceSpacing(node, first, second, penultimate, last)
  217. }
  218. }
  219. }
  220. }