com.google.javascript.jscomp.ProcessClosureProvidesAndRequires Maven / Gradle / Ivy
Show all versions of com.liferay.frontend.js.minifier
/*
* 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;
/**
* 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(Node node) {
this.node = node;
return this;
}
ProvidedNameBuilder setChunk(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 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 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 JSChunk minimumChunk = null;
// The replacement declaration. Null until replace() has been called.
private 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 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();
}
}