Skip to content

Commit 633a626

Browse files
Closure Teamcopybara-github
authored andcommitted
Hoist function assignment to the top of the inner-most *block* scope when rewriting a function declaration as a function expression for eager browser JS compilation parenthesization.
PiperOrigin-RevId: 499470585
1 parent eaf5ec7 commit 633a626

File tree

5 files changed

+248
-61
lines changed

5 files changed

+248
-61
lines changed

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1600,7 +1600,7 @@ private static boolean isFirstChild(Node n) {
16001600

16011601
private void addArrowFunction(Node n, Node first, Node last, Context context) {
16021602
checkState(first.getString().isEmpty(), first);
1603-
boolean funcNeedsParens = arrowFunctionNeedsParens(n);
1603+
boolean funcNeedsParens = arrowFunctionNeedsParens(n) || n.getMarkForParenthesize();
16041604
if (funcNeedsParens) {
16051605
add("(");
16061606
}
@@ -1630,8 +1630,7 @@ private void addArrowFunction(Node n, Node first, Node last, Context context) {
16301630
}
16311631

16321632
private void addFunction(Node n, Node first, Node last, Context context) {
1633-
boolean funcNeedsParens =
1634-
(context == Context.START_OF_EXPR || n.getBooleanProp(Node.MARK_FOR_PARENTHESIZE));
1633+
boolean funcNeedsParens = (context == Context.START_OF_EXPR || n.getMarkForParenthesize());
16351634
if (funcNeedsParens) {
16361635
add("(");
16371636
}

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

Lines changed: 150 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,22 @@
1616

1717
package com.google.javascript.jscomp;
1818

19+
import static com.google.common.base.Preconditions.checkState;
20+
21+
import com.google.common.collect.ArrayListMultimap;
22+
import com.google.common.collect.ListMultimap;
1923
import com.google.javascript.jscomp.diagnostic.LogFile;
2024
import com.google.javascript.rhino.IR;
2125
import com.google.javascript.rhino.Node;
26+
import java.util.ArrayDeque;
27+
import java.util.ArrayList;
28+
import java.util.Collections;
29+
import java.util.Deque;
2230
import java.util.HashMap;
31+
import java.util.List;
2332
import java.util.Map;
2433
import java.util.Set;
34+
import org.jspecify.nullness.Nullable;
2535

2636
/**
2737
* Marks all functions of specified chunks for eager parsing by adding the node property
@@ -38,12 +48,14 @@
3848
* for eager compile for each specified chunk.
3949
*/
4050
public final class ParenthesizeFunctionsInChunks implements CompilerPass {
41-
final AbstractCompiler compiler;
42-
final Set<String> parenthesizeFunctionsInChunks;
43-
44-
// Map for recording diagnostic information about what was marked for eager compilation.
45-
final Map<String, Long> chunkToEagerCompileFnCount = new HashMap<>();
51+
private final AbstractCompiler compiler;
52+
private final Set<String> parenthesizeFunctionsInChunks;
4653

54+
/**
55+
* @param compiler An abstract compiler.
56+
* @param parenthesizeFunctionsInChunks The set of chunk names in which to parenthesize top level
57+
* functions.
58+
*/
4759
public ParenthesizeFunctionsInChunks(
4860
AbstractCompiler compiler, Set<String> parenthesizeFunctionsInChunks) {
4961
this.compiler = compiler;
@@ -52,52 +64,155 @@ public ParenthesizeFunctionsInChunks(
5264

5365
@Override
5466
public void process(Node externs, Node root) {
55-
NodeTraversal.traverse(compiler, root, new Traversal());
56-
for (String key : chunkToEagerCompileFnCount.keySet()) {
57-
try (LogFile log = compiler.createOrReopenLog(getClass(), "eager_compile_chunks.log")) {
58-
log.log("%s: %d fn's marked for eager compile", key, chunkToEagerCompileFnCount.get(key));
67+
Traversal traversal = new Traversal(parenthesizeFunctionsInChunks);
68+
NodeTraversal.traverse(compiler, root, traversal);
69+
70+
Map<String, Long> chunkToEagerCompileFnCounts = traversal.getChunkToEagerCompileFnCounts();
71+
72+
try (LogFile log = compiler.createOrReopenLog(getClass(), "eager_compile_chunks.log")) {
73+
for (Map.Entry<String, Long> entry : chunkToEagerCompileFnCounts.entrySet()) {
74+
log.log("%s: %d fn's marked for eager compile", entry.getKey(), entry.getValue());
5975
}
6076
}
6177
}
6278

63-
private class Traversal implements NodeTraversal.Callback {
79+
private static class Traversal implements NodeTraversal.Callback {
80+
private final Set<String> parenthesizeFunctionsInChunks;
81+
82+
// The stack of nested block scopes for the node we're currently visiting.
83+
private final Deque<Node> nestedBlockScopes = new ArrayDeque<>();
84+
85+
// A multimap relating a scope node to any children nodes which should be hosted into its scope.
86+
private final ListMultimap<Node, Node> hoistNodesToScope = ArrayListMultimap.create();
87+
88+
// Map for recording diagnostic information about what was marked for eager compilation.
89+
private final Map<String, Long> chunkToEagerCompileFnCounts = new HashMap<>();
90+
91+
public Traversal(Set<String> parenthesizeFunctionsInChunks) {
92+
this.parenthesizeFunctionsInChunks = parenthesizeFunctionsInChunks;
93+
}
94+
95+
@Override
96+
public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) {
97+
if (!shouldParenthesizeTree(t, n)) {
98+
return false;
99+
}
100+
101+
if (parent != null && parent.isFunction()) {
102+
return false; // Don't visit the contents of any functions.
103+
}
104+
105+
if (NodeUtil.isStatementBlock(n)) {
106+
nestedBlockScopes.push(n); // Enter block scope.
107+
}
108+
109+
return true;
110+
}
111+
112+
@Override
113+
public void visit(NodeTraversal t, Node n, Node parent) {
114+
if (isInnerMostBlockScope(n)) {
115+
hoistChildrenToTopOfScope(n);
116+
nestedBlockScopes.pop(); // Exit block scope.
117+
} else if (NodeUtil.isFunctionExpression(n)) {
118+
n.setMarkForParenthesize(true);
119+
incrementEagerlyCompiledFunctionCount(t);
120+
} else if (NodeUtil.isFunctionDeclaration(n)) {
121+
n.setMarkForParenthesize(true);
122+
addChildForHoistToScope(functionDeclarationToFunctionExpression(t, n));
123+
incrementEagerlyCompiledFunctionCount(t);
124+
} else {
125+
// Ex: Method function definitions.
126+
// Do nothing.
127+
}
128+
}
129+
130+
public Map<String, Long> getChunkToEagerCompileFnCounts() {
131+
checkState(
132+
nestedBlockScopes.isEmpty(), "Expected empty scope stack. Got: %s", nestedBlockScopes);
133+
checkState(
134+
hoistNodesToScope.isEmpty(), "Expected empty hoist map. Got: %s", hoistNodesToScope);
135+
return chunkToEagerCompileFnCounts;
136+
}
137+
138+
/**
139+
* Whether we should parenthesize functions in the given tree of the traversal. If there is no
140+
* chunk, then we are compiling a single-chunk output, so we will just parenthesize all
141+
* top-level functions.
142+
*/
143+
private boolean shouldParenthesizeTree(NodeTraversal t, Node n) {
144+
if (!n.isScript()) {
145+
return true;
146+
}
64147

65-
Traversal() {}
148+
String chunkName = getChunkName(t);
149+
return chunkName == null || parenthesizeFunctionsInChunks.contains(chunkName);
150+
}
151+
152+
private void incrementEagerlyCompiledFunctionCount(NodeTraversal t) {
153+
chunkToEagerCompileFnCounts.merge(getChunkName(t), 1L, (oldValue, value) -> oldValue + 1);
154+
}
66155

67-
private void rewriteAsFunctionExpression(Node n) {
156+
/** Gets the chunk name of the current traveral or null if it doesn't belong to a chunk. */
157+
private @Nullable String getChunkName(NodeTraversal t) {
158+
JSChunk chunk = t.getChunk();
159+
return (chunk != null ? chunk.getName() : null);
160+
}
161+
162+
/** Whether this node is the inner-most block scope. */
163+
private boolean isInnerMostBlockScope(Node n) {
164+
return !nestedBlockScopes.isEmpty() && nestedBlockScopes.peek() == n;
165+
}
166+
167+
/**
168+
* Converts the given function declaration into a function expression suitable for wrapping in
169+
* parenthesis. The function expression is assigned to a `var` declaration so that it is
170+
* semantically hoisted to the inner-most function scope like with function declarations.
171+
*/
172+
private Node functionDeclarationToFunctionExpression(NodeTraversal t, Node n) {
173+
AbstractCompiler compiler = t.getCompiler();
68174
Node nameNode = n.getFirstChild();
69175
Node name = IR.name(nameNode.getString()).srcref(nameNode);
70176
Node var = IR.var(name).srcref(n);
71-
// Add the VAR, remove the FUNCTION
72-
n.replaceWith(var);
73-
// readd the function, but remove the function name, to guard against
177+
// read the function, but remove the function name, to guard against
74178
// functions that re-assign to themselves, which will end up causing a
75179
// recursive loop.
76-
n.getFirstChild().setString("");
180+
nameNode.setString("");
181+
compiler.reportChangeToEnclosingScope(n.getLastChild());
182+
// Add the VAR, remove the FUNCTION
183+
n.replaceWith(var);
184+
compiler.reportChangeToEnclosingScope(var);
77185
name.addChildToFront(n);
186+
return var;
78187
}
79188

80-
@Override
81-
public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) {
82-
if (n.isScript() || n.isRoot() || n.isModuleBody()) {
83-
return true;
84-
}
85-
if (n.isFunction()) {
86-
String chunkName = t.getChunk().getName();
87-
chunkToEagerCompileFnCount.putIfAbsent(chunkName, 0L);
88-
if (parenthesizeFunctionsInChunks.contains(chunkName)) {
89-
n.putBooleanProp(Node.MARK_FOR_PARENTHESIZE, true);
90-
if (!NodeUtil.isFunctionExpression(n)) {
91-
rewriteAsFunctionExpression(n);
92-
}
93-
chunkToEagerCompileFnCount.put(chunkName, chunkToEagerCompileFnCount.get(chunkName) + 1);
94-
}
95-
return false;
189+
/**
190+
* Marks a node to be moved to the top of its parent *block* scope. This is useful for hoisting
191+
* function expression assignments to emulate how function declarations are assigned.
192+
*
193+
* <pre>
194+
* Function declarations have the following semantics:
195+
* 1. Hoists the function variable declaration to the top of the inner-most *function* scope.
196+
* 2. Hoists the function assignment to the top of the inner-most *block* scope.
197+
* </pre>
198+
*/
199+
private void addChildForHoistToScope(Node node) {
200+
if (nestedBlockScopes.isEmpty()) { // This node is already at the root scope.
201+
return;
96202
}
97-
return true;
203+
204+
Node innerMostBlockScope = nestedBlockScopes.peek();
205+
hoistNodesToScope.put(innerMostBlockScope, node.detach());
98206
}
99207

100-
@Override
101-
public void visit(NodeTraversal t, Node n, Node parent) {}
208+
/** Hoists any marked nodes to the beginning of this scope. */
209+
private void hoistChildrenToTopOfScope(Node scope) {
210+
List<Node> nodes = new ArrayList<>(hoistNodesToScope.removeAll(scope));
211+
Collections.reverse(nodes); // Maintain node order when pushing as first sibling.
212+
213+
for (Node node : nodes) {
214+
scope.addChildToFront(node);
215+
}
216+
}
102217
}
103218
}

src/com/google/javascript/rhino/Node.java

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -253,17 +253,28 @@ public final Node setTrailingNonJSDocComment(NonJSDocComment comment) {
253253
return this;
254254
}
255255

256-
/** Sets whether this node is inside parentheses. */
256+
/** Sets whether this node was inside original source-level parentheses. */
257257
public final void setIsParenthesized(boolean b) {
258258
checkState(IR.mayBeExpression(this));
259259
putBooleanProp(Prop.IS_PARENTHESIZED, b);
260260
}
261261

262-
/** Check whether node was inside parentheses. */
262+
/** Check whether node was inside original source-level parentheses. */
263263
public final boolean getIsParenthesized() {
264264
return getBooleanProp(Prop.IS_PARENTHESIZED);
265265
}
266266

267+
/** Sets whether this node is should be parenthesized in output. */
268+
public final void setMarkForParenthesize(boolean value) {
269+
checkState(IR.mayBeExpression(this));
270+
putBooleanProp(Prop.MARK_FOR_PARENTHESIZE, value);
271+
}
272+
273+
/** Check whether node should be parenthesized in output. */
274+
public final boolean getMarkForParenthesize() {
275+
return getBooleanProp(Prop.MARK_FOR_PARENTHESIZE);
276+
}
277+
267278
// TODO(sdh): Get rid of these by using accessor methods instead.
268279
// These export instances of a private type, which is awkward but a side effect is that it
269280
// prevents anyone from introducing problemmatic uses of the general-purpose accessors.
@@ -297,7 +308,6 @@ public final boolean getIsParenthesized() {
297308
public static final Prop MODULE_EXPORT = Prop.MODULE_EXPORT;
298309
public static final Prop IS_SHORTHAND_PROPERTY = Prop.IS_SHORTHAND_PROPERTY;
299310
public static final Prop ES6_MODULE = Prop.ES6_MODULE;
300-
public static final Prop MARK_FOR_PARENTHESIZE = Prop.MARK_FOR_PARENTHESIZE;
301311

302312
private static final class NumberNode extends Node {
303313

@@ -1002,6 +1012,7 @@ public void validateProperties(Consumer<String> violationMessageConsumer) {
10021012
// this method was created to cover the checks previously done there.
10031013
switch (prop) {
10041014
case IS_PARENTHESIZED:
1015+
case MARK_FOR_PARENTHESIZE:
10051016
if (!IR.mayBeExpression(this)) {
10061017
violationMessageConsumer.accept("non-expression is parenthesized");
10071018
}

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

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ public void setUp() throws Exception {
9393
}
9494

9595
@Test
96-
public void testParenthesizedProperty() {
96+
public void testIsParenthesizedProperty() {
9797
// Since we're building the AST by hand, there won't be any types on it.
9898
typeInfoValidationMode = TypeInfoValidation.NONE;
9999

@@ -112,6 +112,25 @@ public void testParenthesizedProperty() {
112112
expectInvalid(objNode, Check.EXPRESSION, "non-expression is parenthesized");
113113
}
114114

115+
@Test
116+
public void testMarkForParenthesizeProperty() {
117+
// Since we're building the AST by hand, there won't be any types on it.
118+
typeInfoValidationMode = TypeInfoValidation.NONE;
119+
120+
Node n = IR.string("a");
121+
n.setMarkForParenthesize(true);
122+
setTestSourceLocationForTree(n);
123+
expectValid(n, Check.EXPRESSION);
124+
125+
n.setToken(Token.STRING_KEY); // A string key cannot be parenthesized
126+
// We have to put the STRING_KEY into an object and give it a child, so we have an
127+
// expression to validate that is valid other than the bad parenthesized property.
128+
Node objNode = IR.objectlit(n);
129+
n.addChildToFront(IR.number(0));
130+
objNode.srcrefTree(n);
131+
expectInvalid(objNode, Check.EXPRESSION, "non-expression is parenthesized");
132+
}
133+
115134
@Test
116135
public void testValidGetPropAndGetElem() {
117136
valid(

0 commit comments

Comments
 (0)