All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.javascript.jscomp.OptionalChainRewriter Maven / Gradle / Ivy

Go to download

Closure Compiler is a JavaScript optimizing compiler. It parses your JavaScript, analyzes it, removes dead code and rewrites and minimizes what's left. It also checks syntax, variable references, and types, and warns about common JavaScript pitfalls. It is used in many of Google's JavaScript apps, including Gmail, Google Web Search, Google Maps, and Google Docs.

There is a newer version: v20240317
Show newest version
/*
 * Copyright 2020 The Closure Compiler Authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.google.javascript.jscomp;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;

import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.javascript.jscomp.parsing.parser.FeatureSet.Feature;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.Node;
import java.util.ArrayDeque;
import org.jspecify.nullness.Nullable;

/**
 * Rewrites a single optional chain as one or more nested hook expressions.
 *
 * 

Optional chains contained in OPTCHAIN_GETELEM indices or OPTCHAIN_CALL arguments are not * rewritten. * *

Example: * *


 *   a?.b[obj?.index]?.c?.(obj?.arg)
 *   // becomes
 *   (tmp0 = a) == null
 *       ? void 0
 *       : (tmp1 = tmp0.b[obj?.index]) == null
 *           ? void 0
 *           : (tmp2 = tmp1.c) == null
 *               ? void 0
 *               : tmp2.call(tmp1, obj?.arg);
 * 
* *

The unit tests for this class are in RewriteOptionalChainingOperatorTest, because it's most * convenient to test this class as part of the transpilation pass that uses it. */ class OptionalChainRewriter { final AbstractCompiler compiler; final AstFactory astFactory; final TmpVarNameCreator tmpVarNameCreator; // If a scope is provided, newly created variables will be declared in that scope final @Nullable Scope scope; final Node chainParent; final Node wholeChain; final Node insertionPoint; final ArrayDeque deletesToDelete; /** Creates unique names to be used for temporary variables. */ interface TmpVarNameCreator { /** Creates a unique temporary variable name each time it is called. */ String createTmpVarName(); } static class Builder { final AbstractCompiler compiler; final AstFactory astFactory; TmpVarNameCreator tmpVarNameCreator; Scope scope; private Builder(AbstractCompiler compiler) { this.compiler = checkNotNull(compiler); this.astFactory = compiler.createAstFactory(); } @CanIgnoreReturnValue Builder setTmpVarNameCreator(TmpVarNameCreator tmpVarNameCreator) { this.tmpVarNameCreator = checkNotNull(tmpVarNameCreator); return this; } /** Optionally sets a scope in which the rewriter will declare all temporary variables */ @CanIgnoreReturnValue Builder setScope(Scope scope) { this.scope = checkNotNull(scope); return this; } /** @param wholeChain The last Node in the optional chain. Parent of all the rest. */ OptionalChainRewriter build(Node wholeChain) { return new OptionalChainRewriter(this, wholeChain); } } static Builder builder(AbstractCompiler compiler) { return new Builder(compiler); } private OptionalChainRewriter(Builder builder, Node wholeChain) { // This class will only operate on an entire chain. checkArgument(NodeUtil.isEndOfFullOptChain(wholeChain), wholeChain); this.compiler = builder.compiler; this.astFactory = builder.astFactory; this.tmpVarNameCreator = checkNotNull(builder.tmpVarNameCreator); this.scope = builder.scope; this.wholeChain = wholeChain; this.chainParent = checkNotNull(wholeChain.getParent(), wholeChain); // Insert temporaries just before the enclosing statement. Node enclosingStatement = NodeUtil.getEnclosingStatement(wholeChain); if (enclosingStatement.getPrevious() != null && enclosingStatement.getPrevious().isLabelName()) { // Do not insert temporaries between a label name and its statement. // e.g. `{ label: for (const a of b?.c) {...}` // AST shape is // BLOCK // LABEL // parent of enclosingStatement - insert above this // LABEL_NAME // enclosingStatement this.insertionPoint = enclosingStatement.getParent(); } else { this.insertionPoint = enclosingStatement; } this.deletesToDelete = new ArrayDeque<>(); } /** Rewrites the optional chain as a hook with temporary variables introduced as needed. */ void rewrite() { checkState(NodeUtil.isOptChainNode(wholeChain), "already rewritten: %s", wholeChain); // `first?.start.second?.start` // We search from the end of the chain and push the start nodes onto the stack, so the first // one ends up on top. ArrayDeque startNodeStack = new ArrayDeque<>(); Node subchainEnd = wholeChain; while (NodeUtil.isOptChainNode(subchainEnd)) { final Node subchainStart = NodeUtil.getStartOfOptChainSegment(subchainEnd); startNodeStack.push(subchainStart); subchainEnd = subchainStart.getFirstChild(); } checkState(!startNodeStack.isEmpty()); // Each time we rewrite the initial segment of the chain, the remaining chain gets wrapped // in a hook statement like `(tmp0 = a.b) == null ? void 0 : tmp0.rest?.of.chain?.()`, // So wholeChain ends up more deeply nested on each rewrite. // We only care about the top-most replacement here. final Node optChainReplacement = rewriteInitialSegment(startNodeStack.pop(), wholeChain); while (!startNodeStack.isEmpty()) { rewriteInitialSegment(startNodeStack.pop(), wholeChain); } // Handle a non-optional call to an optional chain that ends in an element or property // access. // `(a?.optional.chain)(arg1)` // Writing JavaScript code like this is a bad idea, but it might get automatically // generated, so we must handle it. // The optional chain could evaluate to `undefined`, which we then try to call as a // function. However, if it isn't undefined, we have to preserve the correct `this` value // for the call. if (chainParent.isCall() // The chain will have been replaced by optChainReplacement during the rewriting above. && optChainReplacement.isFirstChildOf(chainParent) // The wholeChain variable will still point to the rewritten final Node of the // chain. It will no longer be optional. && NodeUtil.isNormalGet(wholeChain)) { final Node thisValue = wholeChain.getFirstChild(); final Node tmpThisNode = getSubExprNameNode(thisValue); optChainReplacement.detach(); chainParent.addChildToFront(tmpThisNode); final Node dotCallNode = astFactory .createGetPropWithUnknownType(optChainReplacement, "call") .srcrefTreeIfMissing(optChainReplacement); chainParent.addChildToFront(dotCallNode); } // Report changes here; chainParent can get deleted below this. // E.g. code `(p = a?.b)=>{ return p}` changes to `let tmp0; (p = (tmp0=a)==null?void // 0:tmp0.b)=>{return p}` // This requires recording scope changes at two places: // 1. the function scope changed from rewriting to HOOK (recorded here), // 2. SCRIPT scope changed due to the `let tmp0` inserted (recorded in `declareTempVarName` // where declarations are created). compiler.reportChangeToEnclosingScope(chainParent); if (chainParent.isDelProp()) { // With rewriting above, // `delete a?.b?.c` // synthesizes additional deletes // `delete (tmp0 = a) == null ? true : delete (tmp1 = tmp0.b) == null ? true : delete tmp1.c;` // ^^^^^^ ^^^^^^ // // But we must generate only: // `(tmp0 = a) == null ? true : (tmp1 = tmp0.b) == null ? true : delete tmp1.c;` // That is, the preceding deletes for every hook must be removed. while (!deletesToDelete.isEmpty()) { Node delete = deletesToDelete.removeFirst(); checkState(delete.getFirstChild().isHook(), delete); Node hook = delete.getFirstChild(); hook.detach(); delete.replaceWith(hook); compiler.reportChangeToEnclosingScope(hook); } } // Transpilation of the optional chain adds `let` declarations for temporary variables. // NOTE: If this class is being used before transpilation, it's OK to use `let`, since it will // be transpiled away, if necessary. If it is being used after transpilation, then using `let` // must be OK, because optional chains weren't transpiled away and `let` existed before they // did. final Node enclosingScript = NodeUtil.getEnclosingScript(insertionPoint); NodeUtil.addFeatureToScript(enclosingScript, Feature.LET_DECLARATIONS, compiler); } /** * Rewrites the first part of a possibly-multi-part optional chain. * *

e.g. * *

{@code
   * a()?.b.c?.d;
   * // becomes
   * let tmp0;
   * (tmp0 = a()) == null
   *     ? void 0
   *     : tmp0.b.c?d;
   * }
* * If this optional chain is under a delete, the l-r rewriting must synthesize another `delete` to * ensure the next chain(if present), knows that it must delete the `fullChainEnd`. * *

e.g. * * * *

{@code
   * * delete a?.b.c?.d;
   * * // becomes
   * * let tmp0;
   * * (tmp0 = a()) == null
   * *     ? true
   * *     : delete tmp0.b.c?d;
   * *
   * }
* * @param fullChainStart The very first `?.` node * @param fullChainEnd The very last optional chain node. * @return The hook expression that replaced the chain. */ private Node rewriteInitialSegment(final Node fullChainStart, final Node fullChainEnd) { // `receiverNode?.restOfChain` Node receiverNode = fullChainStart.getFirstChild(); // for `a?.b.c?.d`, this will be `a?.b.c`, because the NodeUtil method finds the end // of the sub-chain, not the full chain. final Node initialChainEnd = NodeUtil.getEndOfOptChainSegment(fullChainStart); // Is this optional chain under delete boolean isBeingDeleted = fullChainEnd.getParent().isDelProp(); if (isBeingDeleted) { deletesToDelete.addLast(fullChainEnd.getParent()); } // If the receiver is an optional chain, we weren't really given the start of a full // chain. checkArgument(!NodeUtil.isOptChainNode(receiverNode), receiverNode); // change the initial chain's nodes to be non-optional NodeUtil.convertToNonOptionalChainSegment(initialChainEnd); final Node placeholder = IR.empty(); fullChainEnd.replaceWith(placeholder); // NOTE: convertToNonOptionalChain() above will have made the chain start // and all the other nodes in the first segment of the chain non-optional, // so fullChainStart.isCall() is the right test here. if (NodeUtil.isNormalGet(receiverNode) && fullChainStart.isCall()) { // `expr.prop?.(x).y` // Needs to become // `(t1 = (t0 = expr).prop) == null ? void 0 : t1.call(t0, x).y` final Node thisValue = receiverNode.getFirstChild(); final Node tmpThisNode = getSubExprNameNode(thisValue); final Node tmpReceiverNode = getSubExprNameNode(receiverNode); receiverNode = fullChainStart.removeFirstChild(); fullChainStart.addChildToFront(tmpThisNode); fullChainStart.addChildToFront( astFactory .createGetPropWithUnknownType(tmpReceiverNode, "call") .srcrefTreeIfMissing(receiverNode)); } else { // `expr?.x.y` // needs to become // `((t0 = expr) == null) ? void 0 : t0.x.y` final Node tmpReceiverNode = getSubExprNameNode(receiverNode); receiverNode = fullChainStart.getFirstChild(); receiverNode.replaceWith(tmpReceiverNode); } final Node optChainReplacement = astFactory .createHook( astFactory.createEq(receiverNode, astFactory.createNull()), isBeingDeleted ? astFactory.createBoolean(true) : astFactory.createUndefinedValue(), isBeingDeleted ? astFactory.createDelProp(fullChainEnd) : fullChainEnd) .srcrefTreeIfMissing(fullChainEnd); placeholder.replaceWith(optChainReplacement); return optChainReplacement; } /** * Given an expression node, declare a temporary variable to hold that expression and replace the * expression with `(tmp = expr)`. * *

e.g. `subExpr.moreExpr` becomes `(tmp = subExpr).moreExpr`, and `let tmp;` gets inserted * before the enclosing statement of this optional chain. * * @param subExpr The sub expression Node * @return A detached NAME node for the temporary variable name and with source info and type * matching `subExpr`, that may be inserted where needed. */ Node getSubExprNameNode(Node subExpr) { String tempVarName = declareTempVarName(subExpr); Node placeholder = IR.empty(); subExpr.replaceWith(placeholder); Node replacement = astFactory.createAssign(tempVarName, subExpr).srcrefTreeIfMissing(subExpr); placeholder.replaceWith(replacement); return replacement.getFirstChild().cloneNode(); } /** * Declare a temporary variable name that will be used to hold the given value. * *

The generated declaration has no assignment, it's just `let tmp;`. * * @param valueNode A node from which to copy the source info and type to be used for the new * variable. * @return the name used for the new temporary variable. */ String declareTempVarName(Node valueNode) { String tempVarName = tmpVarNameCreator.createTmpVarName(); Node declarationStatement = astFactory.createSingleLetNameDeclaration(tempVarName).srcrefTree(valueNode); declarationStatement.getFirstChild().setInferredConstantVar(true); declarationStatement.insertBefore(insertionPoint); compiler.reportChangeToEnclosingScope(declarationStatement); if (scope != null) { scope.declare(tempVarName, declarationStatement.getFirstChild(), /* input= */ null); } return tempVarName; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy