Skip to content

Commit 2f48e59

Browse files
Closure Teamcopybara-github
authored andcommitted
Handle the case of transpiling a computed field with a side effect in RewriteClassMembers.
This ensures that when transpiling computed fields, behavior is not changed for any side effects present in the computed fields and the output code behaves the exact same way as the input code. PiperOrigin-RevId: 553834440
1 parent ff2c49e commit 2f48e59

File tree

2 files changed

+219
-140
lines changed

2 files changed

+219
-140
lines changed

src/com/google/javascript/jscomp/RewriteClassMembers.java

Lines changed: 83 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ public final class RewriteClassMembers implements NodeTraversal.ScopedCallback,
3737
private final SynthesizeExplicitConstructors ctorCreator;
3838
private final Deque<ClassRecord> classStack;
3939

40+
private static final String COMP_FIELD_VAR = "$jscomp$compfield$";
41+
4042
public RewriteClassMembers(AbstractCompiler compiler) {
4143
this.compiler = compiler;
4244
this.astFactory = compiler.createAstFactory();
@@ -61,22 +63,9 @@ public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) {
6163
|| scriptFeatures.contains(Feature.CLASS_STATIC_BLOCK);
6264
case CLASS:
6365
Node classNameNode = NodeUtil.getNameNode(n);
64-
65-
if (classNameNode == null) {
66-
t.report(
67-
n, TranspilationUtil.CANNOT_CONVERT_YET, "Anonymous classes with ES2022 features");
68-
return false;
69-
}
70-
71-
@Nullable Node classInsertionPoint = getStatementDeclaringClass(n, classNameNode);
72-
73-
if (classInsertionPoint == null) {
74-
t.report(
75-
n,
76-
TranspilationUtil.CANNOT_CONVERT_YET,
77-
"Class in a non-extractable location with ES2022 features");
78-
return false;
79-
}
66+
checkState(classNameNode != null, "Class missing a name: %s", n);
67+
Node classInsertionPoint = getStatementDeclaringClass(n, classNameNode);
68+
checkState(classInsertionPoint != null, "Class was not extracted: %s", n);
8069

8170
if (!n.getFirstChild().isEmpty()
8271
&& !classNameNode.matchesQualifiedName(n.getFirstChild())) {
@@ -89,14 +78,10 @@ public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) {
8978
classStack.push(new ClassRecord(n, classNameNode.getQualifiedName(), classInsertionPoint));
9079
break;
9180
case COMPUTED_FIELD_DEF:
81+
checkState(!classStack.isEmpty());
9282
if (NodeUtil.canBeSideEffected(n.getFirstChild())) {
93-
t.report(
94-
n,
95-
TranspilationUtil.CANNOT_CONVERT_YET,
96-
"Class contains computed field with possible side effects");
97-
return false;
83+
classStack.peek().hasCompFieldWithSideEffect = true;
9884
}
99-
checkState(!classStack.isEmpty());
10085
classStack.peek().enterField(n);
10186
break;
10287
case MEMBER_FIELD_DEF:
@@ -128,6 +113,17 @@ public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) {
128113
record.potentiallyRecordNameInRhs(n);
129114
}
130115
break;
116+
case COMPUTED_PROP:
117+
checkState(!classStack.isEmpty());
118+
if (classStack.peek().hasCompFieldWithSideEffect) {
119+
t.report(
120+
n,
121+
TranspilationUtil.CANNOT_CONVERT_YET,
122+
"Class contains computed field with side effect and computed function");
123+
classStack.peek().cannotConvert = true;
124+
break;
125+
}
126+
break;
131127
default:
132128
break;
133129
}
@@ -183,11 +179,54 @@ private void visitClass(NodeTraversal t) {
183179
if (currClassRecord.cannotConvert) {
184180
return;
185181
}
186-
187182
rewriteInstanceMembers(t, currClassRecord);
188183
rewriteStaticMembers(t, currClassRecord);
189184
}
190185

186+
/**
187+
* Extracts the expression in the LHS of a computed field to not disturb possible side effects and
188+
* allow for this and super to be used in the LHS of a computed field in certain cases
189+
*
190+
* <p>E.g.
191+
*
192+
* <pre>
193+
* class Foo {
194+
* [bar('str')] = 4;
195+
* }
196+
* </pre>
197+
*
198+
* becomes
199+
*
200+
* <pre>
201+
* var $jscomp$computedfield$0;
202+
* class Foo {
203+
* [$jscomp$computedfield$0] = 4;
204+
* }
205+
* $jscomp$computedfield$0 = bar('str');
206+
* </pre>
207+
*/
208+
private void extractExpressionFromCompField(
209+
NodeTraversal t, ClassRecord record, Node memberField) {
210+
checkArgument(memberField.isComputedFieldDef(), memberField);
211+
checkState(
212+
!memberField.getFirstChild().isName()
213+
|| !memberField.getFirstChild().getString().startsWith(COMP_FIELD_VAR),
214+
memberField);
215+
Node compExpression = memberField.getFirstChild().detach();
216+
Node compFieldVar =
217+
astFactory.createSingleVarNameDeclaration(
218+
COMP_FIELD_VAR + compiler.getUniqueIdSupplier().getUniqueId(t.getInput()));
219+
Node compFieldName = compFieldVar.getFirstChild();
220+
memberField.addChildToFront(compFieldName.cloneNode());
221+
compFieldVar.insertBefore(record.insertionPointBeforeClass);
222+
compFieldVar.srcrefTreeIfMissing(record.classNode);
223+
Node exprResult =
224+
astFactory.exprResult(astFactory.createAssign(compFieldName.cloneNode(), compExpression));
225+
exprResult.insertAfter(record.insertionPointAfterClass);
226+
record.insertionPointAfterClass = exprResult;
227+
exprResult.srcrefTreeIfMissing(record.classNode);
228+
}
229+
191230
/** Rewrites and moves all instance fields */
192231
private void rewriteInstanceMembers(NodeTraversal t, ClassRecord record) {
193232
Deque<Node> instanceMembers = record.instanceMembers;
@@ -218,6 +257,10 @@ private void rewriteInstanceMembers(NodeTraversal t, ClassRecord record) {
218257
}
219258

220259
Node thisNode = astFactory.createThisForEs6ClassMember(instanceMember);
260+
if (instanceMember.isComputedFieldDef()
261+
&& NodeUtil.canBeSideEffected(instanceMember.getFirstChild())) {
262+
extractExpressionFromCompField(t, record, instanceMember);
263+
}
221264
Node transpiledNode =
222265
instanceMember.isMemberFieldDef()
223266
? convNonCompFieldToGetProp(thisNode, instanceMember.detach())
@@ -256,12 +299,15 @@ private void rewriteStaticMembers(NodeTraversal t, ClassRecord record) {
256299
transpiledNode = convNonCompFieldToGetProp(nameToUse, staticMember.detach());
257300
break;
258301
case COMPUTED_FIELD_DEF:
302+
if (NodeUtil.canBeSideEffected(staticMember.getFirstChild())) {
303+
extractExpressionFromCompField(t, record, staticMember);
304+
}
259305
transpiledNode = convCompFieldToGetElem(nameToUse, staticMember.detach());
260306
break;
261307
default:
262308
throw new IllegalStateException(String.valueOf(staticMember));
263309
}
264-
transpiledNode.insertAfter(record.classInsertionPoint);
310+
transpiledNode.insertAfter(record.insertionPointAfterClass);
265311
t.reportCodeChange();
266312
}
267313
}
@@ -361,6 +407,11 @@ private Node findInitialInstanceInsertionPoint(Node ctorBlock) {
361407
* Accumulates information about different classes while going down the AST in shouldTraverse()
362408
*/
363409
private static final class ClassRecord {
410+
411+
// Keeps track of if there are any computed fields with side effects in the class to throw
412+
// an error if any computed functions are encountered because it requires a unique way to
413+
// transpile to preserve the behavior of the side effects
414+
boolean hasCompFieldWithSideEffect;
364415
// During traversal, contains the current member being traversed. After traversal, always null
365416
@Nullable Node currentMember;
366417
boolean cannotConvert;
@@ -378,12 +429,18 @@ private static final class ClassRecord {
378429

379430
final Node classNode;
380431
final String classNameString;
381-
final Node classInsertionPoint;
432+
433+
// Keeps track of the statement declaring the class
434+
final Node insertionPointBeforeClass;
435+
436+
// Keeps track of the last statement inserted after the class
437+
Node insertionPointAfterClass;
382438

383439
ClassRecord(Node classNode, String classNameString, Node classInsertionPoint) {
384440
this.classNode = classNode;
385441
this.classNameString = classNameString;
386-
this.classInsertionPoint = classInsertionPoint;
442+
this.insertionPointBeforeClass = classInsertionPoint;
443+
this.insertionPointAfterClass = classInsertionPoint;
387444
}
388445

389446
void enterField(Node field) {

0 commit comments

Comments
 (0)