Skip to content

Commit 664e1b0

Browse files
brad4dcopybara-github
authored andcommitted
Add RewriteOptionalChainingOperator transpilation pass
This pass isn't yet used. Once the checks passes have been updated to support optional chaining, a change will be submitted to enable execution of this pass. PiperOrigin-RevId: 321824062
1 parent 8b32043 commit 664e1b0

File tree

6 files changed

+736
-1
lines changed

6 files changed

+736
-1
lines changed

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -774,6 +774,11 @@ Node createAssign(Node lhs, Node rhs) {
774774
return result;
775775
}
776776

777+
/** Creates an assignment expression `lhs = rhs` */
778+
Node createAssign(String lhsName, Node rhs) {
779+
return createAssign(createName(lhsName, rhs.getJSType()), rhs);
780+
}
781+
777782
/**
778783
* Creates an object-literal with zero or more elements, `{}`.
779784
*
@@ -886,6 +891,14 @@ Node createSheq(Node expr1, Node expr2) {
886891
return result;
887892
}
888893

894+
Node createEq(Node expr1, Node expr2) {
895+
Node result = IR.eq(expr1, expr2);
896+
if (isAddingTypes()) {
897+
result.setJSType(getNativeType(JSTypeNative.BOOLEAN_TYPE));
898+
}
899+
return result;
900+
}
901+
889902
Node createNe(Node expr1, Node expr2) {
890903
Node result = IR.ne(expr1, expr2);
891904
if (isAddingTypes()) {
Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
/*
2+
* Copyright 2020 The Closure Compiler Authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.google.javascript.jscomp;
17+
18+
import static com.google.common.base.Preconditions.checkArgument;
19+
import static com.google.common.base.Preconditions.checkNotNull;
20+
import static com.google.common.base.Preconditions.checkState;
21+
22+
import com.google.javascript.jscomp.parsing.parser.FeatureSet.Feature;
23+
import com.google.javascript.rhino.IR;
24+
import com.google.javascript.rhino.Node;
25+
import com.google.javascript.rhino.Token;
26+
import java.util.ArrayDeque;
27+
28+
/**
29+
* Rewrites a single optional chain as one or more nested hook expressions.
30+
*
31+
* <p>Optional chains contained in OPTCHAIN_GETELEM indices or OPTCHAIN_CALL arguments are not
32+
* rewritten.
33+
*
34+
* <p>Example:
35+
*
36+
* <pre><code>
37+
* a?.b[obj?.index]?.c?.(obj?.arg)
38+
* // becomes
39+
* (tmp0 = a) == null
40+
* ? void 0
41+
* : (tmp1 = tmp0.b[obj?.index]) == null
42+
* ? void 0
43+
* : (tmp2 = tmp1.c) == null
44+
* ? void 0
45+
* : tmp2.call(tmp1, obj?.arg);
46+
* </code></pre>
47+
*
48+
* <p>The unit tests for this class are in RewriteOptionalChainingOperatorTest, because it's most
49+
* convenient to test this class as part of the transpilation pass that uses it.
50+
*/
51+
class OptionalChainRewriter {
52+
final AbstractCompiler compiler;
53+
final AstFactory astFactory;
54+
final TmpVarNameCreator tmpVarNameCreator;
55+
final Node chainParent;
56+
final Node wholeChain;
57+
final Node enclosingStatement;
58+
59+
/** Creates unique names to be used for temporary variables. */
60+
interface TmpVarNameCreator {
61+
62+
/** Creates a unique temporary variable name each time it is called. */
63+
String createTmpVarName();
64+
}
65+
66+
static class Builder {
67+
final AbstractCompiler compiler;
68+
final AstFactory astFactory;
69+
TmpVarNameCreator tmpVarNameCreator;
70+
71+
private Builder(AbstractCompiler compiler) {
72+
this.compiler = checkNotNull(compiler);
73+
this.astFactory = compiler.createAstFactory();
74+
}
75+
76+
Builder setTmpVarNameCreator(TmpVarNameCreator tmpVarNameCreator) {
77+
this.tmpVarNameCreator = checkNotNull(tmpVarNameCreator);
78+
return this;
79+
}
80+
81+
/** @param wholeChain The last Node in the optional chain. Parent of all the rest. */
82+
OptionalChainRewriter build(Node wholeChain) {
83+
return new OptionalChainRewriter(this, wholeChain);
84+
}
85+
}
86+
87+
static Builder builder(AbstractCompiler compiler) {
88+
return new Builder(compiler);
89+
}
90+
91+
private OptionalChainRewriter(Builder builder, Node wholeChain) {
92+
// This class will only operate on an entire chain.
93+
checkArgument(NodeUtil.isEndOfFullOptChain(wholeChain), wholeChain);
94+
this.compiler = builder.compiler;
95+
this.astFactory = builder.astFactory;
96+
this.tmpVarNameCreator = checkNotNull(builder.tmpVarNameCreator);
97+
this.wholeChain = wholeChain;
98+
this.chainParent = checkNotNull(wholeChain.getParent(), wholeChain);
99+
this.enclosingStatement = NodeUtil.getEnclosingStatement(wholeChain);
100+
}
101+
102+
/** Rewrites the optional chain as a hook with temporary variables introduced as needed. */
103+
void rewrite() {
104+
checkState(NodeUtil.isOptChainNode(wholeChain), "already rewritten: %s", wholeChain);
105+
106+
// `first?.start.second?.start`
107+
// We search from the end of the chain and push the start nodes onto the stack, so the first
108+
// one ends up on top.
109+
ArrayDeque<Node> startNodeStack = new ArrayDeque<>();
110+
Node subchainEnd = wholeChain;
111+
while (NodeUtil.isOptChainNode(subchainEnd)) {
112+
final Node subchainStart = NodeUtil.getStartOfOptChainSegment(subchainEnd);
113+
startNodeStack.push(subchainStart);
114+
subchainEnd = subchainStart.getFirstChild();
115+
}
116+
117+
checkState(!startNodeStack.isEmpty());
118+
// Each time we rewrite the initial segment of the chain, the remaining chain gets wrapped
119+
// in a hook statement like `(tmp0 = a.b) == null ? void 0 : tmp0.rest?.of.chain?.()`,
120+
// So wholeChain ends up more deeply nested on each rewrite.
121+
// We only care about the top-most replacement here.
122+
final Node optChainReplacement = rewriteInitialSegment(startNodeStack.pop(), wholeChain);
123+
while (!startNodeStack.isEmpty()) {
124+
rewriteInitialSegment(startNodeStack.pop(), wholeChain);
125+
}
126+
127+
// Handle a non-optional call to an optional chain that ends in an element or property
128+
// access.
129+
// `(a?.optional.chain)(arg1)`
130+
// Writing JavaScript code like this is a bad idea, but it might get automatically
131+
// generated, so we must handle it.
132+
// The optional chain could evaluate to `undefined`, which we then try to call as a
133+
// function. However, if it isn't undefined, we have to preserve the correct `this` value
134+
// for the call.
135+
if (chainParent.isCall()
136+
// The chain will have been replaced by optChainReplacement during the rewriting above.
137+
&& optChainReplacement.isFirstChildOf(chainParent)
138+
// The wholeChain variable will still point to the rewritten final Node of the
139+
// chain. It will no longer be optional.
140+
&& NodeUtil.isNormalGet(wholeChain)) {
141+
final Node thisValue = wholeChain.getFirstChild();
142+
final Node tmpThisNode = getSubExprNameNode(thisValue);
143+
optChainReplacement.detach();
144+
chainParent.addChildToFront(tmpThisNode);
145+
final Node dotCallNode =
146+
astFactory
147+
.createGetProp(optChainReplacement, "call")
148+
.useSourceInfoIfMissingFromForTree(optChainReplacement);
149+
chainParent.addChildToFront(dotCallNode);
150+
}
151+
152+
// Transpilation of the optional chain adds `let` declarations for temporary variables.
153+
// NOTE: If this class is being used before transpilation, it's OK to use `let`, since it will
154+
// be transpiled away, if necessary. If it is being used after transpilation, then using `let`
155+
// must be OK, because optional chains weren't transpiled away and `let` existed before they
156+
// did.
157+
final Node enclosingScript = NodeUtil.getEnclosingScript(enclosingStatement);
158+
NodeUtil.addFeatureToScript(enclosingScript, Feature.LET_DECLARATIONS, compiler);
159+
compiler.reportChangeToChangeScope(enclosingScript);
160+
}
161+
162+
/**
163+
* Rewrites the first part of a possibly-multi-part optional chain.
164+
*
165+
* <p>e.g.
166+
*
167+
* <pre>{@code
168+
* a()?.b.c?.d;
169+
* // becomes
170+
* let tmp0;
171+
* (tmp0 = a()) == null
172+
* ? void 0
173+
* : tmp0.b.c?d;
174+
* }</pre>
175+
*
176+
* @param fullChainStart The very first `?.` node
177+
* @param fullChainEnd The very last optional chain node.
178+
* @return The hook expression that replaced the chain.
179+
*/
180+
Node rewriteInitialSegment(final Node fullChainStart, final Node fullChainEnd) {
181+
// `receiverNode?.restOfChain`
182+
Node receiverNode = fullChainStart.getFirstChild();
183+
// for `a?.b.c?.d`, this will be `a?.b.c`, because the NodeUtil method finds the end
184+
// of the sub-chain, not the full chain.
185+
final Node initialChainEnd = NodeUtil.getEndOfOptChainSegment(fullChainStart);
186+
187+
// If the receiver is an optional chain, we weren't really given the start of a full
188+
// chain.
189+
checkArgument(!NodeUtil.isOptChainNode(receiverNode), receiverNode);
190+
191+
// change the initial chain's nodes to be non-optional
192+
convertToNonOptionalChainSegment(initialChainEnd);
193+
194+
final Node placeholder = IR.empty();
195+
fullChainEnd.replaceWith(placeholder);
196+
// NOTE: convertToNonOptionalChain() above will have made the chain start
197+
// and all the other nodes in the first segment of the chain non-optional,
198+
// so fullChainStart.isCall() is the right test here.
199+
if (NodeUtil.isNormalGet(receiverNode) && fullChainStart.isCall()) {
200+
// `expr.prop?.(x).y`
201+
// Needs to become
202+
// `(t1 = (t0 = expr).prop) == null ? void 0 : t1.call(t0, x).y`
203+
final Node thisValue = receiverNode.getFirstChild();
204+
final Node tmpThisNode = getSubExprNameNode(thisValue);
205+
final Node tmpReceiverNode = getSubExprNameNode(receiverNode);
206+
receiverNode = fullChainStart.getFirstChild().detach();
207+
fullChainStart.addChildToFront(tmpThisNode);
208+
fullChainStart.addChildToFront(
209+
astFactory
210+
.createGetProp(tmpReceiverNode, "call")
211+
.useSourceInfoIfMissingFromForTree(receiverNode));
212+
} else {
213+
// `expr?.x.y`
214+
// needs to become
215+
// `((t0 = expr) == null) ? void 0 : t0.x.y`
216+
final Node tmpReceiverNode = getSubExprNameNode(receiverNode);
217+
receiverNode = fullChainStart.getFirstChild();
218+
receiverNode.replaceWith(tmpReceiverNode);
219+
}
220+
final Node optChainReplacement =
221+
astFactory
222+
.createHook(
223+
astFactory.createEq(receiverNode, astFactory.createNull()),
224+
astFactory.createUndefinedValue(),
225+
fullChainEnd)
226+
.useSourceInfoIfMissingFromForTree(fullChainEnd);
227+
placeholder.replaceWith(optChainReplacement);
228+
229+
return optChainReplacement;
230+
}
231+
232+
/**
233+
* Given an expression node, declare a temporary variable to hold that expression and replace the
234+
* expression with `(tmp = expr)`.
235+
*
236+
* <p>e.g. `subExpr.moreExpr` becomes `(tmp = subExpr).moreExpr`, and `let tmp;` gets inserted
237+
* before the enclosing statement of this optional chain.
238+
*
239+
* @param subExpr The sub expression Node
240+
* @return A detached NAME node for the temporary variable name and with source info and type
241+
* matching `subExpr`, that may be inserted where needed.
242+
*/
243+
Node getSubExprNameNode(Node subExpr) {
244+
String tempVarName = declareTempVarName(subExpr);
245+
Node placeholder = IR.empty();
246+
subExpr.replaceWith(placeholder);
247+
Node replacement =
248+
astFactory.createAssign(tempVarName, subExpr).useSourceInfoIfMissingFromForTree(subExpr);
249+
placeholder.replaceWith(replacement);
250+
return replacement.getFirstChild().cloneNode();
251+
}
252+
253+
/**
254+
* Declare a temporary variable name that will be used to hold the given value.
255+
*
256+
* <p>The generated declaration has no assignment, it's just `let tmp;`.
257+
*
258+
* @param valueNode A node from which to copy the source info and type to be used for the new
259+
* variable.
260+
* @return the name used for the new temporary variable.
261+
*/
262+
String declareTempVarName(Node valueNode) {
263+
String tempVarName = tmpVarNameCreator.createTmpVarName();
264+
Node declarationStatement =
265+
astFactory.createSingleLetNameDeclaration(tempVarName).srcrefTree(valueNode);
266+
enclosingStatement.getParent().addChildBefore(declarationStatement, enclosingStatement);
267+
return tempVarName;
268+
}
269+
270+
/**
271+
* Given the end of an optional chain segment. Change all nodes from the end down to the start
272+
* into non-optional nodes.
273+
*/
274+
private static void convertToNonOptionalChainSegment(Node endOfOptChainSegment) {
275+
// Since part of changing the nodes removes the isOptionalChainStart() marker we look for to
276+
// know we're done, this logic is easier to read if we just find all the nodes first, then
277+
// change them.
278+
final ArrayDeque<Node> segmentNodes = new ArrayDeque<>();
279+
Node segmentNode = endOfOptChainSegment;
280+
while (true) {
281+
checkState(NodeUtil.isOptChainNode(segmentNode), segmentNode);
282+
segmentNodes.add(segmentNode);
283+
if (segmentNode.isOptionalChainStart()) {
284+
break;
285+
} else {
286+
segmentNode = segmentNode.getFirstChild();
287+
}
288+
}
289+
for (Node n : segmentNodes) {
290+
n.setIsOptionalChainStart(false);
291+
n.setToken(getNonOptChainToken(n.getToken()));
292+
}
293+
}
294+
295+
private static Token getNonOptChainToken(Token optChainToken) {
296+
switch (optChainToken) {
297+
case OPTCHAIN_CALL:
298+
return Token.CALL;
299+
case OPTCHAIN_GETELEM:
300+
return Token.GETELEM;
301+
case OPTCHAIN_GETPROP:
302+
return Token.GETPROP;
303+
default:
304+
throw new IllegalStateException("Should be an OPTCHAIN token: " + optChainToken);
305+
}
306+
}
307+
}

0 commit comments

Comments
 (0)