jsx-tag-spacing.js 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. /**
  2. * @fileoverview Validates whitespace in and around the JSX opening and closing brackets
  3. * @author Diogo Franco (Kovensky)
  4. */
  5. 'use strict';
  6. const getTokenBeforeClosingBracket = require('../util/getTokenBeforeClosingBracket');
  7. const docsUrl = require('../util/docsUrl');
  8. // ------------------------------------------------------------------------------
  9. // Validators
  10. // ------------------------------------------------------------------------------
  11. function validateClosingSlash(context, node, option) {
  12. const sourceCode = context.getSourceCode();
  13. const SELF_CLOSING_NEVER_MESSAGE = 'Whitespace is forbidden between `/` and `>`; write `/>`';
  14. const SELF_CLOSING_ALWAYS_MESSAGE = 'Whitespace is required between `/` and `>`; write `/ >`';
  15. const NEVER_MESSAGE = 'Whitespace is forbidden between `<` and `/`; write `</`';
  16. const ALWAYS_MESSAGE = 'Whitespace is required between `<` and `/`; write `< /`';
  17. let adjacent;
  18. if (node.selfClosing) {
  19. const lastTokens = sourceCode.getLastTokens(node, 2);
  20. adjacent = !sourceCode.isSpaceBetweenTokens(lastTokens[0], lastTokens[1]);
  21. if (option === 'never') {
  22. if (!adjacent) {
  23. context.report({
  24. node: node,
  25. loc: {
  26. start: lastTokens[0].loc.start,
  27. end: lastTokens[1].loc.end
  28. },
  29. message: SELF_CLOSING_NEVER_MESSAGE,
  30. fix: function(fixer) {
  31. return fixer.removeRange([lastTokens[0].range[1], lastTokens[1].range[0]]);
  32. }
  33. });
  34. }
  35. } else if (option === 'always' && adjacent) {
  36. context.report({
  37. node: node,
  38. loc: {
  39. start: lastTokens[0].loc.start,
  40. end: lastTokens[1].loc.end
  41. },
  42. message: SELF_CLOSING_ALWAYS_MESSAGE,
  43. fix: function(fixer) {
  44. return fixer.insertTextBefore(lastTokens[1], ' ');
  45. }
  46. });
  47. }
  48. } else {
  49. const firstTokens = sourceCode.getFirstTokens(node, 2);
  50. adjacent = !sourceCode.isSpaceBetweenTokens(firstTokens[0], firstTokens[1]);
  51. if (option === 'never') {
  52. if (!adjacent) {
  53. context.report({
  54. node: node,
  55. loc: {
  56. start: firstTokens[0].loc.start,
  57. end: firstTokens[1].loc.end
  58. },
  59. message: NEVER_MESSAGE,
  60. fix: function(fixer) {
  61. return fixer.removeRange([firstTokens[0].range[1], firstTokens[1].range[0]]);
  62. }
  63. });
  64. }
  65. } else if (option === 'always' && adjacent) {
  66. context.report({
  67. node: node,
  68. loc: {
  69. start: firstTokens[0].loc.start,
  70. end: firstTokens[1].loc.end
  71. },
  72. message: ALWAYS_MESSAGE,
  73. fix: function(fixer) {
  74. return fixer.insertTextBefore(firstTokens[1], ' ');
  75. }
  76. });
  77. }
  78. }
  79. }
  80. function validateBeforeSelfClosing(context, node, option) {
  81. const sourceCode = context.getSourceCode();
  82. const NEVER_MESSAGE = 'A space is forbidden before closing bracket';
  83. const ALWAYS_MESSAGE = 'A space is required before closing bracket';
  84. const leftToken = getTokenBeforeClosingBracket(node);
  85. const closingSlash = sourceCode.getTokenAfter(leftToken);
  86. if (leftToken.loc.end.line !== closingSlash.loc.start.line) {
  87. return;
  88. }
  89. if (option === 'always' && !sourceCode.isSpaceBetweenTokens(leftToken, closingSlash)) {
  90. context.report({
  91. node: node,
  92. loc: closingSlash.loc.start,
  93. message: ALWAYS_MESSAGE,
  94. fix: function(fixer) {
  95. return fixer.insertTextBefore(closingSlash, ' ');
  96. }
  97. });
  98. } else if (option === 'never' && sourceCode.isSpaceBetweenTokens(leftToken, closingSlash)) {
  99. context.report({
  100. node: node,
  101. loc: closingSlash.loc.start,
  102. message: NEVER_MESSAGE,
  103. fix: function(fixer) {
  104. const previousToken = sourceCode.getTokenBefore(closingSlash);
  105. return fixer.removeRange([previousToken.range[1], closingSlash.range[0]]);
  106. }
  107. });
  108. }
  109. }
  110. function validateAfterOpening(context, node, option) {
  111. const sourceCode = context.getSourceCode();
  112. const NEVER_MESSAGE = 'A space is forbidden after opening bracket';
  113. const ALWAYS_MESSAGE = 'A space is required after opening bracket';
  114. const openingToken = sourceCode.getTokenBefore(node.name);
  115. if (option === 'allow-multiline') {
  116. if (openingToken.loc.start.line !== node.name.loc.start.line) {
  117. return;
  118. }
  119. }
  120. const adjacent = !sourceCode.isSpaceBetweenTokens(openingToken, node.name);
  121. if (option === 'never' || option === 'allow-multiline') {
  122. if (!adjacent) {
  123. context.report({
  124. node: node,
  125. loc: {
  126. start: openingToken.loc.start,
  127. end: node.name.loc.start
  128. },
  129. message: NEVER_MESSAGE,
  130. fix: function(fixer) {
  131. return fixer.removeRange([openingToken.range[1], node.name.range[0]]);
  132. }
  133. });
  134. }
  135. } else if (option === 'always' && adjacent) {
  136. context.report({
  137. node: node,
  138. loc: {
  139. start: openingToken.loc.start,
  140. end: node.name.loc.start
  141. },
  142. message: ALWAYS_MESSAGE,
  143. fix: function(fixer) {
  144. return fixer.insertTextBefore(node.name, ' ');
  145. }
  146. });
  147. }
  148. }
  149. function validateBeforeClosing(context, node, option) {
  150. // Don't enforce this rule for self closing tags
  151. if (!node.selfClosing) {
  152. const sourceCode = context.getSourceCode();
  153. const NEVER_MESSAGE = 'A space is forbidden before closing bracket';
  154. const ALWAYS_MESSAGE = 'Whitespace is required before closing bracket';
  155. const lastTokens = sourceCode.getLastTokens(node, 2);
  156. const closingToken = lastTokens[1];
  157. const leftToken = lastTokens[0];
  158. if (leftToken.loc.start.line !== closingToken.loc.start.line) {
  159. return;
  160. }
  161. const adjacent = !sourceCode.isSpaceBetweenTokens(leftToken, closingToken);
  162. if (option === 'never' && !adjacent) {
  163. context.report({
  164. node: node,
  165. loc: {
  166. start: leftToken.loc.end,
  167. end: closingToken.loc.start
  168. },
  169. message: NEVER_MESSAGE,
  170. fix: function(fixer) {
  171. return fixer.removeRange([leftToken.range[1], closingToken.range[0]]);
  172. }
  173. });
  174. } else if (option === 'always' && adjacent) {
  175. context.report({
  176. node: node,
  177. loc: {
  178. start: leftToken.loc.end,
  179. end: closingToken.loc.start
  180. },
  181. message: ALWAYS_MESSAGE,
  182. fix: function(fixer) {
  183. return fixer.insertTextBefore(closingToken, ' ');
  184. }
  185. });
  186. }
  187. }
  188. }
  189. // ------------------------------------------------------------------------------
  190. // Rule Definition
  191. // ------------------------------------------------------------------------------
  192. const optionDefaults = {
  193. closingSlash: 'never',
  194. beforeSelfClosing: 'always',
  195. afterOpening: 'never',
  196. beforeClosing: 'allow'
  197. };
  198. module.exports = {
  199. meta: {
  200. docs: {
  201. description: 'Validate whitespace in and around the JSX opening and closing brackets',
  202. category: 'Stylistic Issues',
  203. recommended: false,
  204. url: docsUrl('jsx-tag-spacing')
  205. },
  206. fixable: 'whitespace',
  207. schema: [
  208. {
  209. type: 'object',
  210. properties: {
  211. closingSlash: {
  212. enum: ['always', 'never', 'allow']
  213. },
  214. beforeSelfClosing: {
  215. enum: ['always', 'never', 'allow']
  216. },
  217. afterOpening: {
  218. enum: ['always', 'allow-multiline', 'never', 'allow']
  219. },
  220. beforeClosing: {
  221. enum: ['always', 'never', 'allow']
  222. }
  223. },
  224. default: optionDefaults,
  225. additionalProperties: false
  226. }
  227. ]
  228. },
  229. create: function (context) {
  230. const options = Object.assign({}, optionDefaults, context.options[0]);
  231. return {
  232. JSXOpeningElement: function (node) {
  233. if (options.closingSlash !== 'allow' && node.selfClosing) {
  234. validateClosingSlash(context, node, options.closingSlash);
  235. }
  236. if (options.afterOpening !== 'allow') {
  237. validateAfterOpening(context, node, options.afterOpening);
  238. }
  239. if (options.beforeSelfClosing !== 'allow' && node.selfClosing) {
  240. validateBeforeSelfClosing(context, node, options.beforeSelfClosing);
  241. }
  242. if (options.beforeClosing !== 'allow') {
  243. validateBeforeClosing(context, node, options.beforeClosing);
  244. }
  245. },
  246. JSXClosingElement: function (node) {
  247. if (options.afterOpening !== 'allow') {
  248. validateAfterOpening(context, node, options.afterOpening);
  249. }
  250. if (options.closingSlash !== 'allow') {
  251. validateClosingSlash(context, node, options.closingSlash);
  252. }
  253. if (options.beforeClosing !== 'allow') {
  254. validateBeforeClosing(context, node, options.beforeClosing);
  255. }
  256. }
  257. };
  258. }
  259. };