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

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