Skip to content

Commit b27b1f2

Browse files
alangpiercecpojer
authored andcommitted
Make create-element-to-jsx properly transfer comments (#61)
Some non-obvious aspects of this change: * For the main test, I put a comment between nearly every token. The generated output is really messy, but the main important thing is that every comment in the input appears somewhere in the output. * For closing tags, I had to make a copy of the JSXIdentifier without any comments. Otherwise, `React.createElement('div' /*foo*/, null, ' ')` would become `<div /*foo*/> </div /*foo*/>`. * I had to change the signature of `convertExpressionToJSXAttributes` to also return an `extraComments` value for any comments that didn't have an obvious attribute to attach to. This is also necessary since there might be no attributes, but still some comments. * JSXText elements seem to ignore any comments you attach to them, so I had to just use the default behavior of a JSXExpressionContainer for any child strings that have comments. * Attaching comments to JSXElement nodes that are children of other JSXElement nodes causes the comment to be incorrectly printed as a child string, e.g. `<div><span />/*a comment*/</div>`. To avoid this, whenever the script generates a child JSXElement, it wraps it in a JSXExpressionContainer if there are any comments. * It looks like some comments can be neither leading nor trailing (e.g. `{/*foo*/}`, and that makes them not print when they're transferred to other nodes. So in some cases I had to explicitly set the `leading` and `trailing` booleans when transferring comments. This also makes the comment positioning look nicer in some situations.
1 parent 6c4a2b3 commit b27b1f2

File tree

5 files changed

+156
-30
lines changed

5 files changed

+156
-30
lines changed

README.md

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,6 @@ Converts calls to `React.createElement` into JSX elements.
3838
jscodeshift -t react-codemod/transforms/create-element-to-jsx.js <path>
3939
```
4040

41-
Note that this script will **destroy comments** within `createElement` usages,
42-
so you should manually check for any lost comments after running the
43-
transform.
44-
4541
#### `findDOMNode`
4642

4743
Updates `this.getDOMNode()` or `this.refs.foo.getDOMNode()` calls inside of
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
var React = require('react');
2+
3+
const render = () => {
4+
return /*1*/React/*2*/./*3*/createElement/*4*/(
5+
/*5*/'div'/*6*/,/*7*/
6+
{
7+
/*8*/className/*9*/: /*10*/'foo'/*11*/,/*12*/
8+
/*13*/onClick/*14*/:/*15*/ this.handleClick/*16*/, //17
9+
}/*18*/,
10+
/*19*/React.createElement(/*20*/TodoList/*21*/./*22*/Item/*23*/)/*24*/, //25
11+
React.createElement(
12+
'span',
13+
/*26*/getProps()/*27*/
14+
),
15+
React.createElement('input', /*28*/null/*29*/)
16+
);
17+
};
18+
19+
const render2 = () => {
20+
return React.createElement(
21+
'div', {
22+
className: 'foo', // Prop comment.
23+
},
24+
'hello' // Child string comment.
25+
);
26+
};
27+
28+
const render3 = () => {
29+
return React.createElement(
30+
'div',
31+
null,
32+
React.createElement('span') // Child element comment.
33+
);
34+
};
35+
36+
const render4 = () => {
37+
return React.createElement(Foo, {/* No props to see here! */});
38+
};
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
var React = require('react');
2+
3+
const render = () => {
4+
return /*1*//*4*//*2*//*3*/</*5*/div/*6*//*7*//*18*/
5+
/*8*/className/*9*/=/*10*/"foo"/*11*/
6+
/*12*/
7+
/*13*///17
8+
onClick/*14*/={/*15*/ this.handleClick}/*16*/>{/*19*///25
9+
</*20*/TodoList/*21*/./*22*/Item/*23*/ />/*24*/}<span {.../*26*/getProps()/*27*/} /><input /*28*//*29*/ /></div>;
10+
};
11+
12+
const render2 = () => {
13+
return <div
14+
// Prop comment.
15+
className="foo">{// Child string comment.
16+
'hello'}</div>;
17+
};
18+
19+
const render3 = () => {
20+
return <div>{// Child element comment.
21+
<span />}</div>;
22+
};
23+
24+
const render4 = () => {
25+
return <Foo/* No props to see here! */ />;
26+
};

transforms/__tests__/create-element-to-jsx-test.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,12 @@ describe('create-element-to-jsx', () => {
132132
null,
133133
'create-element-to-jsx-no-props-arg'
134134
);
135+
defineTest(
136+
__dirname,
137+
'create-element-to-jsx',
138+
null,
139+
'create-element-to-jsx-preserve-comments'
140+
);
135141

136142
it('throws when it does not recognize a property type', () => {
137143
const jscodeshift = require('jscodeshift');

transforms/create-element-to-jsx.js

Lines changed: 86 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ module.exports = function(file, api, options) {
2020
.replace(/>/g, '&gt;');
2121

2222
const convertExpressionToJSXAttributes = (expression) => {
23+
if (!expression) {
24+
return {
25+
attributes: [],
26+
extraComments: [],
27+
};
28+
}
29+
2330
const isReactSpread = expression.type === 'CallExpression' &&
2431
expression.callee.type === 'MemberExpression' &&
2532
expression.callee.object.name === 'React' &&
@@ -37,81 +44,129 @@ module.exports = function(file, api, options) {
3744
];
3845

3946
if (isReactSpread || isObjectAssign) {
40-
const jsxAttributes = [];
41-
42-
expression.arguments.forEach((expression) =>
43-
jsxAttributes.push(...convertExpressionToJSXAttributes(expression))
44-
);
47+
const resultAttributes = [];
48+
const resultExtraComments = expression.comments || [];
49+
const {callee} = expression;
50+
for (const node of [callee, callee.object, callee.property]) {
51+
resultExtraComments.push(...(node.comments || []));
52+
}
53+
expression.arguments.forEach((expression) => {
54+
const {attributes, extraComments} = convertExpressionToJSXAttributes(expression);
55+
resultAttributes.push(...attributes);
56+
resultExtraComments.push(...extraComments);
57+
});
4558

46-
return jsxAttributes;
59+
return {
60+
attributes: resultAttributes,
61+
extraComments: resultExtraComments,
62+
};
4763
} else if (validSpreadTypes.indexOf(expression.type) != -1) {
48-
return [j.jsxSpreadAttribute(expression)];
64+
return {
65+
attributes: [j.jsxSpreadAttribute(expression)],
66+
extraComments: [],
67+
};
4968
} else if (expression.type === 'ObjectExpression') {
5069
const attributes = expression.properties.map((property) => {
5170
if (property.type === 'SpreadProperty') {
52-
return j.jsxSpreadAttribute(property.argument);
53-
} else if (property.type === 'Property') {
71+
const spreadAttribute = j.jsxSpreadAttribute(property.argument);
72+
spreadAttribute.comments = property.comments;
73+
return spreadAttribute;
74+
} else if (property.type === 'Property') {
5475
const propertyValueType = property.value.type;
5576

5677
let value;
5778
if (propertyValueType === 'Literal' && typeof property.value.value === 'string') {
5879
value = j.literal(property.value.value);
80+
value.comments = property.value.comments;
5981
} else {
6082
value = j.jsxExpressionContainer(property.value);
6183
}
6284

63-
let propertyKeyName;
85+
let jsxIdentifier;
6486
if (property.key.type === 'Literal') {
65-
propertyKeyName = property.key.value;
87+
jsxIdentifier = j.jsxIdentifier(property.key.value);
6688
} else {
67-
propertyKeyName = property.key.name;
89+
jsxIdentifier = j.jsxIdentifier(property.key.name);
6890
}
91+
jsxIdentifier.comments = property.key.comments;
6992

70-
return j.jsxAttribute(
71-
j.jsxIdentifier(propertyKeyName),
93+
const jsxAttribute = j.jsxAttribute(
94+
jsxIdentifier,
7295
value
7396
);
97+
jsxAttribute.comments = property.comments;
98+
return jsxAttribute;
7499
}
75100
return null;
76101
});
77102

78-
return attributes;
103+
return {
104+
attributes,
105+
extraComments: expression.comments || [],
106+
};
79107
} else if (expression.type === 'Literal' && expression.value === null) {
80-
return [];
108+
return {
109+
attributes: [],
110+
extraComments: expression.comments || [],
111+
}
81112
} else {
82113
throw new Error(`Unexpected attribute of type "${expression.type}"`);
83114
}
84115
};
85116

86117
const jsxIdentifierFor = node => {
118+
let identifier;
87119
if (node.type === 'Literal') {
88-
return j.jsxIdentifier(node.value);
120+
identifier = j.jsxIdentifier(node.value);
89121
} else if (node.type === 'MemberExpression') {
90-
return j.jsxMemberExpression(
122+
identifier = j.jsxMemberExpression(
91123
jsxIdentifierFor(node.object),
92124
jsxIdentifierFor(node.property)
93125
);
94126
} else {
95-
return j.jsxIdentifier(node.name);
127+
identifier = j.jsxIdentifier(node.name);
96128
}
129+
identifier.comments = node.comments;
130+
return identifier;
97131
};
98132

99133
const convertNodeToJSX = (node) => {
134+
const comments = node.value.comments;
135+
const {callee} = node.value;
136+
for (const calleeNode of [callee, callee.object, callee.property]) {
137+
for (const comment of calleeNode.comments || []) {
138+
comment.leading = true;
139+
comment.trailing = false;
140+
comments.push(comment);
141+
}
142+
}
143+
100144
const args = node.value.arguments;
101145

102146
const jsxIdentifier = jsxIdentifierFor(args[0]);
103147
const props = args[1];
104148

105-
const attributes = props ? convertExpressionToJSXAttributes(props) : [];
149+
const {attributes, extraComments} = convertExpressionToJSXAttributes(props);
150+
jsxIdentifier.comments = jsxIdentifier.comments || [];
151+
for (const comment of extraComments) {
152+
comment.leading = false;
153+
comment.trailing = true;
154+
jsxIdentifier.comments.push(comment);
155+
}
106156

107-
const children = node.value.arguments.slice(2).map((child, index) => {
108-
if (child.type === 'Literal' && typeof child.value === 'string') {
157+
const children = args.slice(2).map((child, index) => {
158+
if (child.type === 'Literal' && typeof child.value === 'string' && !child.comments) {
109159
return j.jsxText(encodeJSXTextValue(child.value));
110160
} else if (child.type === 'CallExpression' &&
111161
child.callee.object &&
112162
child.callee.object.name === 'React' &&
113163
child.callee.property.name === 'createElement') {
114-
return convertNodeToJSX(node.get('arguments', index + 2));
164+
const jsxChild = convertNodeToJSX(node.get('arguments', index + 2));
165+
if ((jsxChild.comments || []).length > 0) {
166+
return j.jsxExpressionContainer(jsxChild);
167+
} else {
168+
return jsxChild;
169+
}
115170
} else {
116171
return j.jsxExpressionContainer(child);
117172
}
@@ -120,14 +175,19 @@ module.exports = function(file, api, options) {
120175
const openingElement = j.jsxOpeningElement(jsxIdentifier, attributes);
121176

122177
if (children.length) {
123-
return j.jsxElement(
178+
const endIdentifier = Object.assign({}, jsxIdentifier, {comments: []});
179+
const element = j.jsxElement(
124180
openingElement,
125-
j.jsxClosingElement(jsxIdentifier),
181+
j.jsxClosingElement(endIdentifier),
126182
children
127183
);
184+
element.comments = comments;
185+
return element;
128186
} else {
129187
openingElement.selfClosing = true;
130-
return j.jsxElement(openingElement);
188+
const element = j.jsxElement(openingElement);
189+
element.comments = comments;
190+
return element;
131191
}
132192
};
133193

0 commit comments

Comments
 (0)