jsx-curly-spacing.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401
  1. /**
  2. * @fileoverview Enforce or disallow spaces inside of curly braces in JSX attributes.
  3. * @author Jamund Ferguson
  4. * @author Brandyn Bennett
  5. * @author Michael Ficarra
  6. * @author Vignesh Anand
  7. * @author Jamund Ferguson
  8. * @author Yannick Croissant
  9. * @author Erik Wendel
  10. */
  11. 'use strict';
  12. const has = require('has');
  13. const docsUrl = require('../util/docsUrl');
  14. // ------------------------------------------------------------------------------
  15. // Rule Definition
  16. // ------------------------------------------------------------------------------
  17. const SPACING = {
  18. always: 'always',
  19. never: 'never'
  20. };
  21. const SPACING_VALUES = [SPACING.always, SPACING.never];
  22. module.exports = {
  23. meta: {
  24. docs: {
  25. description: 'Enforce or disallow spaces inside of curly braces in JSX attributes',
  26. category: 'Stylistic Issues',
  27. recommended: false,
  28. url: docsUrl('jsx-curly-spacing')
  29. },
  30. fixable: 'code',
  31. schema: {
  32. definitions: {
  33. basicConfig: {
  34. type: 'object',
  35. properties: {
  36. when: {
  37. enum: SPACING_VALUES
  38. },
  39. allowMultiline: {
  40. type: 'boolean'
  41. },
  42. spacing: {
  43. type: 'object',
  44. properties: {
  45. objectLiterals: {
  46. enum: SPACING_VALUES
  47. }
  48. }
  49. }
  50. }
  51. },
  52. basicConfigOrBoolean: {
  53. oneOf: [{
  54. $ref: '#/definitions/basicConfig'
  55. }, {
  56. type: 'boolean'
  57. }]
  58. }
  59. },
  60. type: 'array',
  61. items: [{
  62. oneOf: [{
  63. allOf: [{
  64. $ref: '#/definitions/basicConfig'
  65. }, {
  66. type: 'object',
  67. properties: {
  68. attributes: {
  69. $ref: '#/definitions/basicConfigOrBoolean'
  70. },
  71. children: {
  72. $ref: '#/definitions/basicConfigOrBoolean'
  73. }
  74. }
  75. }]
  76. }, {
  77. enum: SPACING_VALUES
  78. }]
  79. }, {
  80. type: 'object',
  81. properties: {
  82. allowMultiline: {
  83. type: 'boolean'
  84. },
  85. spacing: {
  86. type: 'object',
  87. properties: {
  88. objectLiterals: {
  89. enum: SPACING_VALUES
  90. }
  91. }
  92. }
  93. },
  94. additionalProperties: false
  95. }]
  96. }
  97. },
  98. create: function(context) {
  99. function normalizeConfig(configOrTrue, defaults, lastPass) {
  100. const config = configOrTrue === true ? {} : configOrTrue;
  101. const when = config.when || defaults.when;
  102. const allowMultiline = has(config, 'allowMultiline') ? config.allowMultiline : defaults.allowMultiline;
  103. const spacing = config.spacing || {};
  104. let objectLiteralSpaces = spacing.objectLiterals || defaults.objectLiteralSpaces;
  105. if (lastPass) {
  106. // On the final pass assign the values that should be derived from others if they are still undefined
  107. objectLiteralSpaces = objectLiteralSpaces || when;
  108. }
  109. return {
  110. when,
  111. allowMultiline,
  112. objectLiteralSpaces
  113. };
  114. }
  115. const DEFAULT_WHEN = SPACING.never;
  116. const DEFAULT_ALLOW_MULTILINE = true;
  117. const DEFAULT_ATTRIBUTES = true;
  118. const DEFAULT_CHILDREN = false;
  119. const sourceCode = context.getSourceCode();
  120. let originalConfig = context.options[0] || {};
  121. if (SPACING_VALUES.indexOf(originalConfig) !== -1) {
  122. originalConfig = Object.assign({when: context.options[0]}, context.options[1]);
  123. }
  124. const defaultConfig = normalizeConfig(originalConfig, {
  125. when: DEFAULT_WHEN,
  126. allowMultiline: DEFAULT_ALLOW_MULTILINE
  127. });
  128. const attributes = has(originalConfig, 'attributes') ? originalConfig.attributes : DEFAULT_ATTRIBUTES;
  129. const attributesConfig = attributes ? normalizeConfig(attributes, defaultConfig, true) : null;
  130. const children = has(originalConfig, 'children') ? originalConfig.children : DEFAULT_CHILDREN;
  131. const childrenConfig = children ? normalizeConfig(children, defaultConfig, true) : null;
  132. // --------------------------------------------------------------------------
  133. // Helpers
  134. // --------------------------------------------------------------------------
  135. /**
  136. * Determines whether two adjacent tokens have a newline between them.
  137. * @param {Object} left - The left token object.
  138. * @param {Object} right - The right token object.
  139. * @returns {boolean} Whether or not there is a newline between the tokens.
  140. */
  141. function isMultiline(left, right) {
  142. return left.loc.end.line !== right.loc.start.line;
  143. }
  144. /**
  145. * Trims text of whitespace between two ranges
  146. * @param {Fixer} fixer - the eslint fixer object
  147. * @param {Location} fromLoc - the start location
  148. * @param {Location} toLoc - the end location
  149. * @param {string} mode - either 'start' or 'end'
  150. * @param {string=} spacing - a spacing value that will optionally add a space to the removed text
  151. * @returns {Object|*|{range, text}}
  152. */
  153. function fixByTrimmingWhitespace(fixer, fromLoc, toLoc, mode, spacing) {
  154. let replacementText = sourceCode.text.slice(fromLoc, toLoc);
  155. if (mode === 'start') {
  156. replacementText = replacementText.replace(/^\s+/gm, '');
  157. } else {
  158. replacementText = replacementText.replace(/\s+$/gm, '');
  159. }
  160. if (spacing === SPACING.always) {
  161. if (mode === 'start') {
  162. replacementText += ' ';
  163. } else {
  164. replacementText = ` ${replacementText}`;
  165. }
  166. }
  167. return fixer.replaceTextRange([fromLoc, toLoc], replacementText);
  168. }
  169. /**
  170. * Reports that there shouldn't be a newline after the first token
  171. * @param {ASTNode} node - The node to report in the event of an error.
  172. * @param {Token} token - The token to use for the report.
  173. * @returns {void}
  174. */
  175. function reportNoBeginningNewline(node, token, spacing) {
  176. context.report({
  177. node: node,
  178. loc: token.loc.start,
  179. message: `There should be no newline after '${token.value}'`,
  180. fix: function(fixer) {
  181. const nextToken = sourceCode.getTokenAfter(token);
  182. return fixByTrimmingWhitespace(fixer, token.range[1], nextToken.range[0], 'start', spacing);
  183. }
  184. });
  185. }
  186. /**
  187. * Reports that there shouldn't be a newline before the last token
  188. * @param {ASTNode} node - The node to report in the event of an error.
  189. * @param {Token} token - The token to use for the report.
  190. * @returns {void}
  191. */
  192. function reportNoEndingNewline(node, token, spacing) {
  193. context.report({
  194. node: node,
  195. loc: token.loc.start,
  196. message: `There should be no newline before '${token.value}'`,
  197. fix: function(fixer) {
  198. const previousToken = sourceCode.getTokenBefore(token);
  199. return fixByTrimmingWhitespace(fixer, previousToken.range[1], token.range[0], 'end', spacing);
  200. }
  201. });
  202. }
  203. /**
  204. * Reports that there shouldn't be a space after the first token
  205. * @param {ASTNode} node - The node to report in the event of an error.
  206. * @param {Token} token - The token to use for the report.
  207. * @returns {void}
  208. */
  209. function reportNoBeginningSpace(node, token) {
  210. context.report({
  211. node: node,
  212. loc: token.loc.start,
  213. message: `There should be no space after '${token.value}'`,
  214. fix: function(fixer) {
  215. const nextToken = sourceCode.getTokenAfter(token);
  216. let nextComment;
  217. // ESLint >=4.x
  218. if (sourceCode.getCommentsAfter) {
  219. nextComment = sourceCode.getCommentsAfter(token);
  220. // ESLint 3.x
  221. } else {
  222. const potentialComment = sourceCode.getTokenAfter(token, {includeComments: true});
  223. nextComment = nextToken === potentialComment ? [] : [potentialComment];
  224. }
  225. // Take comments into consideration to narrow the fix range to what is actually affected. (See #1414)
  226. if (nextComment.length > 0) {
  227. return fixByTrimmingWhitespace(fixer, token.range[1], Math.min(nextToken.range[0], nextComment[0].start), 'start');
  228. }
  229. return fixByTrimmingWhitespace(fixer, token.range[1], nextToken.range[0], 'start');
  230. }
  231. });
  232. }
  233. /**
  234. * Reports that there shouldn't be a space before the last token
  235. * @param {ASTNode} node - The node to report in the event of an error.
  236. * @param {Token} token - The token to use for the report.
  237. * @returns {void}
  238. */
  239. function reportNoEndingSpace(node, token) {
  240. context.report({
  241. node: node,
  242. loc: token.loc.start,
  243. message: `There should be no space before '${token.value}'`,
  244. fix: function(fixer) {
  245. const previousToken = sourceCode.getTokenBefore(token);
  246. let previousComment;
  247. // ESLint >=4.x
  248. if (sourceCode.getCommentsBefore) {
  249. previousComment = sourceCode.getCommentsBefore(token);
  250. // ESLint 3.x
  251. } else {
  252. const potentialComment = sourceCode.getTokenBefore(token, {includeComments: true});
  253. previousComment = previousToken === potentialComment ? [] : [potentialComment];
  254. }
  255. // Take comments into consideration to narrow the fix range to what is actually affected. (See #1414)
  256. if (previousComment.length > 0) {
  257. return fixByTrimmingWhitespace(fixer, Math.max(previousToken.range[1], previousComment[0].end), token.range[0], 'end');
  258. }
  259. return fixByTrimmingWhitespace(fixer, previousToken.range[1], token.range[0], 'end');
  260. }
  261. });
  262. }
  263. /**
  264. * Reports that there should be a space after the first token
  265. * @param {ASTNode} node - The node to report in the event of an error.
  266. * @param {Token} token - The token to use for the report.
  267. * @returns {void}
  268. */
  269. function reportRequiredBeginningSpace(node, token) {
  270. context.report({
  271. node: node,
  272. loc: token.loc.start,
  273. message: `A space is required after '${token.value}'`,
  274. fix: function(fixer) {
  275. return fixer.insertTextAfter(token, ' ');
  276. }
  277. });
  278. }
  279. /**
  280. * Reports that there should be a space before the last token
  281. * @param {ASTNode} node - The node to report in the event of an error.
  282. * @param {Token} token - The token to use for the report.
  283. * @returns {void}
  284. */
  285. function reportRequiredEndingSpace(node, token) {
  286. context.report({
  287. node: node,
  288. loc: token.loc.start,
  289. message: `A space is required before '${token.value}'`,
  290. fix: function(fixer) {
  291. return fixer.insertTextBefore(token, ' ');
  292. }
  293. });
  294. }
  295. /**
  296. * Determines if spacing in curly braces is valid.
  297. * @param {ASTNode} node The AST node to check.
  298. * @returns {void}
  299. */
  300. function validateBraceSpacing(node) {
  301. let config;
  302. switch (node.parent.type) {
  303. case 'JSXAttribute':
  304. case 'JSXOpeningElement':
  305. config = attributesConfig;
  306. break;
  307. case 'JSXElement':
  308. config = childrenConfig;
  309. break;
  310. default:
  311. return;
  312. }
  313. if (config === null) {
  314. return;
  315. }
  316. const first = context.getFirstToken(node);
  317. const last = sourceCode.getLastToken(node);
  318. let second = context.getTokenAfter(first, {includeComments: true});
  319. let penultimate = sourceCode.getTokenBefore(last, {includeComments: true});
  320. if (!second) {
  321. second = context.getTokenAfter(first);
  322. const leadingComments = sourceCode.getNodeByRangeIndex(second.range[0]).leadingComments;
  323. second = leadingComments ? leadingComments[0] : second;
  324. }
  325. if (!penultimate) {
  326. penultimate = sourceCode.getTokenBefore(last);
  327. const trailingComments = sourceCode.getNodeByRangeIndex(penultimate.range[0]).trailingComments;
  328. penultimate = trailingComments ? trailingComments[trailingComments.length - 1] : penultimate;
  329. }
  330. const isObjectLiteral = first.value === second.value;
  331. const spacing = isObjectLiteral ? config.objectLiteralSpaces : config.when;
  332. if (spacing === SPACING.always) {
  333. if (!sourceCode.isSpaceBetweenTokens(first, second)) {
  334. reportRequiredBeginningSpace(node, first);
  335. } else if (!config.allowMultiline && isMultiline(first, second)) {
  336. reportNoBeginningNewline(node, first, spacing);
  337. }
  338. if (!sourceCode.isSpaceBetweenTokens(penultimate, last)) {
  339. reportRequiredEndingSpace(node, last);
  340. } else if (!config.allowMultiline && isMultiline(penultimate, last)) {
  341. reportNoEndingNewline(node, last, spacing);
  342. }
  343. } else if (spacing === SPACING.never) {
  344. if (isMultiline(first, second)) {
  345. if (!config.allowMultiline) {
  346. reportNoBeginningNewline(node, first, spacing);
  347. }
  348. } else if (sourceCode.isSpaceBetweenTokens(first, second)) {
  349. reportNoBeginningSpace(node, first);
  350. }
  351. if (isMultiline(penultimate, last)) {
  352. if (!config.allowMultiline) {
  353. reportNoEndingNewline(node, last, spacing);
  354. }
  355. } else if (sourceCode.isSpaceBetweenTokens(penultimate, last)) {
  356. reportNoEndingSpace(node, last);
  357. }
  358. }
  359. }
  360. // --------------------------------------------------------------------------
  361. // Public
  362. // --------------------------------------------------------------------------
  363. return {
  364. JSXExpressionContainer: validateBraceSpacing,
  365. JSXSpreadAttribute: validateBraceSpacing
  366. };
  367. }
  368. };