Skip to content

Commit fbc7922

Browse files
Closure Teamcopybara-github
Closure Team
authored andcommitted
Add functionality of moving computed fields with side effects into computed member functions in RewriteClassMembers.
This ensures that the original behavior of the side effects is preserved if the computed member functions also contain side effects. PiperOrigin-RevId: 555190394
1 parent 581a65d commit fbc7922

File tree

2 files changed

+113
-53
lines changed

2 files changed

+113
-53
lines changed

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

+83-19
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) {
7979
case COMPUTED_FIELD_DEF:
8080
checkState(!classStack.isEmpty());
8181
if (NodeUtil.canBeSideEffected(n.getFirstChild())) {
82-
classStack.peek().hasCompFieldWithSideEffect = true;
82+
classStack.peek().computedFieldsWithSideEffectsToMove.push(n);
8383
}
8484
classStack.peek().enterField(n);
8585
break;
@@ -114,13 +114,10 @@ public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) {
114114
break;
115115
case COMPUTED_PROP:
116116
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();
124121
}
125122
break;
126123
default:
@@ -182,9 +179,72 @@ private void visitClass(NodeTraversal t) {
182179
rewriteStaticMembers(t, currClassRecord);
183180
}
184181

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+
185244
/**
186245
* 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.
188248
*
189249
* <p>E.g.
190250
*
@@ -207,14 +267,15 @@ private void visitClass(NodeTraversal t) {
207267
private void extractExpressionFromCompField(
208268
NodeTraversal t, ClassRecord record, Node memberField) {
209269
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+
214276
Node compExpression = memberField.getFirstChild().detach();
215277
Node compFieldVar =
216-
astFactory.createSingleVarNameDeclaration(
217-
COMP_FIELD_VAR + compiler.getUniqueIdSupplier().getUniqueId(t.getInput()));
278+
astFactory.createSingleVarNameDeclaration(generateUniqueCompFieldVarName(t));
218279
Node compFieldName = compFieldVar.getFirstChild();
219280
memberField.addChildToFront(compFieldName.cloneNode());
220281
compFieldVar.insertBefore(record.insertionPointBeforeClass);
@@ -226,6 +287,11 @@ private void extractExpressionFromCompField(
226287
exprResult.srcrefTreeIfMissing(record.classNode);
227288
}
228289

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+
229295
/** Rewrites and moves all instance fields */
230296
private void rewriteInstanceMembers(NodeTraversal t, ClassRecord record) {
231297
Deque<Node> instanceMembers = record.instanceMembers;
@@ -404,10 +470,6 @@ private Node findInitialInstanceInsertionPoint(Node ctorBlock) {
404470
*/
405471
private static final class ClassRecord {
406472

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;
411473
// During traversal, contains the current member being traversed. After traversal, always null
412474
@Nullable Node currentMember;
413475
boolean cannotConvert;
@@ -416,6 +478,8 @@ private static final class ClassRecord {
416478
final Deque<Node> instanceMembers = new ArrayDeque<>();
417479
// Static fields + static blocks
418480
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<>();
419483

420484
// Mapping from MEMBER_FIELD_DEF (& COMPUTED_FIELD_DEF) nodes to all name nodes in that RHS
421485
final SetMultimap<Node, Node> referencedNamesByMember =

test/com/google/javascript/jscomp/RewriteClassMembersTest.java

+30-34
Original file line numberDiff line numberDiff line change
@@ -134,40 +134,6 @@ public void testCannotConvertYet() {
134134
" }",
135135
"}"),
136136
CANNOT_CONVERT_YET); // `var` in static block
137-
138-
test(
139-
srcs(
140-
lines(
141-
"function foo(num) {}",
142-
"/** @unrestricted */",
143-
"class Baz {",
144-
" ['f' + foo(1)] = 5;",
145-
" static x = foo(6);",
146-
" ['m' + foo(2)]() {};",
147-
" static [foo(3)] = foo(7);",
148-
" [foo(4)] = 2;",
149-
" get [foo(5)]() {}",
150-
"}")),
151-
error(CANNOT_CONVERT_YET), // Comp field with side effect and computed function
152-
error(CANNOT_CONVERT_YET)); // Comp field with side effect and computed function
153-
/* TODO(user): Move computed fields with side effects into computed functions if
154-
present
155-
expected(
156-
lines(
157-
"function foo(num) {}",
158-
"var COMPFIELD$0;",
159-
"var COMPFIELD$1;",
160-
"var COMPFIELD$2;",
161-
"class Baz {",
162-
" constructor() {",
163-
" this[COMPFIELD$2] = 5;",
164-
" this[COMPFIELD$0] = 2;",
165-
" }",
166-
" [(COMPFIELD$2 = 'f' + foo(1), 'm' + foo(2))]() {}",
167-
" get [(COMPFIELD$1 = foo(3), COMPFIELD$0 = foo(4), foo(5))]() {}",
168-
"}",
169-
"Baz.x = foo(6);",
170-
"Baz[COMPFIELD$1] = foo(7);"))); */
171137
}
172138

173139
@Test
@@ -582,6 +548,36 @@ public void testSideEffectsInComputedField() {
582548
"COMPFIELD$0 = x;",
583549
"Foo.n = x = 5;",
584550
"Foo[COMPFIELD$0] = 'world';")));
551+
552+
computedFieldTest(
553+
srcs(
554+
lines(
555+
"function foo(num) {}",
556+
"/** @unrestricted */",
557+
"class Baz {",
558+
" ['f' + foo(1)];",
559+
" static x = foo(6);",
560+
" ['m' + foo(2)]() {};",
561+
" static [foo(3)] = foo(7);",
562+
" [foo(4)] = 2;",
563+
" get [foo(5)]() {}",
564+
"}")),
565+
expected(
566+
lines(
567+
"function foo(num) {}",
568+
"var COMPFIELD$0;",
569+
"var COMPFIELD$1;",
570+
"var COMPFIELD$2;",
571+
"class Baz {",
572+
" constructor() {",
573+
" this[COMPFIELD$0];",
574+
" this[COMPFIELD$1] = 2;",
575+
" }",
576+
" [(COMPFIELD$0 = 'f' + foo(1), 'm' + foo(2))]() {}",
577+
" get [(COMPFIELD$2 = foo(3), (COMPFIELD$1 = foo(4), foo(5)))]() {}",
578+
"}",
579+
"Baz.x = foo(6);",
580+
"Baz[COMPFIELD$2] = foo(7);")));
585581
}
586582

587583
@Test

0 commit comments

Comments
 (0)