jsx-props-no-multi-spaces.js 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. /**
  2. * @fileoverview Disallow multiple spaces between inline JSX props
  3. * @author Adrian Moennich
  4. */
  5. 'use strict';
  6. const docsUrl = require('../util/docsUrl');
  7. const report = require('../util/report');
  8. // ------------------------------------------------------------------------------
  9. // Rule Definition
  10. // ------------------------------------------------------------------------------
  11. const messages = {
  12. noLineGap: 'Expected no line gap between “{{prop1}}” and “{{prop2}}”',
  13. onlyOneSpace: 'Expected only one space between “{{prop1}}” and “{{prop2}}”',
  14. };
  15. module.exports = {
  16. meta: {
  17. docs: {
  18. description: 'Disallow multiple spaces between inline JSX props',
  19. category: 'Stylistic Issues',
  20. recommended: false,
  21. url: docsUrl('jsx-props-no-multi-spaces'),
  22. },
  23. fixable: 'code',
  24. messages,
  25. schema: [],
  26. },
  27. create(context) {
  28. const sourceCode = context.getSourceCode();
  29. function getPropName(propNode) {
  30. switch (propNode.type) {
  31. case 'JSXSpreadAttribute':
  32. return context.getSourceCode().getText(propNode.argument);
  33. case 'JSXIdentifier':
  34. return propNode.name;
  35. case 'JSXMemberExpression':
  36. return `${getPropName(propNode.object)}.${propNode.property.name}`;
  37. default:
  38. return propNode.name
  39. ? propNode.name.name
  40. : `${context.getSourceCode().getText(propNode.object)}.${propNode.property.name}`; // needed for typescript-eslint parser
  41. }
  42. }
  43. // First and second must be adjacent nodes
  44. function hasEmptyLines(first, second) {
  45. const comments = sourceCode.getCommentsBefore ? sourceCode.getCommentsBefore(second) : [];
  46. const nodes = [].concat(first, comments, second);
  47. for (let i = 1; i < nodes.length; i += 1) {
  48. const prev = nodes[i - 1];
  49. const curr = nodes[i];
  50. if (curr.loc.start.line - prev.loc.end.line >= 2) {
  51. return true;
  52. }
  53. }
  54. return false;
  55. }
  56. function checkSpacing(prev, node) {
  57. if (hasEmptyLines(prev, node)) {
  58. report(context, messages.noLineGap, 'noLineGap', {
  59. node,
  60. data: {
  61. prop1: getPropName(prev),
  62. prop2: getPropName(node),
  63. },
  64. });
  65. }
  66. if (prev.loc.end.line !== node.loc.end.line) {
  67. return;
  68. }
  69. const between = context.getSourceCode().text.slice(prev.range[1], node.range[0]);
  70. if (between !== ' ') {
  71. report(context, messages.onlyOneSpace, 'onlyOneSpace', {
  72. node,
  73. data: {
  74. prop1: getPropName(prev),
  75. prop2: getPropName(node),
  76. },
  77. fix(fixer) {
  78. return fixer.replaceTextRange([prev.range[1], node.range[0]], ' ');
  79. },
  80. });
  81. }
  82. }
  83. function containsGenericType(node) {
  84. const containsTypeParams = typeof node.typeParameters !== 'undefined';
  85. return containsTypeParams && node.typeParameters.type === 'TSTypeParameterInstantiation';
  86. }
  87. function getGenericNode(node) {
  88. const name = node.name;
  89. if (containsGenericType(node)) {
  90. const type = node.typeParameters;
  91. return Object.assign(
  92. {},
  93. node,
  94. {
  95. range: [
  96. name.range[0],
  97. type.range[1],
  98. ],
  99. }
  100. );
  101. }
  102. return name;
  103. }
  104. return {
  105. JSXOpeningElement(node) {
  106. node.attributes.reduce((prev, prop) => {
  107. checkSpacing(prev, prop);
  108. return prop;
  109. }, getGenericNode(node));
  110. },
  111. };
  112. },
  113. };