Skip to content

Commit 501ae96

Browse files
rishipalcopybara-github
authored andcommitted
Fix ES6RewriteBlockScopedDeclaration to generate normalized FOR_IN loop initializer
The pass currently generates var in the initializer part of FOR_IN loops. This makes the AST unnormalized. Fixing this to preserve normalization brings us closer to moving the pass post normalize. PiperOrigin-RevId: 568357040
1 parent 614dac2 commit 501ae96

File tree

3 files changed

+160
-16
lines changed

3 files changed

+160
-16
lines changed

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

Lines changed: 107 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import com.google.javascript.rhino.IR;
3232
import com.google.javascript.rhino.JSDocInfo;
3333
import com.google.javascript.rhino.Node;
34+
import com.google.javascript.rhino.Token;
3435
import java.util.LinkedHashMap;
3536
import java.util.LinkedHashSet;
3637
import java.util.Map;
@@ -174,10 +175,42 @@ private void handleDeclarationList(Node declarationList, Node parent) {
174175
// (i.e. it would've become a separate var with an {@code /** const */} annotation).
175176
return;
176177
}
178+
177179
if (parent.isVanillaFor()) {
178-
handleDeclarationListInForInitializer(declarationList, parent);
180+
/*
181+
* This is needed to handle the case where we get:
182+
*
183+
* <ol>
184+
* <li>`for (let x = ...)` or
185+
* <li>`for (const x = ...`. <
186+
* </ol>
187+
*
188+
* <p>Normalize only moves "var" outside the for initializer, it allows let/const within the
189+
* initializer. So both #1 and #2 can exist regardless if this pass runs before and after
190+
* normalize.
191+
*/
192+
handleDeclarationListInVanillaForInitializer(declarationList, parent);
193+
return;
194+
}
195+
196+
if (parent.isForIn()) {
197+
/*
198+
* This is needed to handle the case where we get:
199+
*
200+
* <ol>
201+
* <li>`for (let x in ...)` or
202+
* <li>`for (const x in ...`. <
203+
* </ol>
204+
*
205+
*
206+
* <p>Normalize only moves "var" outside the for initializer, it allows let/const within the
207+
* initializer. So both #1 and #2 can exist regardless if this pass runs before and after
208+
* normalize.
209+
*/
210+
handleDeclarationListInForInInitializer(declarationList, parent);
179211
return;
180212
}
213+
181214
// convert all names to their own, separate (normalized) declarations
182215
while (declarationList.hasChildren()) {
183216
Node name = declarationList.getLastChild();
@@ -215,7 +248,74 @@ private void handleDeclarationList(Node declarationList, Node parent) {
215248
compiler.reportChangeToEnclosingScope(parent);
216249
}
217250

218-
private void handleDeclarationListInForInitializer(Node declarationList, Node parent) {
251+
/**
252+
* TODO: b/197349249 - We won't have any declaration lists in the FOR_IN initializer here when
253+
* this pass runs post normalize. After that, all lets/consts can be directly converted to vars.
254+
*/
255+
private void handleDeclarationListInForInInitializer(Node declarationList, Node parent) {
256+
checkState(parent.isForIn());
257+
Node first = declarationList;
258+
Node lhs = first.getFirstChild();
259+
Node loopNode = parent;
260+
if (lhs.isDestructuringLhs()) {
261+
// This pass relies on destructuring syntax being already removed. Hence we must not enter
262+
// this case.
263+
// TODO: b/279640656 Enable this code path once this pass runs unconditionally in stage3.
264+
checkState(
265+
false, "Destructuring syntax is unsupported in ES6RewriteBlockScopedDeclarations pass");
266+
// Transform:
267+
// `for (let [a, b = 3] in c) {}` or `for (const [a, b = 3] in c) {}`
268+
// to:
269+
// <pre>
270+
// `var a; var b; for ([a, b = 3] in c) {}`
271+
// </pre>
272+
// respectively
273+
NodeUtil.visitLhsNodesInNode(
274+
lhs,
275+
(name) -> {
276+
// Add a declaration outside the for loop for the given name.
277+
checkState(
278+
name.isName(),
279+
"lhs in destructuring declaration should be a simple name. (%s)",
280+
name);
281+
Node newName = IR.name(name.getString()).srcref(name);
282+
Node newVar = IR.var(newName).srcref(name);
283+
extractInlineJSDoc(declarationList, name, newVar);
284+
// if the initializer name was a const, the newName must no longer be a const.
285+
addNodeBeforeLoop(newVar, loopNode);
286+
});
287+
288+
// Transform `for (var [a, b]... )` to `for ([a, b]...`
289+
Node destructuringPattern = lhs.removeFirstChild();
290+
first.replaceWith(destructuringPattern);
291+
} else {
292+
// Transform:
293+
// for (let a in b) {}
294+
// to:
295+
// var a; for (a in b) {};
296+
// and:
297+
// for (const a in b) {}
298+
// to:
299+
// var a; for (a in b) {};
300+
Node newStatement = first.cloneTree();
301+
Node name = newStatement.getFirstChild().cloneNode();
302+
// cloning also copies over any properties
303+
if (name.getBooleanProp(Node.IS_CONSTANT_NAME)) {
304+
// if the initializer name was a const, it must no longer be a const var.
305+
name.putProp(Node.IS_CONSTANT_NAME, false);
306+
}
307+
newStatement.setToken(Token.VAR);
308+
extractInlineJSDoc(first, first.getFirstChild(), newStatement);
309+
first.replaceWith(name);
310+
addNodeBeforeLoop(newStatement, loopNode);
311+
}
312+
}
313+
314+
/**
315+
* TODO: b/197349249 - We won't have any declaration lists in the FOR initializer here when this
316+
* pass runs post normalize. After that, all lets/consts can be directly converted to vars.
317+
*/
318+
private void handleDeclarationListInVanillaForInitializer(Node declarationList, Node parent) {
219319
checkState(parent.isVanillaFor());
220320
// if the declarationList is in a FOR initializer, move it outside
221321
Node insertSpot = getInsertSpotBeforeLoop(parent);
@@ -224,13 +324,16 @@ private void handleDeclarationListInForInitializer(Node declarationList, Node pa
224324
Node name = declarationList.getLastChild();
225325
Node newDeclaration = IR.var(name.detach()).srcref(declarationList);
226326
extractInlineJSDoc(declarationList, name, newDeclaration);
227-
maybeAddConstJSDoc(declarationList, name, newDeclaration);
327+
if (name.getBooleanProp(Node.IS_CONSTANT_NAME)) {
328+
// if the initializer name was a const, it must no longer be a const var after rewriting.
329+
name.putProp(Node.IS_CONSTANT_NAME, false);
330+
}
228331
// generate normalized var initializer (i.e. outside FOR)
229332
newDeclaration.insertBefore(insertSpot);
230333
insertSpot = newDeclaration;
231334
compiler.reportChangeToEnclosingScope(parent);
232335
}
233-
// make FOR initializer empty
336+
// make FOR initializer empty `for (; cond; incr)`
234337
Node empty = astFactory.createEmpty().srcref(declarationList);
235338
declarationList.replaceWith(empty);
236339
compiler.reportChangeToEnclosingScope(empty);

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -552,11 +552,10 @@ private void extractForInitializer(Node n, @Nullable Node before, @Nullable Node
552552
first.replaceWith(destructuringPattern);
553553
} else {
554554
// Transform:
555-
// for (var a = 1 in b) {}
555+
// for (var a in b) {}
556556
// to:
557-
// var a = 1; for (a in b) {};
557+
// var a; for (a in b) {};
558558
Node newStatement = first;
559-
// Clone just the node, to remove any initialization.
560559
Node name = newStatement.getFirstChild().cloneNode();
561560
first.replaceWith(name);
562561
newStatement.insertBefore(insertBefore);

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

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -394,7 +394,7 @@ public void testRenameConflict() {
394394
}
395395

396396
@Test
397-
public void testForLetInitializer_getsNormalized() {
397+
public void testVanillaFor_letInitializer_getsNormalized() {
398398
Sources srcs =
399399
srcs(
400400
lines(
@@ -419,6 +419,34 @@ public void testForLetInitializer_getsNormalized() {
419419
test(srcs, expected);
420420
}
421421

422+
@Test
423+
public void testVanillaFor_constInitializer_getsNormalized() {
424+
Sources srcs =
425+
srcs(
426+
lines(
427+
"function f() {",
428+
" return function foo() {",
429+
// const initializer (allowed syntactically) can not be updated in the incr.
430+
" for (const i = 0; i < 10; /* no incr */) {",
431+
" }",
432+
" };",
433+
"}"));
434+
435+
Expected expected =
436+
expected(
437+
lines(
438+
"function f() {",
439+
" return function foo() {",
440+
" var i = 0;", // normalized (outside the for) and no @const annotation after
441+
// rewriting
442+
" for (; i < 10; /* no incr */) {",
443+
" }",
444+
" };",
445+
"}"));
446+
447+
test(srcs, expected);
448+
}
449+
422450
@Test
423451
public void testForLetInitializer_declarationList_getsNormalized() {
424452
Sources srcs =
@@ -518,7 +546,8 @@ public void testForLoop() {
518546
expected =
519547
expected(
520548
lines(
521-
"for (var i in [0, 1]) {",
549+
"var i;",
550+
"for (i in [0, 1]) {",
522551
" var f = function() {",
523552
" var i$jscomp$1 = 0;",
524553
" if (true) {",
@@ -538,6 +567,10 @@ public void testForLoop() {
538567

539568
test(
540569
lines(
570+
// TODO: b/197349249 This unnormalized code would not reach this pass once this
571+
// pass runs post normalize. The test must then be updated to the new "normalized"
572+
// code form:
573+
// var x; for (x in y) { ...}
541574
"for (var x in y) {", //
542575
" /** @type {number} */",
543576
" let i;",
@@ -561,7 +594,8 @@ public void testForLoop() {
561594
expected =
562595
expected(
563596
lines(
564-
"for (/** @const */ var i in [0, 1]) {",
597+
"var i;", //
598+
"for (i in [0, 1]) {",
565599
" var f = function() {",
566600
" var i$jscomp$1 = 0;",
567601
" if (true) {",
@@ -982,7 +1016,8 @@ public void testLoopClosureWithContinue() {
9821016
"/** @const */ var obj = {a: 1, b: 2, c: 3, skipMe: 4};",
9831017
"/** @const */ var arr = [];",
9841018
"var LOOP$0 = {};",
985-
"for (/** @const */ var p in obj) {",
1019+
"var p",
1020+
"for (p in obj) {",
9861021
" LOOP$0 = {",
9871022
" p: LOOP$0.p",
9881023
" };",
@@ -1577,7 +1612,8 @@ public void testForIn() {
15771612
lines(
15781613
"/** @const */ var arr = [];",
15791614
"var LOOP$0 = {};",
1580-
"for (var i in [0, 1]) {",
1615+
"var i;",
1616+
"for (i in [0, 1]) {",
15811617
" LOOP$0 = {",
15821618
" i: LOOP$0.i",
15831619
" };",
@@ -1594,6 +1630,10 @@ public void testForIn() {
15941630
"for (;;) {",
15951631
" let a = getArray();",
15961632
" f = function() {",
1633+
// TODO: b/197349249 This unnormalized code would not reach this pass once this
1634+
// pass runs post normalize. The test must then be updated to the new "normalized"
1635+
// code form:
1636+
// var x; for (x in use(a)) { ...}
15971637
" for (var x in use(a)) {",
15981638
" f(a);",
15991639
" a.push(x);",
@@ -1656,7 +1696,8 @@ public void testDoWhileForInCapturedLet() {
16561696
" 5: 5",
16571697
" };",
16581698
" var LOOP$0 = {};",
1659-
" for (var i in obj) {",
1699+
" var i;",
1700+
" for (i in obj) {",
16601701
" LOOP$0 = {",
16611702
" i: LOOP$0.i",
16621703
" };",
@@ -2102,7 +2143,8 @@ public void testUniqueIdAcrossMultipleFiles() {
21022143
Expected expected =
21032144
expected(
21042145
lines(
2105-
"for (/** @const */ var i in [0, 1]) {",
2146+
"var i;",
2147+
"for (i in [0, 1]) {",
21062148
" var f = function() {",
21072149
" var i$jscomp$1 = 0;",
21082150
" if (true) {",
@@ -2111,8 +2153,8 @@ public void testUniqueIdAcrossMultipleFiles() {
21112153
" }",
21122154
"}"),
21132155
lines(
2114-
"for (/** @const */",
2115-
"var b in[0, 1]) {",
2156+
"var b",
2157+
"for (b in[0, 1]) {",
21162158
" var f$jscomp$1 = function() {",
21172159
" var b$jscomp$1 = 0;",
21182160
" if (true) {",

0 commit comments

Comments
 (0)