From 2ff09bb466234be7d330b542f90dffcc33350d27 Mon Sep 17 00:00:00 2001 From: lharker Date: Fri, 8 Mar 2019 15:23:36 -0800 Subject: [PATCH] Split out provide/require handling from ProcessClosurePrimitives Motivations: - We soon will move provide/require rewriting after typechecking, but don't care about the remaining primitives and will keep this before typechecking. - This pass is huge. - This potentially makes it easier to share goog.provide logic with other parts of the compiler. Eventually this should also be its own pass, but for now continues to run as part of ProcessClosurePrimitives. We also plan to remove the require rewriting eventually, and consolidate rewriting for requires in ES modules/goog.modules/goog.provide files/scripts. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=237530052 --- .../jscomp/PreprocessorSymbolTable.java | 35 + .../jscomp/ProcessClosurePrimitives.java | 746 +-------------- .../ProcessClosureProvidesAndRequires.java | 898 ++++++++++++++++++ 3 files changed, 952 insertions(+), 727 deletions(-) create mode 100644 src/com/google/javascript/jscomp/ProcessClosureProvidesAndRequires.java diff --git a/src/com/google/javascript/jscomp/PreprocessorSymbolTable.java b/src/com/google/javascript/jscomp/PreprocessorSymbolTable.java index 458804fac00..6cc8f24b6c2 100644 --- a/src/com/google/javascript/jscomp/PreprocessorSymbolTable.java +++ b/src/com/google/javascript/jscomp/PreprocessorSymbolTable.java @@ -156,4 +156,39 @@ public PreprocessorSymbolTable getInstanceOrNull() { return instance; } } + + /** + * Adds a synthetic reference for a 'string' node representing a reference name. + * + *

This does some work to set the source info for the reference as well. + */ + void addStringNode(Node n, AbstractCompiler compiler) { + String name = n.getString(); + Node syntheticRef = + NodeUtil.newQName( + compiler, name, n /* real source offsets will be filled in below */, name); + + // Offsets to add to source. Named for documentation purposes. + final int forQuote = 1; + final int forDot = 1; + + Node current = null; + for (current = syntheticRef; current.isGetProp(); current = current.getFirstChild()) { + int fullLen = current.getQualifiedName().length(); + int namespaceLen = current.getFirstChild().getQualifiedName().length(); + + current.setSourceEncodedPosition(n.getSourcePosition() + forQuote); + current.setLength(fullLen); + + current + .getLastChild() + .setSourceEncodedPosition(n.getSourcePosition() + namespaceLen + forQuote + forDot); + current.getLastChild().setLength(current.getLastChild().getString().length()); + } + + current.setSourceEncodedPosition(n.getSourcePosition() + forQuote); + current.setLength(current.getString().length()); + + addReference(syntheticRef); + } } diff --git a/src/com/google/javascript/jscomp/ProcessClosurePrimitives.java b/src/com/google/javascript/jscomp/ProcessClosurePrimitives.java index 6a38c3d5d4c..59362216a80 100644 --- a/src/com/google/javascript/jscomp/ProcessClosurePrimitives.java +++ b/src/com/google/javascript/jscomp/ProcessClosurePrimitives.java @@ -16,26 +16,19 @@ 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 static com.google.javascript.jscomp.ClosurePrimitiveErrors.INVALID_CLOSURE_CALL_SCOPE_ERROR; -import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback; -import com.google.javascript.jscomp.parsing.JsDocInfoParser; import com.google.javascript.rhino.IR; import com.google.javascript.rhino.JSDocInfo; -import com.google.javascript.rhino.JSDocInfoBuilder; -import com.google.javascript.rhino.JSTypeExpression; import com.google.javascript.rhino.Node; import com.google.javascript.rhino.Token; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; @@ -169,21 +162,14 @@ class ProcessClosurePrimitives extends AbstractPostOrderCallback implements HotS static final String GOOG = "goog"; private final AbstractCompiler compiler; - private final JSModuleGraph moduleGraph; - - // The goog.provides must be processed in a deterministic order. - private final Map providedNames = new LinkedHashMap<>(); private final Set knownClosureSubclasses = new HashSet<>(); - private final List unrecognizedRequires = new ArrayList<>(); private final Set exportedVariables = new HashSet<>(); private final CheckLevel requiresLevel; private final PreprocessorSymbolTable preprocessorSymbolTable; private final List defineCalls = new ArrayList<>(); private final boolean preserveGoogProvidesAndRequires; - private final List requiresToBeRemoved = new ArrayList<>(); - private final Set maybeTemporarilyLiveNodes = new HashSet<>(); ProcessClosurePrimitives(AbstractCompiler compiler, @Nullable PreprocessorSymbolTable preprocessorSymbolTable, @@ -191,13 +177,8 @@ class ProcessClosurePrimitives extends AbstractPostOrderCallback implements HotS boolean preserveGoogProvidesAndRequires) { this.compiler = compiler; this.preprocessorSymbolTable = preprocessorSymbolTable; - this.moduleGraph = compiler.getModuleGraph(); this.requiresLevel = requiresLevel; this.preserveGoogProvidesAndRequires = preserveGoogProvidesAndRequires; - - // goog is special-cased because it is provided in Closure's base library. - providedNames.put(GOOG, - new ProvidedName(GOOG, null, null, false /* implicit */)); } Set getExportedVariableNames() { @@ -206,46 +187,19 @@ Set getExportedVariableNames() { @Override public void process(Node externs, Node root) { + // Replace goog.provide and goog.require + ProcessClosureProvidesAndRequires processor = + new ProcessClosureProvidesAndRequires( + compiler, preprocessorSymbolTable, requiresLevel, preserveGoogProvidesAndRequires); + + processor.rewriteProvidesAndRequires(externs, root); + + // Replace and validate other Closure primitives NodeTraversal.traverseRoots(compiler, this, externs, root); for (Node n : defineCalls) { replaceGoogDefines(n); } - - for (ProvidedName pn : providedNames.values()) { - pn.replace(); - } - - if (requiresLevel.isOn()) { - for (UnrecognizedRequire r : unrecognizedRequires) { - checkForLateOrMissingProvide(r); - } - } - - for (Node closureRequire : requiresToBeRemoved) { - compiler.reportChangeToEnclosingScope(closureRequire); - closureRequire.detach(); - } - for (Node liveNode : maybeTemporarilyLiveNodes) { - compiler.reportChangeToEnclosingScope(liveNode); - } - } - - private void checkForLateOrMissingProvide(UnrecognizedRequire r) { - // Both goog.require and goog.requireType must have a matching goog.provide. - // However, goog.require must match an earlier goog.provide, while goog.requireType is allowed - // to match a later goog.provide. - DiagnosticType error; - ProvidedName expectedName = providedNames.get(r.namespace); - if (expectedName != null && expectedName.firstNode != null) { - if (r.isRequireType) { - return; - } - error = LATE_PROVIDE_ERROR; - } else { - error = MISSING_PROVIDE_ERROR; - } - compiler.report(JSError.make(r.requireNode, requiresLevel, error, r.namespace)); } /** @@ -306,17 +260,6 @@ public void visit(NodeTraversal t, Node n, Node parent) { case "define": processDefineCall(t, n, parent); break; - case "require": - case "requireType": - if (validateAliasiablePrimitiveCall(t, n, methodName)) { - processRequireCall(t, n, parent); - } - break; - case "provide": - if (validateUnaliasablePrimitiveCall(t, n, methodName)) { - processProvideCall(t, n, parent); - } - break; case "inherits": // Note: inherits is allowed in local scope processInheritsCall(n); @@ -334,11 +277,6 @@ public void visit(NodeTraversal t, Node n, Node parent) { } } break; - case "forwardDeclare": - if (validateAliasiablePrimitiveCall(t, n, methodName)) { - processForwardDeclare(n, parent); - } - break; case "addDependency": if (validateUnaliasablePrimitiveCall(t, n, methodName)) { processAddDependency(n, parent); @@ -360,36 +298,6 @@ public void visit(NodeTraversal t, Node n, Node parent) { case NAME: if (n.isName() && n.getString().equals("CLOSURE_DEFINES")) { handleClosureDefinesValues(n); - } else { - // 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; - - case CLASS: - if (t.inGlobalHoistScope() && !NodeUtil.isClassExpression(n)) { - String name = n.getFirstChild().getString(); - ProvidedName pn = providedNames.get(name); - if (pn != null) { - compiler.report(JSError.make(n, CLASS_NAMESPACE_ERROR, name)); - } - } - break; - - case FUNCTION: - // If this is a declaration of a provided named function, this is an - // error. Hoisted functions will explode if they're provided. - if (t.inGlobalHoistScope() && NodeUtil.isFunctionDeclaration(n)) { - String name = n.getFirstChild().getString(); - ProvidedName pn = providedNames.get(name); - if (pn != null) { - compiler.report(JSError.make(n, FUNCTION_NAMESPACE_ERROR, name)); - } } break; @@ -416,17 +324,6 @@ private boolean validateUnaliasablePrimitiveCall(NodeTraversal t, Node n, String return validatePrimitiveCallWithMessage(t, n, methodName, CLOSURE_CALL_CANNOT_BE_ALIASED_ERROR); } - /** - * Verifies that a) the call is in the global scope and b) the return value is unused - * - *

This method is for primitives that do return a value in modules, but not in scripts/ - * goog.provide files - */ - private boolean validateAliasiablePrimitiveCall(NodeTraversal t, Node n, String methodName) { - return validatePrimitiveCallWithMessage( - t, n, methodName, CLOSURE_CALL_CANNOT_BE_ALIASED_OUTSIDE_MODULE_ERROR); - } - /** * @param methodName list of primitve types classed together with this one * @param invalidAliasingError which DiagnosticType to emit if this call is aliased. this depends @@ -484,153 +381,21 @@ static boolean isValidDefineValue(Node val) { } } - /** Handles a goog.require or goog.requireType call. */ - private void processRequireCall(NodeTraversal t, Node n, Node parent) { - Node left = n.getFirstChild(); - Node arg = left.getNext(); - String method = left.getFirstChild().getNext().getString(); - if (verifyLastArgumentIsString(left, arg)) { - String ns = arg.getString(); - ProvidedName provided = providedNames.get(ns); - if (provided == null || !provided.isExplicitlyProvided()) { - unrecognizedRequires.add(new UnrecognizedRequire(n, ns, method.equals("requireType"))); - } else { - JSModule providedModule = provided.explicitModule; - - if (!provided.isFromExterns()) { - // TODO(tbreisacher): Report an error if there's a goog.provide in an @externs file. - checkNotNull(providedModule, n); - - JSModule module = t.getModule(); - // A cross-chunk goog.require must match a goog.provide in an earlier chunk. However, a - // cross-chunk goog.requireType is allowed to match a goog.provide in a later chunk. - if (module != providedModule - && !moduleGraph.dependsOn(module, providedModule) - && !method.equals("requireType")) { - compiler.report( - JSError.make( - n, XMODULE_REQUIRE_ERROR, ns, providedModule.getName(), module.getName())); - } - } - } - - maybeAddToSymbolTable(left); - maybeAddStringNodeToSymbolTable(arg); - - // Requires should be removed before further processing. - // Some clients run closure pass multiple times, first with - // the checks for broken requires turned off. In these cases, we - // allow broken requires to be preserved by the first run to - // let them be caught in the subsequent run. - if (!preserveGoogProvidesAndRequires && (provided != null || requiresLevel.isOn())) { - requiresToBeRemoved.add(parent); - } - } - } - - /** - * Handles a goog.provide call. - */ - private void processProvideCall(NodeTraversal t, Node n, Node parent) { - checkState(n.isCall()); - Node left = n.getFirstChild(); - Node arg = left.getNext(); - if (verifyProvide(left, arg)) { - String ns = arg.getString(); - - maybeAddToSymbolTable(left); - maybeAddStringNodeToSymbolTable(arg); - - if (providedNames.containsKey(ns)) { - ProvidedName previouslyProvided = providedNames.get(ns); - if (!previouslyProvided.isExplicitlyProvided()) { - previouslyProvided.addProvide(parent, t.getModule(), true); - } else { - String explicitSourceName = previouslyProvided.explicitNode.getSourceFileName(); - compiler.report(JSError.make(n, DUPLICATE_NAMESPACE_ERROR, ns, explicitSourceName)); - } - } else { - registerAnyProvidedPrefixes(ns, parent, t.getModule()); - providedNames.put( - ns, new ProvidedName(ns, parent, t.getModule(), true)); - } - } - } - - /** - * Handles a goog.define call. - */ + /** Handles a goog.define call. */ private void processDefineCall(NodeTraversal t, Node n, Node parent) { Node left = n.getFirstChild(); Node args = left.getNext(); if (verifyDefine(t, parent, left, args)) { Node nameNode = args; - maybeAddToSymbolTable(left); - maybeAddStringNodeToSymbolTable(nameNode); + maybeAddNameToSymbolTable(left); + maybeAddStringToSymbolTable(nameNode); this.defineCalls.add(n); } } - /** - * Handles a stub definition for a goog.provided name - * (e.g. a @typedef or a definition from externs) - * - * @param n EXPR_RESULT node. - */ - private void handleStubDefinition(NodeTraversal t, Node n) { - if (!t.inGlobalHoistScope()) { - return; - } - JSDocInfo info = n.getFirstChild().getJSDocInfo(); - boolean hasStubDefinition = info != null && (n.isFromExterns() || info.hasTypedefType()); - if (hasStubDefinition) { - if (n.getFirstChild().isQualifiedName()) { - String name = n.getFirstChild().getQualifiedName(); - ProvidedName pn = providedNames.get(name); - if (pn != null) { - n.putBooleanProp(Node.WAS_PREVIOUSLY_PROVIDED, true); - pn.addDefinition(n, t.getModule()); - } else if (n.getBooleanProp(Node.WAS_PREVIOUSLY_PROVIDED)) { - // We didn't find it in the providedNames, but it was previously marked as provided. - // This implies we're in hotswap pass and the current typedef is a provided namespace. - ProvidedName provided = new ProvidedName(name, n, t.getModule(), true); - providedNames.put(name, provided); - } - } - } - } - - /** - * Handles a candidate definition for a goog.provided name. - */ - private void handleCandidateProvideDefinition( - NodeTraversal t, Node n, Node parent) { - if (t.inGlobalHoistScope()) { - String name = null; - if (n.isName() && NodeUtil.isNameDeclaration(parent)) { - name = n.getString(); - } else if (n.isAssign() && parent.isExprResult()) { - name = n.getFirstChild().getQualifiedName(); - } - - if (name != null) { - if (parent.getBooleanProp(Node.IS_NAMESPACE)) { - processProvideFromPreviousPass(t, name, parent); - } else { - ProvidedName pn = providedNames.get(name); - if (pn != null) { - pn.addDefinition(parent, t.getModule()); - } - } - } - } - } - - /** - * Processes the base class call. - */ + /** Processes the base class call. */ private void processBaseClassCall(NodeTraversal t, Node n) { // Two things must hold for every goog.base call: // 1) We must be calling it on "this". @@ -962,48 +727,6 @@ private void reportBadClosureCommonDefinesDefinition(Node n) { compiler.report(JSError.make(n, CLOSURE_DEFINES_ERROR)); } - /** - * Processes the output of processed-provide from a previous pass. This will - * update our data structures in the same manner as if the provide had been - * processed in this pass. - */ - private void processProvideFromPreviousPass( - NodeTraversal t, String name, Node parent) { - if (!providedNames.containsKey(name)) { - // Record this provide created on a previous pass, and create a dummy - // EXPR node as a placeholder to simulate an explicit provide. - Node expr = new Node(Token.EXPR_RESULT); - expr.useSourceInfoIfMissingFromForTree(parent); - parent.getParent().addChildBefore(expr, parent); - /** - * 'expr' has been newly added to the AST, but it might be removed again before this pass - * finishes. Keep it in a list for later change reporting if it doesn't get removed again - * before the end of the pass. - */ - maybeTemporarilyLiveNodes.add(expr); - - JSModule module = t.getModule(); - registerAnyProvidedPrefixes(name, expr, module); - - // If registerAnyProvidedPrefixes didn't add any children, add a no-op child so that - // the AST is valid. - if (!expr.hasChildren()) { - expr.addChildToBack(NodeUtil.newUndefinedNode(parent)); - } - - ProvidedName provided = new ProvidedName(name, expr, module, true); - providedNames.put(name, provided); - provided.addDefinition(parent, module); - } else { - // Remove this provide if it came from a previous pass since we have an - // replacement already. - if (isNamespacePlaceholder(parent)) { - compiler.reportChangeToEnclosingScope(parent); - parent.detach(); - } - } - } - /** * Processes a call to goog.setCssNameMapping(). Either the argument to goog.setCssNameMapping() * is valid, in which case it will be used to create a CssRenamingMap for the compiler of this @@ -1291,448 +1014,17 @@ private boolean verifySetCssNameMapping(Node methodName, Node firstArg) { return true; } - /** - * 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, JSModule 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, false /* implicit */); - } else { - providedNames.put( - prefixNs, - new ProvidedName(prefixNs, node, module, false /* implicit */)); - } - } - } - - // ------------------------------------------------------------------------- - - /** - * Information required to replace a goog.provide call later in the traversal. - */ - private class ProvidedName { - private final String namespace; - - // The node and module where the call was explicitly or implicitly - // goog.provided. - private final Node firstNode; - private final JSModule firstModule; - - // The node where the call was explicitly goog.provided. May be null - // if the namespace is always provided implicitly. - private Node explicitNode = null; - private JSModule explicitModule = null; - - // There are child namespaces of this one. - private boolean hasAChildNamespace = false; - - // The candidate definition. - private Node candidateDefinition = null; - - // The minimum module where the provide must appear. - private JSModule minimumModule = null; - - // The replacement declaration. - private Node replacementNode = null; - - ProvidedName(String namespace, Node node, JSModule module, - boolean explicit) { - Preconditions.checkArgument(node == null /* The base case */ || node.isExprResult()); - this.namespace = namespace; - this.firstNode = node; - this.firstModule = module; - - addProvide(node, module, explicit); - } - - /** - * Add an implicit or explicit provide. - */ - void addProvide(Node node, JSModule module, boolean explicit) { - if (explicit) { - // goog.provide('name.space'); - checkState(explicitNode == null); - checkArgument(node.isExprResult()); - explicitNode = node; - explicitModule = module; - } else { - // goog.provide('name.space.some.child'); - hasAChildNamespace = true; - } - updateMinimumModule(module); - } - - boolean isExplicitlyProvided() { - return explicitNode != null; - } - - boolean isFromExterns() { - return explicitNode.isFromExterns(); - } - - /** - * Record function declaration, variable declaration or assignment that - * refers to the same name as the provide statement. Give preference to - * declarations; if no declaration exists, record a reference to an - * assignment so it repurposed later. - */ - void addDefinition(Node node, JSModule module) { - Preconditions.checkArgument( - node.isExprResult() // assign - || node.isFunction() - || NodeUtil.isNameDeclaration(node)); - checkArgument(explicitNode != node); - if ((candidateDefinition == null) || !node.isExprResult()) { - candidateDefinition = node; - updateMinimumModule(module); - } - } - - private void updateMinimumModule(JSModule newModule) { - if (minimumModule == null) { - minimumModule = newModule; - } else if (moduleGraph.getModuleCount() > 1) { - minimumModule = moduleGraph.getDeepestCommonDependencyInclusive( - minimumModule, newModule); - } else { - // If there is no module graph, then there must be exactly one - // module in the program. - checkState(newModule == minimumModule, "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. - */ - void replace() { - 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 (candidateDefinition != null && explicitNode != null) { - JSDocInfo info; - if (candidateDefinition.isExprResult()) { - info = candidateDefinition.getFirstChild().getJSDocInfo(); - } else { - info = candidateDefinition.getJSDocInfo(); - } - - // Validate that the namespace is not declared as a generic object type. - if (info != null) { - JSTypeExpression expr = info.getType(); - if (expr != null) { - Node n = expr.getRoot(); - if (n.getToken() == Token.BANG) { - n = n.getFirstChild(); - } - if (n.isString() - && !n.hasChildren() // templated object types are ok. - && n.getString().equals("Object")) { - compiler.report( - JSError.make(candidateDefinition, WEAK_NAMESPACE_TYPE)); - } - } - } - - // Does this need a VAR keyword? - replacementNode = candidateDefinition; - if (candidateDefinition.isExprResult()) { - Node exprNode = candidateDefinition.getOnlyChild(); - if (exprNode.isAssign()) { - // namespace = value; - candidateDefinition.putBooleanProp(Node.IS_NAMESPACE, true); - Node nameNode = exprNode.getFirstChild(); - if (nameNode.isName()) { - // Need to convert this assign to a var declaration. - Node valueNode = nameNode.getNext(); - exprNode.removeChild(nameNode); - exprNode.removeChild(valueNode); - nameNode.addChildToFront(valueNode); - Node varNode = IR.var(nameNode); - varNode.useSourceInfoFrom(candidateDefinition); - candidateDefinition.replaceWith(varNode); - varNode.setJSDocInfo(exprNode.getJSDocInfo()); - compiler.reportChangeToEnclosingScope(varNode); - replacementNode = varNode; - } - } else { - // /** @typedef {something} */ name.space.Type; - checkState(exprNode.isQualifiedName(), exprNode); - // If this namespace has child namespaces, we still need to add an object to hang them - // on to avoid creating broken code. - // We must cast the type of the literal to unknown, because the type checker doesn't - // expect the namespace to have a value. - if (hasAChildNamespace) { - replaceWith(createDeclarationNode( - IR.cast(IR.objectlit(), createUnknownTypeJsDocInfo()))); - } - } - } - } else { - // Handle the case where there's not a duplicate definition. - replaceWith(createDeclarationNode(IR.objectlit())); - } - if (explicitNode != null) { - if (preserveGoogProvidesAndRequires && explicitNode.hasChildren()) { - return; - } - /* - * If 'explicitNode' was added earlier in this pass then don't bother to report its removal - * right here as a change (since the original AST state is being restored). Also remove - * 'explicitNode' from the list of "possibly live" nodes so that it does not get reported as - * a change at the end of the pass. - */ - if (!maybeTemporarilyLiveNodes.remove(explicitNode)) { - compiler.reportChangeToEnclosingScope(explicitNode); - } - explicitNode.detach(); - } - } - - private void replaceWith(Node replacement) { - replacementNode = replacement; - if (firstModule == minimumModule) { - firstNode.getParent().addChildBefore(replacementNode, 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(minimumModule) - .addChildToBack(replacementNode); - } else { - // Add it after the parent namespace. - ProvidedName parentName = - providedNames.get(namespace.substring(0, indexOfDot)); - checkNotNull(parentName); - checkNotNull(parentName.replacementNode); - parentName.replacementNode.getParent().addChildAfter( - replacementNode, parentName.replacementNode); - } - } - compiler.reportChangeToEnclosingScope(replacementNode); - } - - /** - * Create the declaration node for this name, without inserting it - * into the AST. - */ - private Node createDeclarationNode(Node 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 (candidateDefinition == null) { - 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) { - Node lhs = - NodeUtil.newQName( - compiler, - namespace, - firstNode /* real source info will be filled in below */, - namespace); - Node decl = IR.exprResult(IR.assign(lhs, value)); - decl.putBooleanProp(Node.IS_NAMESPACE, true); - if (candidateDefinition == null) { - 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 provideStringNode = getProvideStringNode(); - int offset = provideStringNode == null ? 0 : getSourceInfoOffset(); - Node sourceInfoNode = provideStringNode == null ? firstNode : provideStringNode; - newNode.useSourceInfoIfMissingFromForTree(sourceInfoNode); - if (offset != 0) { - newNode.setSourceEncodedPositionForTree( - sourceInfoNode.getSourcePosition() + offset); - // Given namespace "foo.bar.baz" we create node for "baz" here and need to calculate - // length of the last component which is "baz". - int lengthOfLastComponent = namespace.length() - (namespace.lastIndexOf(".") + 1); - newNode.setLengthForTree(lengthOfLastComponent); - } - } - - /** - * Get the offset into the provide node where the symbol appears. - */ - private int getSourceInfoOffset() { - int indexOfLastDot = namespace.lastIndexOf('.'); - - // +1 for the opening quote - // +1 for the dot - // if there's no dot, then the -1 index cancels it out - // so elegant! - return 2 + indexOfLastDot; - } - - private Node getProvideStringNode() { - return (firstNode.getFirstChild() != null && NodeUtil.isExprCall(firstNode)) - ? firstNode.getFirstChild().getLastChild() - : null; - } - } - - private JSDocInfo createUnknownTypeJsDocInfo() { - JSDocInfoBuilder castToUnknownBuilder = new JSDocInfoBuilder(true); - castToUnknownBuilder.recordType( - new JSTypeExpression( - JsDocInfoParser.parseTypeString("?"), "")); - return castToUnknownBuilder.build(); - } - - /** - * @return Whether the node is namespace placeholder. - */ - 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(); - } - - /** - * The string in {@code n} is a reference name. Create a synthetic - * node for it with all the proper source info, and add it to the symbol - * table. - */ - private void maybeAddStringNodeToSymbolTable(Node n) { - if (preprocessorSymbolTable == null) { - return; - } - - String name = n.getString(); - Node syntheticRef = NodeUtil.newQName( - compiler, name, - n /* real source offsets will be filled in below */, - name); - - // Offsets to add to source. Named for documentation purposes. - final int forQuote = 1; - final int forDot = 1; - - Node current = null; - for (current = syntheticRef; - current.isGetProp(); - current = current.getFirstChild()) { - int fullLen = current.getQualifiedName().length(); - int namespaceLen = current.getFirstChild().getQualifiedName().length(); - - current.setSourceEncodedPosition(n.getSourcePosition() + forQuote); - current.setLength(fullLen); - - current.getLastChild().setSourceEncodedPosition( - n.getSourcePosition() + namespaceLen + forQuote + forDot); - current.getLastChild().setLength( - current.getLastChild().getString().length()); - } - - current.setSourceEncodedPosition(n.getSourcePosition() + forQuote); - current.setLength(current.getString().length()); - - maybeAddToSymbolTable(syntheticRef); - } - - /** - * Add the given qualified name node to the symbol table. - */ - private void maybeAddToSymbolTable(Node n) { + /** Add the given qualified name node to the symbol table. */ + private void maybeAddStringToSymbolTable(Node string) { if (preprocessorSymbolTable != null) { - preprocessorSymbolTable.addReference(n); + preprocessorSymbolTable.addStringNode(string, compiler); } } - // ------------------------------------------------------------------------- - - /** - * Information required to create a {@code MISSING_PROVIDE_ERROR} warning. - */ - private static class UnrecognizedRequire { - final Node requireNode; - final String namespace; - final boolean isRequireType; - - UnrecognizedRequire(Node requireNode, String namespace, boolean isRequireType) { - this.requireNode = requireNode; - this.namespace = namespace; - this.isRequireType = isRequireType; + /** Add the given qualified name node to the symbol table. */ + private void maybeAddNameToSymbolTable(Node name) { + if (preprocessorSymbolTable != null) { + preprocessorSymbolTable.addReference(name); } } } diff --git a/src/com/google/javascript/jscomp/ProcessClosureProvidesAndRequires.java b/src/com/google/javascript/jscomp/ProcessClosureProvidesAndRequires.java new file mode 100644 index 00000000000..a84dbe6f544 --- /dev/null +++ b/src/com/google/javascript/jscomp/ProcessClosureProvidesAndRequires.java @@ -0,0 +1,898 @@ +/* + * Copyright 2006 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 static com.google.javascript.jscomp.ClosurePrimitiveErrors.INVALID_CLOSURE_CALL_SCOPE_ERROR; + +import com.google.common.base.Preconditions; +import com.google.common.collect.Iterables; +import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback; +import com.google.javascript.jscomp.parsing.JsDocInfoParser; +import com.google.javascript.rhino.IR; +import com.google.javascript.rhino.JSDocInfo; +import com.google.javascript.rhino.JSDocInfoBuilder; +import com.google.javascript.rhino.JSTypeExpression; +import com.google.javascript.rhino.Node; +import com.google.javascript.rhino.Token; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Set; +import javax.annotation.Nullable; + +/** + * Replaces goog.provide calls, removes goog.{require,requireType} calls, verifies that each + * goog.{require,requireType} has a corresponding goog.provide, and performs some Closure-pecific + * simplifications. + * + * @author chrisn@google.com (Chris Nokleberg) + */ +class ProcessClosureProvidesAndRequires { + + /** The root Closure namespace */ + static final String GOOG = "goog"; + + private final AbstractCompiler compiler; + private final JSModuleGraph moduleGraph; + + // The goog.provides must be processed in a deterministic order. + private final Map providedNames = new LinkedHashMap<>(); + + private final List unrecognizedRequires = new ArrayList<>(); + private final CheckLevel requiresLevel; + private final PreprocessorSymbolTable preprocessorSymbolTable; + // 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<>(); + // Set of nodes to report changed at the end of rewriting + private final Set maybeTemporarilyLiveNodes = new HashSet<>(); + private boolean hasRewritingOccurred = false; + + ProcessClosureProvidesAndRequires( + AbstractCompiler compiler, + @Nullable PreprocessorSymbolTable preprocessorSymbolTable, + CheckLevel requiresLevel, + boolean preserveGoogProvidesAndRequires) { + this.compiler = compiler; + this.preprocessorSymbolTable = preprocessorSymbolTable; + this.moduleGraph = compiler.getModuleGraph(); + this.requiresLevel = requiresLevel; + this.preserveGoogProvidesAndRequires = preserveGoogProvidesAndRequires; + + // goog is special-cased because it is provided in Closure's base library. + providedNames.put(GOOG, new ProvidedName(GOOG, null, null, false /* implicit */)); + } + + /** Collects all goog.provides and goog.require namespace */ + void rewriteProvidesAndRequires(Node externs, Node root) { + checkState(!hasRewritingOccurred, "Cannot call rewriteProvidesAndRequires twice per instance"); + hasRewritingOccurred = true; + + // TODO(b/124920011): split this into one method to collect provides/requires and a second + // method to rewrite them. + NodeTraversal.traverseRoots(compiler, new CollectDefinitions(), externs, root); + + for (ProvidedName pn : providedNames.values()) { + pn.replace(); + } + + if (requiresLevel.isOn()) { + for (UnrecognizedRequire r : unrecognizedRequires) { + checkForLateOrMissingProvide(r); + } + } + + for (Node closureRequire : requiresToBeRemoved) { + compiler.reportChangeToEnclosingScope(closureRequire); + closureRequire.detach(); + } + for (Node liveNode : maybeTemporarilyLiveNodes) { + compiler.reportChangeToEnclosingScope(liveNode); + } + } + + private void checkForLateOrMissingProvide(UnrecognizedRequire r) { + // Both goog.require and goog.requireType must have a matching goog.provide. + // However, goog.require must match an earlier goog.provide, while goog.requireType is allowed + // to match a later goog.provide. + DiagnosticType error; + ProvidedName expectedName = providedNames.get(r.namespace); + if (expectedName != null && expectedName.firstNode != null) { + if (r.isRequireType) { + return; + } + error = ProcessClosurePrimitives.LATE_PROVIDE_ERROR; + } else { + error = ProcessClosurePrimitives.MISSING_PROVIDE_ERROR; + } + compiler.report(JSError.make(r.requireNode, requiresLevel, error, r.namespace)); + } + + private class CollectDefinitions extends AbstractPostOrderCallback { + @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. + String methodName = name.getNext().getString(); + switch (methodName) { + case "require": + case "requireType": + if (validateAliasiablePrimitiveCall(t, n, methodName)) { + processRequireCall(t, n, parent); + } + break; + case "provide": + if (validateUnaliasablePrimitiveCall(t, n, methodName)) { + processProvideCall(t, n, parent); + } + break; + case "forwardDeclare": + if (validateAliasiablePrimitiveCall(t, n, methodName)) { + 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; + + case CLASS: + if (t.inGlobalHoistScope() && !NodeUtil.isClassExpression(n)) { + String name = n.getFirstChild().getString(); + ProvidedName pn = providedNames.get(name); + if (pn != null) { + compiler.report( + JSError.make(n, ProcessClosurePrimitives.CLASS_NAMESPACE_ERROR, name)); + } + } + break; + + case FUNCTION: + // If this is a declaration of a provided named function, this is an + // error. Hoisted functions will explode if they're provided. + if (t.inGlobalHoistScope() && NodeUtil.isFunctionDeclaration(n)) { + String name = n.getFirstChild().getString(); + ProvidedName pn = providedNames.get(name); + if (pn != null) { + compiler.report( + JSError.make(n, ProcessClosurePrimitives.FUNCTION_NAMESPACE_ERROR, name)); + } + } + break; + + default: + break; + } + } + } + + /** + * Verifies that a) the call is in the global scope and b) the return value is unused + * + *

This method is for primitives that never return a value. + */ + private boolean validateUnaliasablePrimitiveCall(NodeTraversal t, Node n, String methodName) { + return validatePrimitiveCallWithMessage( + t, n, methodName, ProcessClosurePrimitives.CLOSURE_CALL_CANNOT_BE_ALIASED_ERROR); + } + + /** + * Verifies that a) the call is in the global scope and b) the return value is unused + * + *

This method is for primitives that do return a value in modules, but not in scripts/ + * goog.provide files + */ + private boolean validateAliasiablePrimitiveCall(NodeTraversal t, Node n, String methodName) { + return validatePrimitiveCallWithMessage( + t, + n, + methodName, + ProcessClosurePrimitives.CLOSURE_CALL_CANNOT_BE_ALIASED_OUTSIDE_MODULE_ERROR); + } + + /** + * @param methodName list of primitve types classed together with this one + * @param invalidAliasingError which DiagnosticType to emit if this call is aliased. this depends + * on whether the primitive is sometimes aliasiable in a module or never aliasable. + */ + private boolean validatePrimitiveCallWithMessage( + NodeTraversal t, Node n, String methodName, DiagnosticType invalidAliasingError) { + // Ignore invalid primitives if we didn't strip module sugar. + if (compiler.getOptions().shouldPreserveGoogModule()) { + return true; + } + + if (!t.inGlobalHoistScope()) { + compiler.report(JSError.make(n, INVALID_CLOSURE_CALL_SCOPE_ERROR)); + return false; + } else if (!n.getParent().isExprResult()) { + // If the call is in the global hoist scope, but the result is used + compiler.report(JSError.make(n, invalidAliasingError, GOOG + "." + methodName)); + return false; + } + return true; + } + + /** Handles a goog.require or goog.requireType call. */ + private void processRequireCall(NodeTraversal t, Node n, Node parent) { + Node left = n.getFirstChild(); + Node arg = left.getNext(); + String method = left.getFirstChild().getNext().getString(); + if (verifyLastArgumentIsString(left, arg)) { + String ns = arg.getString(); + ProvidedName provided = providedNames.get(ns); + if (provided == null || !provided.isExplicitlyProvided()) { + unrecognizedRequires.add(new UnrecognizedRequire(n, ns, method.equals("requireType"))); + } else { + JSModule providedModule = provided.explicitModule; + + if (!provided.isFromExterns()) { + checkNotNull(providedModule, n); + + JSModule module = t.getModule(); + // A cross-chunk goog.require must match a goog.provide in an earlier chunk. However, a + // cross-chunk goog.requireType is allowed to match a goog.provide in a later chunk. + if (module != providedModule + && !moduleGraph.dependsOn(module, providedModule) + && !method.equals("requireType")) { + compiler.report( + JSError.make( + n, + ProcessClosurePrimitives.XMODULE_REQUIRE_ERROR, + ns, + providedModule.getName(), + module.getName())); + } + } + } + + maybeAddNameToSymbolTable(left); + maybeAddStringToSymbolTable(arg); + + // Requires should be removed before further processing. + // Some clients run closure pass multiple times, first with + // the checks for broken requires turned off. In these cases, we + // allow broken requires to be preserved by the first run to + // let them be caught in the subsequent run. + if (!preserveGoogProvidesAndRequires && (provided != null || requiresLevel.isOn())) { + requiresToBeRemoved.add(parent); + } + } + } + + /** Handles a goog.provide call. */ + private void processProvideCall(NodeTraversal t, Node n, Node parent) { + checkState(n.isCall()); + Node left = n.getFirstChild(); + Node arg = left.getNext(); + if (verifyProvide(left, arg)) { + String ns = arg.getString(); + + maybeAddNameToSymbolTable(left); + maybeAddStringToSymbolTable(arg); + + if (providedNames.containsKey(ns)) { + ProvidedName previouslyProvided = providedNames.get(ns); + if (!previouslyProvided.isExplicitlyProvided()) { + previouslyProvided.addProvide(parent, t.getModule(), true); + } else { + String explicitSourceName = previouslyProvided.explicitNode.getSourceFileName(); + compiler.report( + JSError.make( + n, ProcessClosurePrimitives.DUPLICATE_NAMESPACE_ERROR, ns, explicitSourceName)); + } + } else { + registerAnyProvidedPrefixes(ns, parent, t.getModule()); + providedNames.put(ns, new ProvidedName(ns, parent, t.getModule(), true)); + } + } + } + + /** + * Handles a stub definition for a goog.provided name (e.g. a @typedef or a definition from + * externs) + * + * @param n EXPR_RESULT node. + */ + private void handleStubDefinition(NodeTraversal t, Node n) { + if (!t.inGlobalHoistScope()) { + return; + } + JSDocInfo info = n.getFirstChild().getJSDocInfo(); + boolean hasStubDefinition = info != null && (n.isFromExterns() || info.hasTypedefType()); + if (hasStubDefinition) { + if (n.getFirstChild().isQualifiedName()) { + String name = n.getFirstChild().getQualifiedName(); + ProvidedName pn = providedNames.get(name); + if (pn != null) { + n.putBooleanProp(Node.WAS_PREVIOUSLY_PROVIDED, true); + pn.addDefinition(n, t.getModule()); + } else if (n.getBooleanProp(Node.WAS_PREVIOUSLY_PROVIDED)) { + // We didn't find it in the providedNames, but it was previously marked as provided. + // This implies we're in hotswap pass and the current typedef is a provided namespace. + ProvidedName provided = new ProvidedName(name, n, t.getModule(), true); + providedNames.put(name, provided); + } + } + } + } + + /** Handles a candidate definition for a goog.provided name. */ + private void handleCandidateProvideDefinition(NodeTraversal t, Node n, Node parent) { + if (t.inGlobalHoistScope()) { + String name = null; + if (n.isName() && NodeUtil.isNameDeclaration(parent)) { + name = n.getString(); + } else if (n.isAssign() && parent.isExprResult()) { + name = n.getFirstChild().getQualifiedName(); + } + + if (name != null) { + if (parent.getBooleanProp(Node.IS_NAMESPACE)) { + processProvideFromPreviousPass(t, name, parent); + } else { + ProvidedName pn = providedNames.get(name); + if (pn != null) { + pn.addDefinition(parent, t.getModule()); + } + } + } + } + } + + /** + * Processes the output of processed-provide from a previous pass. This will update our data + * structures in the same manner as if the provide had been processed in this pass. + */ + private void processProvideFromPreviousPass(NodeTraversal t, String name, Node parent) { + if (!providedNames.containsKey(name)) { + // Record this provide created on a previous pass, and create a dummy + // EXPR node as a placeholder to simulate an explicit provide. + Node expr = new Node(Token.EXPR_RESULT); + expr.useSourceInfoIfMissingFromForTree(parent); + parent.getParent().addChildBefore(expr, parent); + /** + * 'expr' has been newly added to the AST, but it might be removed again before this pass + * finishes. Keep it in a list for later change reporting if it doesn't get removed again + * before the end of the pass. + */ + maybeTemporarilyLiveNodes.add(expr); + + JSModule module = t.getModule(); + registerAnyProvidedPrefixes(name, expr, module); + + // If registerAnyProvidedPrefixes didn't add any children, add a no-op child so that + // the AST is valid. + if (!expr.hasChildren()) { + expr.addChildToBack(NodeUtil.newUndefinedNode(parent)); + } + + ProvidedName provided = new ProvidedName(name, expr, module, true); + providedNames.put(name, provided); + provided.addDefinition(parent, module); + } else { + // Remove this provide if it came from a previous pass since we have an + // replacement already. + if (isNamespacePlaceholder(parent)) { + compiler.reportChangeToEnclosingScope(parent); + parent.detach(); + } + } + } + + /** + * Verifies that a provide method call has exactly one argument, and that it's a string literal + * and that the contents of the string are valid JS tokens. Reports a compile error if it doesn't. + * + * @return Whether the argument checked out okay + */ + private boolean verifyProvide(Node methodName, Node arg) { + if (!verifyLastArgumentIsString(methodName, arg)) { + return false; + } + + if (!NodeUtil.isValidQualifiedName( + compiler.getOptions().getLanguageIn().toFeatureSet(), arg.getString())) { + compiler.report( + JSError.make( + arg, + ProcessClosurePrimitives.INVALID_PROVIDE_ERROR, + arg.getString(), + compiler.getOptions().getLanguageIn().toString())); + return false; + } + + return true; + } + + /** Process a goog.forwardDeclare() call and record the specified forward declaration. */ + private void processForwardDeclare(Node n, Node parent) { + CodingConvention convention = compiler.getCodingConvention(); + + String typeDeclaration = null; + try { + typeDeclaration = Iterables.getOnlyElement(convention.identifyTypeDeclarationCall(n)); + } catch (NullPointerException | NoSuchElementException | IllegalArgumentException e) { + compiler.report( + JSError.make( + n, + ProcessClosurePrimitives.INVALID_FORWARD_DECLARE, + "A single type could not identified for the goog.forwardDeclare statement")); + } + + if (typeDeclaration != null) { + compiler.forwardDeclareType(typeDeclaration); + // Forward declaration was recorded and we can remove the call. + Node toRemove = parent.isExprResult() ? parent : parent.getParent(); + NodeUtil.deleteNode(toRemove, compiler); + } + } + + /** + * Verifies that a method call has exactly one argument, and that it's a string literal. Reports a + * compile error if it doesn't. + * + * @return Whether the argument checked out okay + */ + private boolean verifyLastArgumentIsString(Node methodName, Node arg) { + return verifyNotNull(methodName, arg) + && verifyOfType(methodName, arg, Token.STRING) + && verifyIsLast(methodName, arg); + } + + /** @return Whether the argument checked out okay */ + private boolean verifyNotNull(Node methodName, Node arg) { + if (arg == null) { + compiler.report( + JSError.make( + methodName, + ProcessClosurePrimitives.NULL_ARGUMENT_ERROR, + methodName.getQualifiedName())); + return false; + } + return true; + } + + /** @return Whether the argument checked out okay */ + private boolean verifyOfType(Node methodName, Node arg, Token desiredType) { + if (arg.getToken() != desiredType) { + compiler.report( + JSError.make( + methodName, + ProcessClosurePrimitives.INVALID_ARGUMENT_ERROR, + methodName.getQualifiedName())); + return false; + } + return true; + } + + /** @return Whether the argument checked out okay */ + private boolean verifyIsLast(Node methodName, Node arg) { + if (arg.getNext() != null) { + compiler.report( + JSError.make( + methodName, + ProcessClosurePrimitives.TOO_MANY_ARGUMENTS_ERROR, + methodName.getQualifiedName())); + return false; + } + return true; + } + + /** + * 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, JSModule 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, false /* implicit */); + } else { + providedNames.put(prefixNs, new ProvidedName(prefixNs, node, module, false /* implicit */)); + } + } + } + + // ------------------------------------------------------------------------- + + /** Information required to replace a goog.provide call later in the traversal. */ + private class ProvidedName { + private final String namespace; + + // The node and module where the call was explicitly or implicitly + // goog.provided. + private final Node firstNode; + private final JSModule firstModule; + + // The node where the call was explicitly goog.provided. May be null + // if the namespace is always provided implicitly. + private Node explicitNode = null; + private JSModule explicitModule = null; + + // There are child namespaces of this one. + private boolean hasAChildNamespace = false; + + // The candidate definition. + private Node candidateDefinition = null; + + // The minimum module where the provide must appear. + private JSModule minimumModule = null; + + // The replacement declaration. + private Node replacementNode = null; + + ProvidedName(String namespace, Node node, JSModule module, boolean explicit) { + Preconditions.checkArgument(node == null /* The base case */ || node.isExprResult()); + this.namespace = namespace; + this.firstNode = node; + this.firstModule = module; + + addProvide(node, module, explicit); + } + + /** Add an implicit or explicit provide. */ + void addProvide(Node node, JSModule module, boolean explicit) { + if (explicit) { + // goog.provide('name.space'); + checkState(explicitNode == null); + checkArgument(node.isExprResult()); + explicitNode = node; + explicitModule = module; + } else { + // goog.provide('name.space.some.child'); + hasAChildNamespace = true; + } + updateMinimumModule(module); + } + + boolean isExplicitlyProvided() { + return explicitNode != null; + } + + boolean isFromExterns() { + return explicitNode.isFromExterns(); + } + + /** + * Record function declaration, variable declaration or assignment that refers to the same name + * as the provide statement. Give preference to declarations; if no declaration exists, record a + * reference to an assignment so it repurposed later. + */ + void addDefinition(Node node, JSModule module) { + Preconditions.checkArgument( + node.isExprResult() // assign + || node.isFunction() + || NodeUtil.isNameDeclaration(node)); + checkArgument(explicitNode != node); + if ((candidateDefinition == null) || !node.isExprResult()) { + candidateDefinition = node; + updateMinimumModule(module); + } + } + + private void updateMinimumModule(JSModule newModule) { + if (minimumModule == null) { + minimumModule = newModule; + } else if (moduleGraph.getModuleCount() > 1) { + minimumModule = moduleGraph.getDeepestCommonDependencyInclusive(minimumModule, newModule); + } else { + // If there is no module graph, then there must be exactly one + // module in the program. + checkState(newModule == minimumModule, "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. + */ + void replace() { + 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 (candidateDefinition != null && explicitNode != null) { + JSDocInfo info; + if (candidateDefinition.isExprResult()) { + info = candidateDefinition.getFirstChild().getJSDocInfo(); + } else { + info = candidateDefinition.getJSDocInfo(); + } + + // Validate that the namespace is not declared as a generic object type. + if (info != null) { + JSTypeExpression expr = info.getType(); + if (expr != null) { + Node n = expr.getRoot(); + if (n.getToken() == Token.BANG) { + n = n.getFirstChild(); + } + if (n.isString() + && !n.hasChildren() // templated object types are ok. + && n.getString().equals("Object")) { + compiler.report( + JSError.make(candidateDefinition, ProcessClosurePrimitives.WEAK_NAMESPACE_TYPE)); + } + } + } + + // Does this need a VAR keyword? + replacementNode = candidateDefinition; + if (candidateDefinition.isExprResult()) { + Node exprNode = candidateDefinition.getOnlyChild(); + if (exprNode.isAssign()) { + // namespace = value; + candidateDefinition.putBooleanProp(Node.IS_NAMESPACE, true); + Node nameNode = exprNode.getFirstChild(); + if (nameNode.isName()) { + // Need to convert this assign to a var declaration. + Node valueNode = nameNode.getNext(); + exprNode.removeChild(nameNode); + exprNode.removeChild(valueNode); + nameNode.addChildToFront(valueNode); + Node varNode = IR.var(nameNode); + varNode.useSourceInfoFrom(candidateDefinition); + candidateDefinition.replaceWith(varNode); + varNode.setJSDocInfo(exprNode.getJSDocInfo()); + compiler.reportChangeToEnclosingScope(varNode); + replacementNode = varNode; + } + } else { + // /** @typedef {something} */ name.space.Type; + checkState(exprNode.isQualifiedName(), exprNode); + // If this namespace has child namespaces, we still need to add an object to hang them + // on to avoid creating broken code. + // We must cast the type of the literal to unknown, because the type checker doesn't + // expect the namespace to have a value. + if (hasAChildNamespace) { + replaceWith( + createDeclarationNode(IR.cast(IR.objectlit(), createUnknownTypeJsDocInfo()))); + } + } + } + } else { + // Handle the case where there's not a duplicate definition. + replaceWith(createDeclarationNode(IR.objectlit())); + } + if (explicitNode != null) { + if (preserveGoogProvidesAndRequires && explicitNode.hasChildren()) { + return; + } + /* + * If 'explicitNode' was added earlier in this pass then don't bother to report its removal + * right here as a change (since the original AST state is being restored). Also remove + * 'explicitNode' from the list of "possibly live" nodes so that it does not get reported as + * a change at the end of the pass. + */ + if (!maybeTemporarilyLiveNodes.remove(explicitNode)) { + compiler.reportChangeToEnclosingScope(explicitNode); + } + explicitNode.detach(); + } + } + + private void replaceWith(Node replacement) { + replacementNode = replacement; + if (firstModule == minimumModule) { + firstNode.getParent().addChildBefore(replacementNode, 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(minimumModule).addChildToBack(replacementNode); + } else { + // Add it after the parent namespace. + ProvidedName parentName = providedNames.get(namespace.substring(0, indexOfDot)); + checkNotNull(parentName); + checkNotNull(parentName.replacementNode); + parentName + .replacementNode + .getParent() + .addChildAfter(replacementNode, parentName.replacementNode); + } + } + compiler.reportChangeToEnclosingScope(replacementNode); + } + + /** Create the declaration node for this name, without inserting it into the AST. */ + private Node createDeclarationNode(Node 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 (candidateDefinition == null) { + 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) { + Node lhs = + NodeUtil.newQName( + compiler, + namespace, + firstNode /* real source info will be filled in below */, + namespace); + Node decl = IR.exprResult(IR.assign(lhs, value)); + decl.putBooleanProp(Node.IS_NAMESPACE, true); + if (candidateDefinition == null) { + 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 provideStringNode = getProvideStringNode(); + int offset = provideStringNode == null ? 0 : getSourceInfoOffset(); + Node sourceInfoNode = provideStringNode == null ? firstNode : provideStringNode; + newNode.useSourceInfoIfMissingFromForTree(sourceInfoNode); + if (offset != 0) { + newNode.setSourceEncodedPositionForTree(sourceInfoNode.getSourcePosition() + offset); + // Given namespace "foo.bar.baz" we create node for "baz" here and need to calculate + // length of the last component which is "baz". + int lengthOfLastComponent = namespace.length() - (namespace.lastIndexOf(".") + 1); + newNode.setLengthForTree(lengthOfLastComponent); + } + } + + /** Get the offset into the provide node where the symbol appears. */ + private int getSourceInfoOffset() { + int indexOfLastDot = namespace.lastIndexOf('.'); + + // +1 for the opening quote + // +1 for the dot + // if there's no dot, then the -1 index cancels it out + // so elegant! + return 2 + indexOfLastDot; + } + + private Node getProvideStringNode() { + return (firstNode.getFirstChild() != null && NodeUtil.isExprCall(firstNode)) + ? firstNode.getFirstChild().getLastChild() + : null; + } + } + + private JSDocInfo createUnknownTypeJsDocInfo() { + JSDocInfoBuilder castToUnknownBuilder = new JSDocInfoBuilder(true); + castToUnknownBuilder.recordType( + new JSTypeExpression( + JsDocInfoParser.parseTypeString("?"), "")); + return castToUnknownBuilder.build(); + } + + /** @return Whether the node is namespace placeholder. */ + 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(); + } + + /** Add the given qualified name node to the symbol table. */ + private void maybeAddStringToSymbolTable(Node string) { + if (preprocessorSymbolTable != null) { + preprocessorSymbolTable.addStringNode(string, compiler); + } + } + + /** Add the given qualified name node to the symbol table. */ + private void maybeAddNameToSymbolTable(Node name) { + if (preprocessorSymbolTable != null) { + preprocessorSymbolTable.addReference(name); + } + } + + // ------------------------------------------------------------------------- + + /** + * Information required to create a {@link ProcessClosurePrimitives#MISSING_PROVIDE_ERROR} + * warning. + */ + private static class UnrecognizedRequire { + final Node requireNode; + final String namespace; + final boolean isRequireType; + + UnrecognizedRequire(Node requireNode, String namespace, boolean isRequireType) { + this.requireNode = requireNode; + this.namespace = namespace; + this.isRequireType = isRequireType; + } + } +}