@@ -79,7 +79,7 @@ public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) {
79
79
case COMPUTED_FIELD_DEF :
80
80
checkState (!classStack .isEmpty ());
81
81
if (NodeUtil .canBeSideEffected (n .getFirstChild ())) {
82
- classStack .peek ().hasCompFieldWithSideEffect = true ;
82
+ classStack .peek ().computedFieldsWithSideEffectsToMove . push ( n ) ;
83
83
}
84
84
classStack .peek ().enterField (n );
85
85
break ;
@@ -114,13 +114,10 @@ public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) {
114
114
break ;
115
115
case COMPUTED_PROP :
116
116
checkState (!classStack .isEmpty ());
117
- if (classStack .peek ().hasCompFieldWithSideEffect ) {
118
- t .report (
119
- n ,
120
- TranspilationUtil .CANNOT_CONVERT_YET ,
121
- "Class contains computed field with side effect and computed function" );
122
- classStack .peek ().cannotConvert = true ;
123
- break ;
117
+ ClassRecord record = classStack .peek ();
118
+ if (!record .computedFieldsWithSideEffectsToMove .isEmpty ()) {
119
+ moveComputedFieldsWithSideEffectsInsideComputedFunction (t , record , n );
120
+ record .computedFieldsWithSideEffectsToMove .clear ();
124
121
}
125
122
break ;
126
123
default :
@@ -182,9 +179,72 @@ private void visitClass(NodeTraversal t) {
182
179
rewriteStaticMembers (t , currClassRecord );
183
180
}
184
181
182
+ /**
183
+ * Move computed fields with side effects into a computed function (Token is COMPUTED_PROP) to
184
+ * preserve the original behavior of possible side effects such as printing something in the call
185
+ * to a function in the computed field and function.
186
+ *
187
+ * <p>The computed fields that get moved into a computed function must be located above the
188
+ * computed function and below any other computed functions in the class. Any computed field not
189
+ * above a computed function gets extracted by {@link #extractExpressionFromCompField}.
190
+ *
191
+ * <p>E.g.
192
+ *
193
+ * <pre>
194
+ * function bar(num) {}
195
+ * class Foo {
196
+ * [bar(1)] = 9;
197
+ * [bar(2)]() {}
198
+ * }
199
+ * </pre>
200
+ *
201
+ * becomes
202
+ *
203
+ * <pre>
204
+ * function bar(num) {}
205
+ * var $jscomp$compfield$0;
206
+ * class Foo {
207
+ * [$jscomp$compfield$0] = 9;
208
+ * [($jscomp$compfield$0 = bar(1), bar(2))]() {}
209
+ * }
210
+ * </pre>
211
+ */
212
+ private void moveComputedFieldsWithSideEffectsInsideComputedFunction (
213
+ NodeTraversal t , ClassRecord record , Node computedFunction ) {
214
+ checkArgument (computedFunction .isComputedProp (), computedFunction );
215
+ Deque <Node > computedFields = record .computedFieldsWithSideEffectsToMove ;
216
+ while (!computedFields .isEmpty ()) {
217
+ Node computedField = computedFields .pop ();
218
+ Node computedFieldVar =
219
+ astFactory
220
+ .createSingleVarNameDeclaration (generateUniqueCompFieldVarName (t ))
221
+ .srcrefTreeIfMissing (record .classNode );
222
+ // `var $jscomp$compfield$0` is inserted before the class
223
+ computedFieldVar .insertBefore (record .insertionPointBeforeClass );
224
+ Node newNameNode = computedFieldVar .getFirstChild ();
225
+ Node fieldLhsExpression = computedField .getFirstChild ();
226
+ // Computed field changes from `[fieldLhs] = rhs;` to `[$jscomp$compfield$0] = rhs;`
227
+ fieldLhsExpression .replaceWith (newNameNode .cloneNode ());
228
+ Node assignComputedLhsExpressionToNewVarName =
229
+ astFactory
230
+ .createAssign (newNameNode .cloneNode (), fieldLhsExpression )
231
+ .srcrefTreeIfMissing (computedField );
232
+ Node existingComputedFunctionExpression = computedFunction .getFirstChild ();
233
+ Node newComma =
234
+ astFactory
235
+ .createComma (
236
+ assignComputedLhsExpressionToNewVarName ,
237
+ existingComputedFunctionExpression .detach ())
238
+ .srcrefTreeIfMissing (computedFunction );
239
+ // `[funcExpr]() {}` becomes `[($jscomp$compfield$0 = fieldLhs, funcExpr)]() {}`
240
+ computedFunction .addChildToFront (newComma );
241
+ }
242
+ }
243
+
185
244
/**
186
245
* Extracts the expression in the LHS of a computed field to not disturb possible side effects and
187
- * allow for this and super to be used in the LHS of a computed field in certain cases
246
+ * allow for this and super to be used in the LHS of a computed field in certain cases. Does not
247
+ * extract a computed field that was already moved into a computed function.
188
248
*
189
249
* <p>E.g.
190
250
*
@@ -207,14 +267,15 @@ private void visitClass(NodeTraversal t) {
207
267
private void extractExpressionFromCompField (
208
268
NodeTraversal t , ClassRecord record , Node memberField ) {
209
269
checkArgument (memberField .isComputedFieldDef (), memberField );
210
- checkState (
211
- !memberField .getFirstChild ().isName ()
212
- || !memberField .getFirstChild ().getString ().startsWith (COMP_FIELD_VAR ),
213
- memberField );
270
+ // Computed field was already moved into a computed function and doesn't need to be extracted
271
+ if (memberField .getFirstChild ().isName ()
272
+ && memberField .getFirstChild ().getString ().startsWith (COMP_FIELD_VAR )) {
273
+ return ;
274
+ }
275
+
214
276
Node compExpression = memberField .getFirstChild ().detach ();
215
277
Node compFieldVar =
216
- astFactory .createSingleVarNameDeclaration (
217
- COMP_FIELD_VAR + compiler .getUniqueIdSupplier ().getUniqueId (t .getInput ()));
278
+ astFactory .createSingleVarNameDeclaration (generateUniqueCompFieldVarName (t ));
218
279
Node compFieldName = compFieldVar .getFirstChild ();
219
280
memberField .addChildToFront (compFieldName .cloneNode ());
220
281
compFieldVar .insertBefore (record .insertionPointBeforeClass );
@@ -226,6 +287,11 @@ private void extractExpressionFromCompField(
226
287
exprResult .srcrefTreeIfMissing (record .classNode );
227
288
}
228
289
290
+ /** Returns $jscomp$compfield$[FILE_ID]$[number] */
291
+ private String generateUniqueCompFieldVarName (NodeTraversal t ) {
292
+ return COMP_FIELD_VAR + compiler .getUniqueIdSupplier ().getUniqueId (t .getInput ());
293
+ }
294
+
229
295
/** Rewrites and moves all instance fields */
230
296
private void rewriteInstanceMembers (NodeTraversal t , ClassRecord record ) {
231
297
Deque <Node > instanceMembers = record .instanceMembers ;
@@ -404,10 +470,6 @@ private Node findInitialInstanceInsertionPoint(Node ctorBlock) {
404
470
*/
405
471
private static final class ClassRecord {
406
472
407
- // Keeps track of if there are any computed fields with side effects in the class to throw
408
- // an error if any computed functions are encountered because it requires a unique way to
409
- // transpile to preserve the behavior of the side effects
410
- boolean hasCompFieldWithSideEffect ;
411
473
// During traversal, contains the current member being traversed. After traversal, always null
412
474
@ Nullable Node currentMember ;
413
475
boolean cannotConvert ;
@@ -416,6 +478,8 @@ private static final class ClassRecord {
416
478
final Deque <Node > instanceMembers = new ArrayDeque <>();
417
479
// Static fields + static blocks
418
480
final Deque <Node > staticMembers = new ArrayDeque <>();
481
+ // Computed fields with side effects after the most recent computed member function
482
+ final Deque <Node > computedFieldsWithSideEffectsToMove = new ArrayDeque <>();
419
483
420
484
// Mapping from MEMBER_FIELD_DEF (& COMPUTED_FIELD_DEF) nodes to all name nodes in that RHS
421
485
final SetMultimap <Node , Node > referencedNamesByMember =
0 commit comments