getProp-parser-test.js 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. /* eslint-env mocha */
  2. import assert from 'assert';
  3. import entries from 'object.entries';
  4. import fromEntries from 'object.fromentries';
  5. import { getOpeningElement, setParserName, fallbackToBabylon } from '../helper';
  6. import getProp from '../../src/getProp';
  7. const literal = {
  8. source: '<div {...{ id: "foo" }} />',
  9. target: '<div id="foo" />',
  10. offset: { keyOffset: -6, valueOffset: -7 },
  11. };
  12. const expression1 = {
  13. source: '<div {...{ id }} />',
  14. target: '<div id={id} />',
  15. offset: { keyOffset: -6, valueOffset: -2 },
  16. };
  17. const expression2 = {
  18. source: '<div {...{ id: `foo${bar}baz` }} />', // eslint-disable-line no-template-curly-in-string
  19. target: '<div id={`foo${bar}baz`} />', // eslint-disable-line no-template-curly-in-string
  20. offset: { keyOffset: -6, valueOffset: -6 },
  21. };
  22. describe('getProp', () => {
  23. it('should create the correct AST for literal with flow parser', () => {
  24. actualTest('flow', literal);
  25. });
  26. it('should create the correct AST for literal with babel parser', () => {
  27. actualTest('babel', literal);
  28. });
  29. it('should create the correct AST for expression with flow parser (1)', () => {
  30. actualTest('flow', expression1);
  31. });
  32. it('should create the correct AST for expression with babel parser (1)', () => {
  33. actualTest('babel', expression1);
  34. });
  35. it('should create the correct AST for expression with flow parser (2)', () => {
  36. actualTest('flow', expression2);
  37. });
  38. it('should create the correct AST for expression with babel parser (2)', () => {
  39. actualTest('babel', expression2);
  40. });
  41. });
  42. function actualTest(parserName, test) {
  43. setParserName(parserName);
  44. const { source, target, offset } = test;
  45. const sourceProps = stripConstructors(getOpeningElement(source).attributes);
  46. const targetProps = stripConstructors(getOpeningElement(target).attributes);
  47. const prop = 'id';
  48. const sourceResult = getProp(sourceProps, prop);
  49. const targetResult = getProp(targetProps, prop);
  50. if (fallbackToBabylon && parserName === 'babel' && test === literal) {
  51. // Babylon (node < 6) adds an `extra: null` prop to a literal if it is parsed from a
  52. // JSXAttribute, other literals don't get this.
  53. sourceResult.value.extra = null;
  54. }
  55. assert.deepStrictEqual(
  56. adjustLocations(sourceResult, offset),
  57. adjustRange(targetResult),
  58. );
  59. }
  60. function adjustRange({ name, value: { expression, ...value }, ...node }) {
  61. return {
  62. ...adjustNodeRange(node),
  63. name: adjustNodeRange(name),
  64. value: {
  65. ...adjustNodeRange(value),
  66. ...(expression ? { expression: adjustNodeRangeRecursively(expression) } : {}),
  67. },
  68. };
  69. }
  70. function adjustNodeRange(node) {
  71. if (!node.loc) {
  72. return node;
  73. }
  74. const [start, end] = node.range || [node.start, node.end];
  75. return {
  76. ...node,
  77. end: undefined,
  78. range: [start, end],
  79. start: undefined,
  80. };
  81. }
  82. function adjustNodeRangeRecursively(node) {
  83. if (Array.isArray(node)) {
  84. return node.map(adjustNodeRangeRecursively);
  85. }
  86. if (node && typeof node === 'object') {
  87. return adjustNodeRange(mapValues(node, adjustNodeRangeRecursively));
  88. }
  89. return node;
  90. }
  91. function stripConstructors(value) {
  92. return JSON.parse(JSON.stringify(value));
  93. }
  94. function adjustLocations(node, { keyOffset, valueOffset }) {
  95. const hasExpression = !!node.value.expression;
  96. return {
  97. ...adjustNodeLocations(node, {
  98. startOffset: keyOffset,
  99. endOffset: valueOffset + (hasExpression ? 1 : 0),
  100. }),
  101. name: adjustNodeLocations(node.name, { startOffset: keyOffset, endOffset: keyOffset }),
  102. value: {
  103. ...adjustNodeLocations(node.value, {
  104. startOffset: valueOffset - (hasExpression ? 1 : 0),
  105. endOffset: valueOffset + (hasExpression ? 1 : 0),
  106. }),
  107. ...(hasExpression
  108. ? {
  109. expression: adjustLocationsRecursively(
  110. node.value.expression,
  111. { startOffset: valueOffset, endOffset: valueOffset },
  112. ),
  113. }
  114. : {}
  115. ),
  116. },
  117. };
  118. }
  119. function adjustNodeLocations(node, { startOffset, endOffset }) {
  120. if (!node.loc) {
  121. return node;
  122. }
  123. const [start, end] = node.range || [];
  124. return {
  125. ...node,
  126. end: undefined,
  127. loc: {
  128. ...node.loc,
  129. start: {
  130. ...node.loc.start,
  131. column: node.loc.start.column + startOffset,
  132. },
  133. end: {
  134. ...node.loc.end,
  135. column: node.loc.end.column + endOffset,
  136. },
  137. },
  138. range: [start + startOffset, end + endOffset],
  139. start: undefined,
  140. };
  141. }
  142. function adjustLocationsRecursively(node, { startOffset, endOffset }) {
  143. if (Array.isArray(node)) {
  144. return node.map((x) => adjustLocationsRecursively(x, { startOffset, endOffset }));
  145. }
  146. if (node && typeof node === 'object') {
  147. return adjustNodeLocations(
  148. mapValues(node, (x) => adjustLocationsRecursively(x, { startOffset, endOffset })),
  149. { startOffset, endOffset },
  150. );
  151. }
  152. return node;
  153. }
  154. function mapValues(o, f) {
  155. return fromEntries(entries(o).map(([k, v]) => [k, f(v)]));
  156. }