Skip to content

Commit 5e53a60

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

File tree

2 files changed

+178
-19
lines changed

2 files changed

+178
-19
lines changed

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

Lines changed: 90 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
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;
3534
import java.util.LinkedHashMap;
3635
import java.util.LinkedHashSet;
3736
import java.util.Map;
@@ -154,36 +153,109 @@ private static void maybeAddConstJSDoc(Node srcDeclaration, Node srcName, Node d
154153
}
155154
}
156155

156+
/**
157+
* Given a declarationList of let/const declarations, convert all declared names to their own,
158+
* separate (normalized) var declarations.
159+
*
160+
* <p>"const i = 0, j = 0;" becomes "/\*\* @const *\/ var i = 0; /\** @const *\/ var j = 0;"
161+
*
162+
* <p>If the declarationList of let/const declarations is in a FOR intializer, moves those into
163+
* separate declarations outside the FOR loop (for maintaining normalization).
164+
*
165+
* <p>TODO: b/197349249 - We won't have any declaration lists here when this pass runs post
166+
* normalize.
167+
*/
157168
private void handleDeclarationList(Node declarationList, Node parent) {
158-
// Normalize: "const i = 0, j = 0;" becomes "/** @const */ var i = 0; /** @const */ var j = 0;"
159-
while (declarationList.hasMoreThanOneChild()) {
169+
if (declarationList.isVar() && declarationList.hasOneChild()) {
170+
// This declaration list is already handled and we can safely return. This happens because
171+
// this method is also called by {@code replaceDeclarationWithProperty} where it gets
172+
// repeatedly called for each name in the declarationList. After the first call
173+
// to this (for the first name), this declarationList would no longer be {@code Token.CONST}
174+
// (i.e. it would've become a separate var with an {@code /** const */} annotation).
175+
return;
176+
}
177+
if (parent.isVanillaFor()) {
178+
handleDeclarationListInForInitializer(declarationList, parent);
179+
return;
180+
}
181+
// convert all names to their own, separate (normalized) declarations
182+
while (declarationList.hasChildren()) {
160183
Node name = declarationList.getLastChild();
161184
Node newDeclaration = IR.var(name.detach()).srcref(declarationList);
185+
/*
186+
* This method gets called multiple times by {@code replaceDeclarationWithProperty}. In the
187+
* first call, it splits the declarationList into individual /\** @const *\/ var declarations.
188+
* i.e. `const a,b` --> `/\** @const *\/ var a; /\** @const *\/ var b;`
189+
*
190+
* <p>Then it gets invoked for each individual var declaration. once for each name in the
191+
* original declarationList). However, after the first call to this (for the first name), this
192+
* declarationList would no longer be {@code Token.CONST} (i.e. it would've become a separate
193+
* var with an {@code /*\* const *\/} annotation).
194+
*
195+
* <p>Previously, this method rewrote those individual var declarations again into new var
196+
* declarations. Without copying over the JSDoc, those newly created var declarations did not
197+
* contain `@const`, and produce this:
198+
*
199+
* <p>`/\** @const *\/ var a; /\** @const *\/ var b;` -->`var a; var b;`
200+
*
201+
* <p>Now, even though the "redundant rewriting" is handled by early-returning from this
202+
* method whenever this method gets called with a non-declaration var list, it's still
203+
* important that the individual var declarations created by the first rewriting contain the
204+
* annotations from original declaration list. Hence we propagate JSDoc from declarationList
205+
* into the individual var declarations.
206+
*/
207+
extractInlineJSDoc(declarationList, name, newDeclaration);
162208
maybeAddConstJSDoc(declarationList, name, newDeclaration);
163209
newDeclaration.insertAfter(declarationList);
164210
compiler.reportChangeToEnclosingScope(parent);
165211
}
166-
maybeAddConstJSDoc(declarationList, declarationList.getFirstChild(), declarationList);
167-
declarationList.setToken(Token.VAR);
212+
213+
// declarationList has no children left. Remove.
214+
declarationList.detach();
215+
compiler.reportChangeToEnclosingScope(parent);
216+
}
217+
218+
private void handleDeclarationListInForInitializer(Node declarationList, Node parent) {
219+
checkState(parent.isVanillaFor());
220+
// if the declarationList is in a FOR initializer, move it outside
221+
Node insertSpot = getInsertSpotBeforeLoop(parent);
222+
// convert all names to their own, separate (normalized) declarations
223+
while (declarationList.hasChildren()) {
224+
Node name = declarationList.getLastChild();
225+
Node newDeclaration = IR.var(name.detach()).srcref(declarationList);
226+
extractInlineJSDoc(declarationList, name, newDeclaration);
227+
maybeAddConstJSDoc(declarationList, name, newDeclaration);
228+
// generate normalized var initializer (i.e. outside FOR)
229+
newDeclaration.insertBefore(insertSpot);
230+
insertSpot = newDeclaration;
231+
compiler.reportChangeToEnclosingScope(parent);
232+
}
233+
// make FOR initializer empty
234+
Node empty = astFactory.createEmpty().srcref(declarationList);
235+
declarationList.replaceWith(empty);
236+
compiler.reportChangeToEnclosingScope(empty);
168237
}
169238

170239
private void addNodeBeforeLoop(Node newNode, Node loopNode) {
240+
Node insertSpot = getInsertSpotBeforeLoop(loopNode);
241+
newNode.insertBefore(insertSpot);
242+
compiler.reportChangeToEnclosingScope(newNode);
243+
}
244+
245+
private Node getInsertSpotBeforeLoop(Node loopNode) {
171246
Node insertSpot = loopNode;
172247
while (insertSpot.getParent().isLabel()) {
173248
insertSpot = insertSpot.getParent();
174249
}
175-
newNode.insertBefore(insertSpot);
176-
compiler.reportChangeToEnclosingScope(newNode);
250+
return insertSpot;
177251
}
178252

179253
private void rewriteDeclsToVars() {
180254
if (!letConsts.isEmpty()) {
181255
for (Node n : letConsts) {
182-
if (n.isConst()) {
183-
handleDeclarationList(n, n.getParent());
184-
}
185-
n.setToken(Token.VAR);
186-
compiler.reportChangeToEnclosingScope(n);
256+
// for both lets and consts we want to split the declaration lists when converting them to
257+
// vars (to maintain normalization)
258+
handleDeclarationList(n, n.getParent());
187259
}
188260
}
189261
}
@@ -416,11 +488,14 @@ private void replaceDeclarationWithProperty(
416488
LoopObject loopObject, String newPropertyName, Node reference) {
417489
Node declaration = reference.getParent();
418490
Node grandParent = declaration.getParent();
419-
// If the declaration contains multiple declared variables, split it apart.
420-
handleDeclarationList(declaration, grandParent);
421-
// Record that the let / const declaration statement has been turned into one or more
491+
// Record that the let / const declaration statement will get turned into one or more
422492
// var statements by handleDeclarationList(), so we won't try to change it again later.
423493
letConsts.remove(declaration);
494+
// If the declaration contains multiple declared variables, split it apart.
495+
// NOTE: This call could be made for each declarationList once, rather than each name in that
496+
// list
497+
handleDeclarationList(declaration, grandParent);
498+
424499
// The variable we're working with may have been moved to a new var statement.
425500
declaration = reference.getParent();
426501
if (reference.hasChildren()) {

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

Lines changed: 88 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -299,8 +299,9 @@ public void testLetShadowingTranspileOnly() {
299299
@Test
300300
public void testLetShadowingWithMultivariateDeclaration() {
301301
Sources srcs = srcs("var x, y; for (let x, y;;) {}");
302+
// normalized var decls outside the for loop
302303
Expected expected =
303-
expected("var x, y; for (var x$jscomp$1 = void 0, y$jscomp$1 = void 0;;) {}");
304+
expected("var x, y; var x$jscomp$1 = void 0; var y$jscomp$1 = void 0; for (;;) {}");
304305
test(srcs, expected);
305306
}
306307

@@ -392,6 +393,88 @@ public void testRenameConflict() {
392393
test(srcs, expected);
393394
}
394395

396+
@Test
397+
public void testForLetInitializer_getsNormalized() {
398+
Sources srcs =
399+
srcs(
400+
lines(
401+
"function f() {",
402+
" return function foo() {",
403+
" for (let i = 0; i < 10; i++) {",
404+
" }",
405+
" };",
406+
"}"));
407+
408+
Expected expected =
409+
expected(
410+
lines(
411+
"function f() {",
412+
" return function foo() {",
413+
" var i = 0;", // normalized
414+
" for (; i < 10; i++) {",
415+
" }",
416+
" };",
417+
"}"));
418+
419+
test(srcs, expected);
420+
}
421+
422+
@Test
423+
public void testForLetInitializer_declarationList_getsNormalized() {
424+
Sources srcs =
425+
srcs(
426+
lines(
427+
"function f() {",
428+
" return function foo() {",
429+
" for (let i = 0, y = 0; i < 10; i++) {",
430+
" }",
431+
" };",
432+
"}"));
433+
434+
Expected expected =
435+
expected(
436+
lines(
437+
"function f() {",
438+
" return function foo() {",
439+
" var i = 0; var y=0;", // normalized
440+
" for (; i < 10; i++) {",
441+
" }",
442+
" };",
443+
"}"));
444+
445+
test(srcs, expected);
446+
}
447+
448+
@Test
449+
public void testsForVarInitializer_staysNormalized() {
450+
Sources srcs =
451+
srcs(
452+
lines(
453+
"function f() {",
454+
" return function foo() {",
455+
" var i = 0;",
456+
" for ( ;i < 10; i++) {", // originally normalized
457+
" }",
458+
" };",
459+
"}"));
460+
461+
testSame(srcs);
462+
}
463+
464+
@Test
465+
public void testsForVarInitializer_unchanged() {
466+
Sources srcs =
467+
srcs(
468+
lines(
469+
"function f() {",
470+
" return function foo() {",
471+
" for (var i = 0; i < 10; i++) {", // originally unnormalized
472+
" }",
473+
" };",
474+
"}"));
475+
testSame(srcs);
476+
}
477+
395478
@Test
396479
public void testForLoop() {
397480
Sources srcs =
@@ -412,7 +495,8 @@ public void testForLoop() {
412495
"function use(x) {}",
413496
"function f() {",
414497
" /** @const */ var y = 0;",
415-
" for (var x$jscomp$1 = 0; x$jscomp$1 < 10; x$jscomp$1++) {",
498+
" var x$jscomp$1 = 0;",
499+
" for (; x$jscomp$1 < 10; x$jscomp$1++) {",
416500
" /** @const */ var y$jscomp$1 = x$jscomp$1 * 2;",
417501
" /** @const */ var z = y$jscomp$1;",
418502
" }",
@@ -445,11 +529,11 @@ public void testForLoop() {
445529
loopClosureTest(srcs, expected);
446530

447531
srcs = srcs("for (let i = 0;;) { let i; }");
448-
expected = expected("for (var i = 0;;) { var i$jscomp$1 = void 0; }");
532+
expected = expected("var i = 0; for (;;) { var i$jscomp$1 = void 0; }");
449533
loopClosureTest(srcs, expected);
450534

451535
srcs = srcs("for (let i = 0;;) {} let i;");
452-
expected = expected("for (var i$jscomp$1 = 0;;) {} var i;");
536+
expected = expected("var i$jscomp$1 = 0; for (;;) {} var i;");
453537
loopClosureTest(srcs, expected);
454538

455539
test(

0 commit comments

Comments
 (0)