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

com.google.javascript.jscomp.ProcessClosureProvidesAndRequires 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 2019 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.common.annotations.GwtIncompatible;
import com.google.common.base.Preconditions;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.JSDocInfo;
import com.google.javascript.rhino.Node;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.jspecify.nullness.Nullable;

/**
 * Replaces `goog.provide` calls and removes goog.{require,requireType,forwardDeclare} calls.
 *
 * 

We expect all `goog.modules` and `goog.require`s in modules to have been rewritten. This is * why all remaining require/requireType calls must refer to a `goog.provide`, although the original * JS code may contain `goog.require`s of a `goog.module`. * *

This also annotates all provided namespace definitions `a.b = {};` with {@link * Node#IS_NAMESPACE} so that later passes know they refer to a `goog.provide`d namespace. * *

If modules have not been rewritten, this pass also includes legacy Closure module namespaces * in the list of {@link ProvidedName}s. */ class ProcessClosureProvidesAndRequires implements CompilerPass { static final DiagnosticType TYPEDEF_CHILD_OF_PROVIDE = DiagnosticType.error( "JSC_TYPEDEF_CHILD_OF_PROVIDE", "invalid @typedef goog.provide {0}\n" + "Parent namespace {1} is goog.provided and initialized in the same file"); // The root Closure namespace private static final String GOOG = "goog"; private final AbstractCompiler compiler; private final JSChunkGraph chunkGraph; // Use a LinkedHashMap because the goog.provides must be processed in a deterministic order. private final Map providedNames = new LinkedHashMap<>(); // If this is true, rewriting will not remove any goog.provide or goog.require calls private final boolean preserveGoogProvidesAndRequires; private final List requiresToBeRemoved = new ArrayList<>(); // Whether this instance has already rewritten goog.provides, which can only happen once private boolean hasRewritingOccurred = false; private final Set forwardDeclaresToRemove = new HashSet<>(); private final AstFactory astFactory; ProcessClosureProvidesAndRequires( AbstractCompiler compiler, boolean preserveGoogProvidesAndRequires) { this.compiler = compiler; this.chunkGraph = compiler.getModuleGraph(); this.preserveGoogProvidesAndRequires = preserveGoogProvidesAndRequires; this.astFactory = compiler.createAstFactory(); } /** When invoked as compiler pass, we rewrite all provides and requires. */ @Override public void process(Node externs, Node root) { rewriteProvidesAndRequires(externs, root); } /** Collects all `goog.provide`s in the given namespace and warns on invalid code */ Map collectProvidedNames(Node externs, Node root) { if (this.providedNames.isEmpty()) { // goog is special-cased because it is provided in Closure's base library. providedNames.put( GOOG, new ProvidedNameBuilder().setNamespace(GOOG).setNode(null).setChunk(null).build()); NodeTraversal.traverseRoots(compiler, new CollectDefinitions(), externs, root); } return this.providedNames; } /** * Rewrites all provides and requires in the given namespace. * *

Call this instead of {@link #collectProvidedNames(Node, Node)} if you want rewriting. */ void rewriteProvidesAndRequires(Node externs, Node root) { checkState(!hasRewritingOccurred, "Cannot call rewriteProvidesAndRequires twice per instance"); hasRewritingOccurred = true; collectProvidedNames(externs, root); for (ProvidedName pn : providedNames.values()) { pn.replace(preserveGoogProvidesAndRequires, providedNames); } for (Node closureRequire : requiresToBeRemoved) { compiler.reportChangeToEnclosingScope(closureRequire); closureRequire.detach(); } for (Node forwardDeclare : forwardDeclaresToRemove) { NodeUtil.deleteNode(forwardDeclare, compiler); } } private class CollectDefinitions implements NodeTraversal.Callback { @Override public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) { // Don't recurse into modules, which cannot have goog.provides. We do need to handle legacy // goog.modules, but we do that quickly here rather than descending all the way into them. // We completely ignore ES modules and CommonJS modules. if ((n.isModuleBody() && n.getParent().getBooleanProp(Node.GOOG_MODULE)) || NodeUtil.isBundledGoogModuleScopeRoot(n)) { Node googModuleCall = n.getFirstChild(); String closureNamespace = googModuleCall.getFirstChild().getSecondChild().getString(); Node maybeLegacyNamespaceCall = googModuleCall.getNext(); if (maybeLegacyNamespaceCall != null && NodeUtil.isGoogModuleDeclareLegacyNamespaceCall(maybeLegacyNamespaceCall)) { processLegacyModuleCall(closureNamespace, googModuleCall, t.getChunk()); } return false; } return !n.isModuleBody(); } @Override public void visit(NodeTraversal t, Node n, Node parent) { switch (n.getToken()) { case CALL: Node left = n.getFirstChild(); if (left.isGetProp()) { Node name = left.getFirstChild(); if (name.isName() && GOOG.equals(name.getString())) { // For the sake of simplicity, we report code changes // when we see a provides/requires, and don't worry about // reporting the change when we actually do the replacement. switch (left.getString()) { case "require": case "requireType": if (isValidPrimitiveCall(t, n)) { processRequireCall(n, parent); } break; case "provide": if (isValidPrimitiveCall(t, n)) { processProvideCall(t, n, parent); } break; case "forwardDeclare": if (isValidPrimitiveCall(t, n)) { processForwardDeclare(n, parent); } break; } } } break; case ASSIGN: case NAME: // If this is an assignment to a provided name, remove the provided object. handleCandidateProvideDefinition(t, n, parent); break; case EXPR_RESULT: handleStubDefinition(t, n); break; default: break; } } } private boolean isValidPrimitiveCall(NodeTraversal t, Node n) { // Ignore invalid primitives if we didn't strip module sugar. if (compiler.getOptions().shouldPreserveGoogModule()) { return true; } return t.inGlobalHoistScope() && n.getParent().isExprResult(); } /** Handles a goog.require or goog.requireType call. */ private void processRequireCall(Node call, Node parent) { if (!verifyOnlyArgumentIsString(call)) { return; } if (!preserveGoogProvidesAndRequires) { requiresToBeRemoved.add(parent); } } /** Handles a goog.module that is a legacy namespace. */ private void processLegacyModuleCall(String namespace, Node googModuleCall, JSChunk chunk) { registerAnyProvidedPrefixes(namespace, googModuleCall, chunk); providedNames.put( namespace, new ProvidedNameBuilder() .setNamespace(namespace) .setNode(googModuleCall) .setChunk(chunk) .setExplicit(true) .setFromLegacyModule(true) .build()); } /** Handles a goog.provide call. */ private void processProvideCall(NodeTraversal t, Node call, Node parent) { checkState(call.isCall()); if (!verifyOnlyArgumentIsString(call)) { return; } Node left = call.getFirstChild(); Node arg = left.getNext(); String ns = arg.getString(); JSDocInfo info = NodeUtil.getBestJSDocInfo(call); boolean isImplicitlyInitialized = info != null && info.isProvideAlreadyProvided(); if (providedNames.containsKey(ns)) { ProvidedName previouslyProvided = providedNames.get(ns); if (!previouslyProvided.isExplicitlyProvided()) { previouslyProvided.addProvide(parent, t.getChunk(), /* explicit= */ true, chunkGraph); } } else { registerAnyProvidedPrefixes(ns, parent, t.getChunk()); providedNames.put( ns, new ProvidedNameBuilder() .setNamespace(ns) .setNode(parent) .setChunk(t.getChunk()) .setExplicit(true) .setHasImplicitInitialization(isImplicitlyInitialized) .build()); } } /** * Handles a stub definition for a goog.provided name (e.g. a @typedef or a definition from * externs) * * @param exprResult EXPR_RESULT node. */ private void handleStubDefinition(NodeTraversal t, Node exprResult) { if (!t.inGlobalHoistScope()) { return; } boolean isExternStub = exprResult.isFromExterns(); boolean isTypedefStub = isTypedefStubDeclaration(exprResult); // recognize @typedefs only if using this pass for typechecking via // collectProvidedNames. We don't want rewriting to depend on @typedef annotations. boolean isValidTypedefStubDefinition = isTypedefStub && !this.hasRewritingOccurred; if (isValidTypedefStubDefinition || isExternStub) { if (exprResult.getFirstChild().isQualifiedName()) { String name = exprResult.getFirstChild().getQualifiedName(); ProvidedName pn = providedNames.get(name); if (pn != null) { pn.addDefinition(exprResult, t.getChunk(), chunkGraph); } } } if (isTypedefStub) { checkNestedTypedefProvide(exprResult); } } /** * Checks that code doesn't use goog.provides in a way that will cause broken output code. * * @param exprResult an EXPR_RESULT node with an @typedef type */ private void checkNestedTypedefProvide(Node exprResult) { // forbid this pattern: // goog.provide('my.parent'); // goog.provide('my.parent.ChildTypedef'); // my.parent = [...]; // some initialization, doesn't matter what // /** @typedef {...} */ // my.parent.ChildType; // at one point, this pattern was supported. now it would produce code that crashes at // runtime because the compiler would initializer `my.parent.ChildType = {}` before // `my.parent = [...]`, so report an error. String name = exprResult.getFirstChild().getQualifiedName(); if (name == null || !name.contains(".")) { // @typedefs on simple names are okay. return; } if (!providedNames.containsKey(name)) { // non-provided names don't matter. return; } String parentName = name.substring(0, name.lastIndexOf(".")); ProvidedName parent = providedNames.get(parentName); Node parentDefinition = parent.getCandidateDefinition(); if (parentDefinition == null || !parentDefinition.getStaticSourceFile().equals(exprResult.getStaticSourceFile()) || isTypedefStubDeclaration(parentDefinition)) { return; } compiler.report(JSError.make(exprResult, TYPEDEF_CHILD_OF_PROVIDE, name, parentName)); } private boolean isTypedefStubDeclaration(Node statement) { if (!statement.isExprResult()) { return false; } JSDocInfo info = NodeUtil.getBestJSDocInfo(statement); return info != null && info.hasTypedefType(); } /** Handles a candidate definition for a goog.provided name. */ private void handleCandidateProvideDefinition(NodeTraversal t, Node n, Node parent) { if (!t.inGlobalHoistScope()) { return; } String name = null; switch (n.getParent().getToken()) { case LET: case CONST: if (!t.inGlobalScope()) { // let/const in the global hoist scope but not the global scope are not globals return; } // fall through case VAR: name = n.getString(); break; case EXPR_RESULT: if (n.isAssign()) { name = n.getFirstChild().getQualifiedName(); } break; case CLASS: // Class and function provides are forbidden; see ProcessClosurePrimitives's case FUNCTION: // CLASS_NAMESPACE_ERROR and FUNCTION_NAMESPACE_ERROR. default: break; } if (name == null) { return; } ProvidedName pn = providedNames.get(name); if (pn != null) { pn.addDefinition(parent, t.getChunk(), chunkGraph); } } /** Marks a goog.forwardDeclare call for removal. */ private void processForwardDeclare(Node n, Node parent) { CodingConvention convention = compiler.getCodingConvention(); List typeDeclarations = convention.identifyTypeDeclarationCall(n); if (!preserveGoogProvidesAndRequires && typeDeclarations != null && typeDeclarations.size() == 1) { // Forward declaration was recorded and we can remove the call. Node toRemove = parent.isExprResult() ? parent : parent.getParent(); forwardDeclaresToRemove.add(toRemove); } } /** * Verifies that a method call has exactly one argument, and that it's a string literal. * * @return Whether the argument checked out okay */ private boolean verifyOnlyArgumentIsString(Node call) { Node arg = call.getSecondChild(); return arg != null && arg.isStringLit() && arg.getNext() == null; } /** * Registers ProvidedNames for prefix namespaces if they haven't already been defined. The prefix * namespaces must be registered in order from shortest to longest. * * @param ns The namespace whose prefixes may need to be provided. * @param node The EXPR of the provide call. * @param module The current module. */ private void registerAnyProvidedPrefixes(String ns, Node node, JSChunk module) { int pos = ns.indexOf('.'); while (pos != -1) { String prefixNs = ns.substring(0, pos); pos = ns.indexOf('.', pos + 1); if (providedNames.containsKey(prefixNs)) { providedNames.get(prefixNs).addProvide(node, module, /* explicit= */ false, chunkGraph); } else { providedNames.put( prefixNs, new ProvidedNameBuilder() .setNamespace(prefixNs) .setNode(node) .setChunk(module) .setExplicit(false) .build()); } } } private class ProvidedNameBuilder { private String namespace; private Node node; private JSChunk chunk; private boolean explicit; private boolean fromLegacyModule; private boolean hasImplicitInitialization; ProvidedNameBuilder setNamespace(String namespace) { this.namespace = namespace; return this; } /** * @param node Can be null (for GOOG or an implicit name), an EXPR_RESULT for a goog.provide, or * an EXPR_RESULT or name declaration for a previously provided name. */ ProvidedNameBuilder setNode(@Nullable Node node) { this.node = node; return this; } ProvidedNameBuilder setChunk(@Nullable JSChunk chunk) { this.chunk = chunk; return this; } /** * @param explicit Whether this came from an actual goog.provide('a.b.c'); call */ ProvidedNameBuilder setExplicit(boolean explicit) { this.explicit = explicit; return this; } /** * @param alreadyInitialized Whether this came from an actual goog.provide('a.b.c'); call */ ProvidedNameBuilder setHasImplicitInitialization(boolean alreadyInitialized) { this.hasImplicitInitialization = alreadyInitialized; return this; } /** Whether this comes from a legacy goog.module */ ProvidedNameBuilder setFromLegacyModule(boolean fromLegacyModule) { this.fromLegacyModule = fromLegacyModule; return this; } ProvidedName build() { return new ProvidedName(this, chunkGraph, compiler, astFactory); } } // ------------------------------------------------------------------------- /** * Stores information about a Closure namespace created by a goog.provide * *

There are three ways that we find these namespaces in the AST: * *

    *
  • An explicit goog.provide. `goog.provide('a.b.c');` creates a ProvidedName for 'a.b.c'. *
  • An implicit parent namespace. `goog.provide('a.b.c');` creates a ProvidedName for 'a.b'. *
  • A provide definition processed by an earlier run. `a.b.c = {};` when annotated * IS_NAMESPACE *
*/ static class ProvidedName { // The Closure namespace this name represents, e.g. `a.b` for `goog.provide('a.b');` private final String namespace; // The first node in the AST that creates this ProvidedName. // This is always a goog.provide('a.b'), null (for implicit namespaces and 'goog'), or an // assignment or declaration for a 'previously provided' name or parent namespace of such. // This should only be used for source info and a place to hang namespace definitions. private final Node firstNode; // The chunk where this namespace was first goog.provided, if chunks exist. */ private final JSChunk firstChunk; // When set the namespace will not have an initialization added, even if the namespace // initialization is not otherwise visibile to this pass. This allows for trivial aliasing of // a tree of namespaces. private final boolean hasImplicitInitialization; // The node where the call was explicitly goog.provided. Null if the namespace is implicit. // If this is previously provided, this will instead be the expression or declaration marked // as IS_NAMESPACE. private @Nullable Node explicitNode = null; // The candidate definition for this namespace. For example, given // goog.provide('a.b'); // /** @constructor * / // a.b = function() {}; // the 'candidate definition' of 'a.b' is the GETPROP 'a.b' from the constructor declaration. private @Nullable Node candidateDefinition = null; // The minimum chunk where the provide namespace definition must appear. If child namespaces of // this provide appear in multiple chunks, this chunk must be earlier than all child // namespace's chunks. private @Nullable JSChunk minimumChunk = null; // The replacement declaration. Null until replace() has been called. private @Nullable Node replacementNode = null; // Whether this comes from a goog.module with declareLegacyNamespace. private final boolean fromLegacyModule; private final AbstractCompiler compiler; private final AstFactory astFactory; private ProvidedName( ProvidedNameBuilder builder, JSChunkGraph chunkGraph, AbstractCompiler compiler, AstFactory astFactory) { this.compiler = compiler; this.astFactory = astFactory; Node node = builder.node; Preconditions.checkArgument( node == null || NodeUtil.isExprCall(node) || (!builder.explicit && (NodeUtil.isExprAssign(node) || NodeUtil.isNameDeclaration(node) || (node.isExprResult() && node.getFirstChild().isQualifiedName()))), node); this.namespace = builder.namespace; this.firstNode = builder.node; this.firstChunk = builder.chunk; this.fromLegacyModule = builder.fromLegacyModule; this.hasImplicitInitialization = builder.hasImplicitInitialization; addProvide(node, builder.chunk, builder.explicit, chunkGraph); } /** * Adds an implicit or explicit provide. * *

Every provided name can have multiple implicit provides but a maximum of one explicit * provide. * * @param node the EXPR_RESULT representing this provide or possible a VAR for a previously * provided name. null if implicit. */ void addProvide(Node node, JSChunk chunk, boolean explicit, JSChunkGraph chunkGraph) { if (explicit) { // goog.provide('name.space'); checkState(explicitNode == null); checkArgument(node.isExprResult(), node); explicitNode = node; } updateMinimumChunk(chunk, chunkGraph); } /** Whether there existed a `goog.provide('a.b');` for this name 'a.b' */ boolean isExplicitlyProvided() { return explicitNode != null; } boolean hasImplicitInitialization() { return hasImplicitInitialization; } boolean isFromLegacyModule() { return this.fromLegacyModule; } private boolean hasCandidateDefinition() { return candidateDefinition != null; } /** * Returns the `goog.provide` or legacy namespace `goog.module` call that created this name, if * any, or otherwise the first 'previous provide' assignment that created this name. */ Node getFirstProvideCall() { return firstNode; } /** * Returns the definition of this provided namespace in the input code, if any, or null. * *

For example, this returns `a.b = class {};` given 'a.b' in * *

     *   goog.provide('a.b');
     *   a.b = class {};
     * 
* * Note: this method will only return candidate definitions that count towards provide * rewriting. If a name is defined, then provided, the candidate definition will not be the * early definition. This doesn't completely mimic uncompiled behavior, but supports some legacy * code. Externs definitions never count. */ Node getCandidateDefinition() { return candidateDefinition; } /** Returns the Closure namespace of this provide, e.g. "a.b" for `goog.provide('a.b');` */ String getNamespace() { return namespace; } /** * Records function declaration, variable declarations, and assignments that refer to this * provided namespace. * *

This pass gives preference to declarations. If no declaration exists, records a reference * to an assignment so it can be repurposed later into a declaration. */ private void addDefinition(Node node, JSChunk module, JSChunkGraph chunkGraph) { Preconditions.checkArgument( node.isExprResult() // assign || node.isFunction() || NodeUtil.isNameDeclaration(node)); checkArgument(explicitNode != node); if ((candidateDefinition == null) || !node.isExprResult()) { candidateDefinition = node; updateMinimumChunk(module, chunkGraph); } } private void updateMinimumChunk(JSChunk newChunk, JSChunkGraph chunkGraph) { if (minimumChunk == null) { minimumChunk = newChunk; } else if (chunkGraph.getChunkCount() > 1) { minimumChunk = chunkGraph.getDeepestCommonDependencyInclusive(minimumChunk, newChunk); } else { // If there is no module graph, then there must be exactly one // module in the program. checkState(newChunk == minimumChunk, "Missing module graph"); } } /** * Replace the provide statement. * *

If we're providing a name with no definition, then create one. If we're providing a name * with a duplicate definition, then make sure that definition becomes a declaration. */ private void replace( boolean preserveGoogProvidesAndRequires, Map providedNames) { checkState( !this.isFromLegacyModule(), "Cannot rewrite provides without having rewritten goog.modules, found %s", firstNode); if (firstNode == null) { // Don't touch the base case ('goog'). replacementNode = candidateDefinition; return; } // Handle the case where there is a duplicate definition for an explicitly // provided symbol. if (hasCandidateDefinition() && explicitNode != null) { // Does this need a VAR keyword? replacementNode = candidateDefinition; if (candidateDefinition.isExprResult()) { Node exprNode = candidateDefinition.getOnlyChild(); if (exprNode.isAssign()) { Node nameNode = exprNode.getFirstChild(); if (nameNode.isName()) { // In the case of a simple name, `name = value;`, we need to ensure the name is // actually declared with `var`. convertProvideAssignmentToVarDeclaration(exprNode, nameNode); } else { // `some.provided.namespace = value;` // We don't need to change the definition, but mark it as 'IS_NAMESPACE' so that // future passes know this was originally provided. candidateDefinition.putBooleanProp(Node.IS_NAMESPACE, true); } } } } else { // Handle the case where there's not an existing definition. if (!hasImplicitInitialization) { createNamespaceInitialization( createDeclarationNode(astFactory.createObjectLit()), providedNames); } } // Remove the `goog.provide('a.b.c');` call. if (explicitNode != null) { if (preserveGoogProvidesAndRequires) { return; } compiler.reportChangeToEnclosingScope(explicitNode); explicitNode.detach(); } } private void convertProvideAssignmentToVarDeclaration(Node assignNode, Node nameNode) { // Convert `providedName = value;` into `var providedName = value;`. checkArgument(assignNode.isAssign(), assignNode); checkArgument(nameNode.isName(), nameNode); Node valueNode = nameNode.getNext(); nameNode.detach(); valueNode.detach(); Node varNode = IR.var(nameNode, valueNode).srcref(candidateDefinition); varNode.setJSDocInfo(assignNode.getJSDocInfo()); varNode.putBooleanProp(Node.IS_NAMESPACE, true); candidateDefinition.replaceWith(varNode); replacementNode = varNode; compiler.reportChangeToEnclosingScope(varNode); } /** Adds an assignment or declaration to this namespace to the AST, using the provided value */ private void createNamespaceInitialization( Node replacement, Map providedNames) { replacementNode = replacement; if (firstChunk == minimumChunk) { replacementNode.insertBefore(firstNode); } else { // In this case, the name was implicitly provided by two independent // modules. We need to move this code up to a common module. int indexOfDot = namespace.lastIndexOf('.'); if (indexOfDot == -1) { // Any old place is fine. compiler.getNodeForCodeInsertion(minimumChunk).addChildToBack(replacementNode); } else { // Add it after the parent namespace. ProvidedName parentName = providedNames.get(namespace.substring(0, indexOfDot)); checkNotNull(parentName); checkNotNull(parentName.replacementNode); replacementNode.insertAfter(parentName.replacementNode); } } compiler.reportChangeToEnclosingScope(replacementNode); } /** * Create the declaration node for this name, without inserting it into the AST. * * @param value the object literal namespace, possibly in a CAST */ private Node createDeclarationNode(Node value) { checkArgument(value.isObjectLit() || value.isCast(), value); if (namespace.indexOf('.') == -1) { return makeVarDeclNode(value); } else { return makeAssignmentExprNode(value); } } /** Creates a simple namespace variable declaration (e.g. var foo = {};). */ private Node makeVarDeclNode(Node value) { Node name = IR.name(namespace); name.addChildToFront(value); Node decl = IR.var(name); decl.putBooleanProp(Node.IS_NAMESPACE, true); if (compiler.getCodingConvention().isConstant(namespace)) { name.putBooleanProp(Node.IS_CONSTANT_NAME, true); } if (!hasCandidateDefinition()) { decl.setJSDocInfo(NodeUtil.createConstantJsDoc()); } checkState(isNamespacePlaceholder(decl)); setSourceInfo(decl); return decl; } /** Creates a dotted namespace assignment expression (e.g. foo.bar = {};). */ private Node makeAssignmentExprNode(Node value) { // Note: as of May 2021, using the unknown type vs. the actual inferred type both produced the // same optimized JS after type-based optimizations. So the lack of type info is intentional. Node lhs = astFactory.createQNameWithUnknownType(namespace).srcrefTree(firstNode); Node decl = IR.exprResult(astFactory.createAssign(lhs, value)); decl.putBooleanProp(Node.IS_NAMESPACE, true); if (!hasCandidateDefinition()) { decl.getFirstChild().setJSDocInfo(NodeUtil.createConstantJsDoc()); } checkState(isNamespacePlaceholder(decl)); setSourceInfo(decl); // This function introduces artifical nodes and we don't need them for indexing. // Marking all but the last one as non-indexable. So if this function adds: // foo.bar.baz = {}; // then we mark foo and bar as non-indexable. lhs.getFirstChild().makeNonIndexableRecursive(); return decl; } /** Copy source info to the new node. */ private void setSourceInfo(Node newNode) { Node sourceInfoNode = firstNode; Node provideStringNode = getProvideStringNode(); if (provideStringNode != null) { // Given namespace "foo.bar.baz" we create node for "baz" here and need to calculate // length and start of the last component which is "baz". int firstCharIndex = namespace.lastIndexOf('.') + 1; // If no dots, then 0. sourceInfoNode = provideStringNode.cloneNode(); sourceInfoNode.setLinenoCharno( sourceInfoNode.getLineno(), sourceInfoNode.getCharno() + firstCharIndex + 1); // +1 for quote sourceInfoNode.setLength(namespace.length() - firstCharIndex); } newNode.srcrefTree(sourceInfoNode); } private @Nullable Node getProvideStringNode() { return (firstNode.hasChildren() && NodeUtil.isExprCall(firstNode)) ? firstNode.getFirstChild().getLastChild() : null; } @Override @GwtIncompatible("Unnecessary") // This is just for debugging in an IDE. public String toString() { String explicitOrImplicit = isExplicitlyProvided() ? "explicit" : "implicit"; return String.format("ProvidedName: %s, %s", namespace, explicitOrImplicit); } } /** * Returns whether the node initializes a goog.provide'd namespace (e.g. `a.b = {};`) with a * simple namespace object literal (e.g. not `a.b = class {}`;) */ private static boolean isNamespacePlaceholder(Node n) { if (!n.getBooleanProp(Node.IS_NAMESPACE)) { return false; } Node value = null; if (n.isExprResult()) { Node assign = n.getFirstChild(); value = assign.getLastChild(); } else if (n.isVar()) { Node name = n.getFirstChild(); value = name.getFirstChild(); } if (value == null) { return false; } if (value.isCast()) { // There may be a cast to unknown type wrapped around the value. value = value.getOnlyChild(); } return value.isObjectLit() && !value.hasChildren(); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy