blob: 68ee660c756f275560c52558b849ed78c3e04f10 [file] [log] [blame]
/*
* 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.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.javascript.jscomp.GlobalNamespace.Name;
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.Set;
import java.util.WeakHashMap;
import javax.annotation.Nullable;
/**
* Apache Royale copied ProcessClosurePrimitives and modified it to handle
* Royale modules.
*
* 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 ProcessClosurePrimitivesWithModuleSupport extends AbstractPostOrderCallback implements HotSwapCompilerPass {
static final DiagnosticType NULL_ARGUMENT_ERROR = DiagnosticType.error(
"JSC_NULL_ARGUMENT_ERROR",
"method \"{0}\" called without an argument");
static final DiagnosticType EXPECTED_OBJECTLIT_ERROR = DiagnosticType.error(
"JSC_EXPECTED_OBJECTLIT_ERROR",
"method \"{0}\" expected an object literal argument");
static final DiagnosticType EXPECTED_STRING_ERROR =
DiagnosticType.error(
"JSC_EXPECTED_STRING_ERROR", "method \"{0}\" expected a string argument");
static final DiagnosticType INVALID_ARGUMENT_ERROR = DiagnosticType.error(
"JSC_INVALID_ARGUMENT_ERROR",
"method \"{0}\" called with invalid argument");
static final DiagnosticType INVALID_STYLE_ERROR = DiagnosticType.error(
"JSC_INVALID_CSS_NAME_MAP_STYLE_ERROR",
"Invalid CSS name map style {0}");
static final DiagnosticType TOO_MANY_ARGUMENTS_ERROR = DiagnosticType.error(
"JSC_TOO_MANY_ARGUMENTS_ERROR",
"method \"{0}\" called with more than one argument");
static final DiagnosticType DUPLICATE_NAMESPACE_ERROR =
DiagnosticType.error(
"JSC_DUPLICATE_NAMESPACE_ERROR",
"namespace \"{0}\" cannot be provided twice\n" //
+ "Originally provided at {1}");
static final DiagnosticType WEAK_NAMESPACE_TYPE = DiagnosticType.warning(
"JSC_WEAK_NAMESPACE_TYPE",
"Provided symbol declared with type Object. This is rarely useful. "
+ "For more information see "
+ "https://github.com/google/closure-compiler/wiki/A-word-about-the-type-Object");
static final DiagnosticType CLASS_NAMESPACE_ERROR = DiagnosticType.error(
"JSC_CLASS_NAMESPACE_ERROR",
"\"{0}\" cannot be both provided and declared as a class. Try var {0} = class '{'...'}'");
static final DiagnosticType FUNCTION_NAMESPACE_ERROR = DiagnosticType.error(
"JSC_FUNCTION_NAMESPACE_ERROR",
"\"{0}\" cannot be both provided and declared as a function");
static final DiagnosticType MISSING_PROVIDE_ERROR = DiagnosticType.error(
"JSC_MISSING_PROVIDE_ERROR",
"required \"{0}\" namespace never provided");
static final DiagnosticType LATE_PROVIDE_ERROR = DiagnosticType.error(
"JSC_LATE_PROVIDE_ERROR",
"required \"{0}\" namespace not provided yet");
static final DiagnosticType INVALID_PROVIDE_ERROR = DiagnosticType.error(
"JSC_INVALID_PROVIDE_ERROR",
"\"{0}\" is not a valid {1} qualified name");
static final DiagnosticType INVALID_DEFINE_NAME_ERROR = DiagnosticType.error(
"JSC_INVALID_DEFINE_NAME_ERROR",
"\"{0}\" is not a valid JS identifier name");
static final DiagnosticType MISSING_DEFINE_ANNOTATION = DiagnosticType.error(
"JSC_INVALID_MISSING_DEFINE_ANNOTATION",
"Missing @define annotation");
static final DiagnosticType XMODULE_REQUIRE_ERROR =
DiagnosticType.warning(
"JSC_XMODULE_REQUIRE_ERROR",
"namespace \"{0}\" is required in module {2} but provided in module {1}."
+ " Is module {2} missing a dependency on module {1}?");
static final DiagnosticType NON_STRING_PASSED_TO_SET_CSS_NAME_MAPPING_ERROR =
DiagnosticType.error(
"JSC_NON_STRING_PASSED_TO_SET_CSS_NAME_MAPPING_ERROR",
"goog.setCssNameMapping only takes an object literal with string values");
static final DiagnosticType INVALID_CSS_RENAMING_MAP = DiagnosticType.warning(
"INVALID_CSS_RENAMING_MAP",
"Invalid entries in css renaming map: {0}");
static final DiagnosticType GOOG_BASE_CLASS_ERROR = DiagnosticType.error(
"JSC_GOOG_BASE_CLASS_ERROR",
"incorrect use of goog.base: {0}");
static final DiagnosticType BASE_CLASS_ERROR = DiagnosticType.error(
"JSC_BASE_CLASS_ERROR",
"incorrect use of {0}.base: {1}");
static final DiagnosticType CLOSURE_DEFINES_ERROR = DiagnosticType.error(
"JSC_CLOSURE_DEFINES_ERROR",
"Invalid CLOSURE_DEFINES definition");
static final DiagnosticType INVALID_FORWARD_DECLARE = DiagnosticType.error(
"JSC_INVALID_FORWARD_DECLARE",
"Malformed goog.forwardDeclaration");
static final DiagnosticType USE_OF_GOOG_BASE = DiagnosticType.disabled(
"JSC_USE_OF_GOOG_BASE",
"goog.base is not compatible with ES5 strict mode.\n"
+ "Please use an alternative.\n"
+ "For EcmaScript classes use the super keyword. For traditional Closure classes,\n"
+ "use the class specific base method instead. For example, for the constructor MyClass:\n"
+ " MyClass.base(this, ''constructor'')");
static final DiagnosticType CLOSURE_CALL_CANNOT_BE_ALIASED_ERROR =
DiagnosticType.error(
"JSC_CLOSURE_CALL_CANNOT_BE_ALIASED_ERROR",
"Closure primitive method {0} may not be aliased");
static final DiagnosticType CLOSURE_CALL_CANNOT_BE_ALIASED_OUTSIDE_MODULE_ERROR =
DiagnosticType.error(
"JSC_CLOSURE_CALL_CANNOT_BE_ALIASED_ERROR",
"Closure primitive method {0} may not be aliased outside a module (ES "
+ "module, CommonJS module, or goog.module)");
/** 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<String, ProvidedName> providedNames = new LinkedHashMap<String, ProvidedName>();
private final Set<String> knownClosureSubclasses = new HashSet<String>();
private final List<UnrecognizedRequire> unrecognizedRequires = new ArrayList<UnrecognizedRequire>();
private final Set<String> exportedVariables = new HashSet<String>();
private final CheckLevel requiresLevel;
private final PreprocessorSymbolTable preprocessorSymbolTable;
private final List<Node> defineCalls = new ArrayList<Node>();
private final boolean preserveGoogProvidesAndRequires;
private final List<Node> requiresToBeRemoved = new ArrayList<Node>();
private final Set<Node> maybeTemporarilyLiveNodes = new HashSet<Node>();
ProcessClosurePrimitivesWithModuleSupport(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 */));
}
Set<String> getExportedVariableNames() {
return exportedVariables;
}
/** list of strings from the externs */
private ArrayList<String> externStrings = new ArrayList<String>();
/** a map extern node trees to their list of vars that are in the externs but also renamed/aliased */
public static WeakHashMap<Node, List<String>> externedAliases = new WeakHashMap<Node, List<String>>();
/** list of namespaces that are in the externs but also renamed/aliased */
private ArrayList<String> externAliases = new ArrayList<String>();
/** a map extern node trees to the processed goog.provides */
public static WeakHashMap<Node, List<String>> providedsMap = new WeakHashMap<Node, List<String>>();
/** list of namespaces that are goog.provided */
private ArrayList<String> provideds = new ArrayList<String>();
@Override
public void process(Node externs, Node root) {
/*
* ApacheRoyale: build list of namespaces from externs
*/
GlobalNamespace externNamespace = new GlobalNamespace(compiler, externs);
List<Name> externNames = externNamespace.getNameForest();
for (Name en : externNames) {
addExternNameAndDescendants(en, externStrings);
}
externedAliases.put(externs, externAliases);
providedsMap.put(externs, provideds);
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));
}
private Node getAnyValueOfType(JSDocInfo jsdoc) {
checkArgument(jsdoc.hasType());
Node typeAst = jsdoc.getType().getRoot();
if (typeAst.getToken() == Token.BANG) {
typeAst = typeAst.getLastChild();
}
checkState(typeAst.isString(), typeAst);
/** Converted switch to else/if for Java 1.6 compatibility */
String astType = typeAst.getString();
if (astType.contentEquals("boolean"))
return IR.falseNode();
if (astType.contentEquals("string"))
return IR.string("");
if (astType.contentEquals("number"))
return IR.number(0);
throw new RuntimeException(typeAst.getString());
}
/**
* @param n
*/
private void replaceGoogDefines(Node n) {
Node parent = n.getParent();
checkState(parent.isExprResult());
String name = n.getSecondChild().getString();
JSDocInfo jsdoc = n.getJSDocInfo();
Node value =
n.isFromExterns() ? getAnyValueOfType(jsdoc).srcref(n) : n.getChildAtIndex(2).detach();
Node replacement = NodeUtil.newQNameDeclaration(compiler, name, value, jsdoc);
replacement.useSourceInfoIfMissingFromForTree(parent);
parent.replaceWith(replacement);
compiler.reportChangeToEnclosingScope(replacement);
}
@Override
public void hotSwapScript(Node scriptRoot, Node originalRoot) {
// TODO(bashir): Implement a real hot-swap version instead and make it fully
// consistent with the full version.
this.compiler.process(this);
}
@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();
/** Converted switch to else/if for Java 1.6 compatibility */
if (methodName.contentEquals("base"))
processBaseClassCall(t, n);
else if (methodName.contentEquals("define"))
{
if (validateUnaliasablePrimitiveCall(t, n, methodName)) {
processDefineCall(t, n, parent);
}
}
else if (methodName.contentEquals("require") ||
methodName.contentEquals("requireType"))
{
if (validateAliasiablePrimitiveCall(t, n, methodName)) {
processRequireCall(t, n, parent);
}
}
else if (methodName.contentEquals("provide"))
{
if (validateUnaliasablePrimitiveCall(t, n, methodName)) {
processProvideCall(t, n, parent);
}
}
else if (methodName.contentEquals("inherits"))
processInheritsCall(n);
else if (methodName.contentEquals("exportSymbol"))
{
// Note: exportSymbol is allowed in local scope
Node arg = left.getNext();
if (arg.isString()) {
String argString = arg.getString();
int dot = argString.indexOf('.');
if (dot == -1) {
exportedVariables.add(argString);
} else {
exportedVariables.add(argString.substring(0, dot));
}
}
}
else if (methodName.contentEquals("forwardDeclare"))
{
if (validateAliasiablePrimitiveCall(t, n, methodName)) {
processForwardDeclare(t, n, parent);
}
}
else if (methodName.contentEquals("addDependency"))
{
if (validateUnaliasablePrimitiveCall(t, n, methodName)) {
processAddDependency(n, parent);
}
}
else if (methodName.contentEquals("setCssNameMapping")) {
processSetCssNameMapping(t, n, parent);
}
} else if (left.getLastChild().getString().equals("base")) {
// maybe an "base" setup by goog.inherits
maybeProcessClassBaseCall(t, n);
}
}
break;
case ASSIGN:
case NAME:
if (n.isName() && n.getString().equals("CLOSURE_DEFINES")) {
handleClosureDefinesValues(t, 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(t.makeError(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(t.makeError(n, FUNCTION_NAMESPACE_ERROR, name));
}
}
break;
case GETPROP:
if (n.getFirstChild().isName()
&& !parent.isCall()
&& !parent.isAssign()
&& n.matchesQualifiedName("goog.base")
&& !n.getSourceFileName().endsWith("goog.js")) {
reportBadGoogBaseUse(t, n, "May only be called directly.");
}
break;
default:
break;
}
}
/**
* Verifies that a) the call is in the global scope and b) the return value is unused
*
* <p>This method is for primitives that never return a value.
*/
private boolean validateUnaliasablePrimitiveCall(NodeTraversal t, Node n, String methodName) {
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
*
* <p>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
* 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(t.makeError(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(t.makeError(n, invalidAliasingError, GOOG + "." + methodName));
return false;
}
return true;
}
private void handleClosureDefinesValues(NodeTraversal t, Node n) {
// var CLOSURE_DEFINES = {};
if (NodeUtil.isNameDeclaration(n.getParent())
&& n.hasOneChild()
&& n.getFirstChild().isObjectLit()) {
HashMap<String, Node> builder = new HashMap<String, Node>();
builder.putAll(compiler.getDefaultDefineValues());
for (Node c : n.getFirstChild().children()) {
if (c.isStringKey()
&& c.getFirstChild() != null // Shorthand assignment
&& isValidDefineValue(c.getFirstChild())) {
builder.put(c.getString(), c.getFirstChild().cloneTree());
} else {
reportBadClosureCommonDefinesDefinition(t, c);
}
}
compiler.setDefaultDefineValues(ImmutableMap.copyOf(builder));
}
}
static boolean isValidDefineValue(Node val) {
switch (val.getToken()) {
case STRING:
case NUMBER:
case TRUE:
case FALSE:
return true;
case NEG:
return val.getFirstChild().isNumber();
default:
return false;
}
}
/** 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(t, 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(
t.makeError(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(t, 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(t.makeError(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.
*/
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);
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.
*/
private void processBaseClassCall(NodeTraversal t, Node n) {
// Two things must hold for every goog.base call:
// 1) We must be calling it on "this".
// 2) We must be calling it on a prototype method of the same name as
// the one we're in, OR we must be calling it from a constructor.
// If both of those things are true, then we can rewrite:
// <pre>
// function Foo() {
// goog.base(this);
// }
// goog.inherits(Foo, BaseFoo);
// Foo.prototype.bar = function() {
// goog.base(this, 'bar', 1);
// };
// </pre>
// as the easy-to-optimize:
// <pre>
// function Foo() {
// BaseFoo.call(this);
// }
// goog.inherits(Foo, BaseFoo);
// Foo.prototype.bar = function() {
// Foo.superClass_.bar.call(this, 1);
// };
//
// Most of the logic here is just to make sure the AST's
// structure is what we expect it to be.
// If requested report uses of goog.base.
t.report(n, USE_OF_GOOG_BASE);
if (baseUsedInClass(n)){
reportBadGoogBaseUse(t, n, "goog.base in ES6 class is not allowed. Use super instead.");
return;
}
Node callee = n.getFirstChild();
Node thisArg = callee.getNext();
if (thisArg == null || !thisArg.isThis()) {
reportBadGoogBaseUse(t, n, "First argument must be 'this'.");
return;
}
Node enclosingFnNameNode = getEnclosingDeclNameNode(n);
if (enclosingFnNameNode == null) {
reportBadGoogBaseUse(t, n, "Could not find enclosing method.");
return;
}
String enclosingQname = enclosingFnNameNode.getQualifiedName();
if (!enclosingQname.contains(".prototype.")) {
// Handle constructors.
Node enclosingParent = enclosingFnNameNode.getParent();
Node maybeInheritsExpr =
(enclosingParent.isAssign() ? enclosingParent.getParent() : enclosingParent).getNext();
Node baseClassNode = null;
if (maybeInheritsExpr != null
&& maybeInheritsExpr.isExprResult()
&& maybeInheritsExpr.getFirstChild().isCall()) {
Node callNode = maybeInheritsExpr.getFirstChild();
if (callNode.getFirstChild().matchesQualifiedName("goog.inherits")
&& callNode.getLastChild().isQualifiedName()) {
baseClassNode = callNode.getLastChild();
}
}
if (baseClassNode == null) {
reportBadGoogBaseUse(
t, n, "Could not find goog.inherits for base class");
return;
}
// We're good to go.
Node newCallee =
NodeUtil.newQName(
compiler, baseClassNode.getQualifiedName() + ".call", callee, "goog.base");
n.replaceChild(callee, newCallee);
compiler.reportChangeToEnclosingScope(newCallee);
} else {
// Handle methods.
Node methodNameNode = thisArg.getNext();
if (methodNameNode == null || !methodNameNode.isString()) {
reportBadGoogBaseUse(t, n, "Second argument must name a method.");
return;
}
String methodName = methodNameNode.getString();
String ending = ".prototype." + methodName;
if (enclosingQname == null || !enclosingQname.endsWith(ending)) {
reportBadGoogBaseUse(
t, n, "Enclosing method does not match " + methodName);
return;
}
// We're good to go.
Node className =
enclosingFnNameNode.getFirstFirstChild();
n.replaceChild(
callee,
NodeUtil.newQName(
compiler,
className.getQualifiedName() + ".superClass_." + methodName + ".call",
callee, "goog.base"));
n.removeChild(methodNameNode);
compiler.reportChangeToEnclosingScope(n);
}
}
private void maybeProcessClassBaseCall(NodeTraversal t, Node n) {
// Two things must hold for every base call:
// 1) We must be calling it on "this".
// 2) We must be calling it on a prototype method of the same name as
// the one we're in, OR we must be calling it from a constructor.
// If both of those things are true, then we can rewrite:
// <pre>
// function Foo() {
// Foo.base(this);
// }
// goog.inherits(Foo, BaseFoo);
// Foo.prototype.bar = function() {
// Foo.base(this, 'bar', 1);
// };
// </pre>
// as the easy-to-optimize:
// <pre>
// function Foo() {
// BaseFoo.call(this);
// }
// goog.inherits(Foo, BaseFoo);
// Foo.prototype.bar = function() {
// Foo.superClass_.bar.call(this, 1);
// };
//
// Most of the logic here is just to make sure the AST's
// structure is what we expect it to be.
Node callTarget = n.getFirstChild();
Node baseContainerNode = callTarget.getFirstChild();
if (!baseContainerNode.isUnscopedQualifiedName()) {
// Some unknown "base" method.
return;
}
String baseContainer = callTarget.getFirstChild().getQualifiedName();
Node enclosingFnNameNode = getEnclosingDeclNameNode(n);
if (enclosingFnNameNode == null || !enclosingFnNameNode.isUnscopedQualifiedName()) {
// some unknown container method.
if (knownClosureSubclasses.contains(baseContainer)) {
reportBadBaseMethodUse(t, n, baseContainer, "Could not find enclosing method.");
} else if (baseUsedInClass(n)) {
Node clazz = NodeUtil.getEnclosingClass(n);
if ((clazz.getFirstChild().isName()
&& clazz.getFirstChild().getString().equals(baseContainer))
|| (clazz.getSecondChild().isName()
&& clazz.getSecondChild().getString().equals(baseContainer))) {
reportBadBaseMethodUse(t, n, clazz.getFirstChild().getString(),
"base method is not allowed in ES6 class. Use super instead.");
}
}
return;
}
if (baseUsedInClass(n)) {
reportBadGoogBaseUse(t, n, "goog.base in ES6 class is not allowed. Use super instead.");
return;
}
String enclosingQname = enclosingFnNameNode.getQualifiedName();
if (!enclosingQname.contains(".prototype.")) {
// Handle constructors.
// Check if this is some other "base" method.
if (!enclosingQname.equals(baseContainer)) {
// Report misuse of "base" methods from other known classes.
if (knownClosureSubclasses.contains(baseContainer)) {
reportBadBaseMethodUse(t, n, baseContainer, "Must be used within "
+ baseContainer + " methods");
}
return;
}
// Determine if this is a class with a "base" method created by
// goog.inherits.
Node enclosingParent = enclosingFnNameNode.getParent();
Node maybeInheritsExpr =
(enclosingParent.isAssign() ? enclosingParent.getParent() : enclosingParent).getNext();
while (maybeInheritsExpr != null && maybeInheritsExpr.isEmpty()) {
maybeInheritsExpr = maybeInheritsExpr.getNext();
}
Node baseClassNode = null;
if (maybeInheritsExpr != null
&& maybeInheritsExpr.isExprResult()
&& maybeInheritsExpr.getFirstChild().isCall()) {
Node callNode = maybeInheritsExpr.getFirstChild();
if (callNode.getFirstChild().matchesQualifiedName("goog.inherits")
&& callNode.getLastChild().isQualifiedName()) {
baseClassNode = callNode.getLastChild();
}
}
if (baseClassNode == null) {
// If there is no "goog.inherits", this might be some other "base"
// method.
return;
}
// This is the expected method, validate its parameters.
Node callee = n.getFirstChild();
Node thisArg = callee.getNext();
if (thisArg == null || !thisArg.isThis()) {
reportBadBaseMethodUse(t, n, baseContainer,
"First argument must be 'this'.");
return;
}
// Handle methods.
Node methodNameNode = thisArg.getNext();
if (methodNameNode == null
|| !methodNameNode.isString()
|| !methodNameNode.getString().equals("constructor")) {
reportBadBaseMethodUse(t, n, baseContainer,
"Second argument must be 'constructor'.");
return;
}
// We're good to go.
n.replaceChild(
callee,
NodeUtil.newQName(
compiler,
baseClassNode.getQualifiedName() + ".call",
callee, enclosingQname + ".base"));
n.removeChild(methodNameNode);
compiler.reportChangeToEnclosingScope(n);
} else {
if (!knownClosureSubclasses.contains(baseContainer)) {
// Can't determine if this is a known "class" that has a known "base"
// method.
return;
}
boolean misuseOfBase = !enclosingFnNameNode.
getFirstFirstChild().matchesQualifiedName(baseContainer);
if (misuseOfBase) {
// Report misuse of "base" methods from other known classes.
reportBadBaseMethodUse(t, n, baseContainer, "Must be used within "
+ baseContainer + " methods");
return;
}
// The super class is known.
Node callee = n.getFirstChild();
Node thisArg = callee.getNext();
if (thisArg == null || !thisArg.isThis()) {
reportBadBaseMethodUse(t, n, baseContainer,
"First argument must be 'this'.");
return;
}
// Handle methods.
Node methodNameNode = thisArg.getNext();
if (methodNameNode == null || !methodNameNode.isString()) {
reportBadBaseMethodUse(t, n, baseContainer,
"Second argument must name a method.");
return;
}
String methodName = methodNameNode.getString();
String ending = ".prototype." + methodName;
if (enclosingQname == null || !enclosingQname.endsWith(ending)) {
reportBadBaseMethodUse(t, n, baseContainer,
"Enclosing method does not match " + methodName);
return;
}
// We're good to go.
Node className =
enclosingFnNameNode.getFirstFirstChild();
n.replaceChild(
callee,
NodeUtil.newQName(
compiler,
className.getQualifiedName() + ".superClass_." + methodName + ".call",
callee, enclosingQname + ".base"));
n.removeChild(methodNameNode);
compiler.reportChangeToEnclosingScope(n);
}
}
/**
* Processes the goog.inherits call.
*/
private void processInheritsCall(Node n) {
if (n.getChildCount() == 3) {
Node subClass = n.getSecondChild();
Node superClass = subClass.getNext();
if (subClass.isUnscopedQualifiedName() && superClass.isUnscopedQualifiedName()) {
knownClosureSubclasses.add(subClass.getQualifiedName());
}
}
}
/**
* Returns the qualified name node of the function whose scope we're in,
* or null if it cannot be found.
*/
private static Node getEnclosingDeclNameNode(Node n) {
Node fn = NodeUtil.getEnclosingFunction(n);
return fn == null ? null : NodeUtil.getNameNode(fn);
}
/** Verify if goog.base call is used in a class */
private boolean baseUsedInClass(Node n){
for (Node curr = n; curr != null; curr = curr.getParent()){
if (curr.isClassMembers()) {
return true;
}
}
return false;
}
/** Reports an incorrect use of super-method calling. */
private void reportBadGoogBaseUse(
NodeTraversal t, Node n, String extraMessage) {
compiler.report(t.makeError(n, GOOG_BASE_CLASS_ERROR, extraMessage));
}
/** Reports an incorrect use of super-method calling. */
private void reportBadBaseMethodUse(
NodeTraversal t, Node n, String className, String extraMessage) {
compiler.report(t.makeError(n, BASE_CLASS_ERROR, className, extraMessage));
}
/** Reports an incorrect CLOSURE_DEFINES definition. */
private void reportBadClosureCommonDefinesDefinition(
NodeTraversal t, Node n) {
compiler.report(t.makeError(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 CompilerPass, or it is invalid
* and a JSCompiler error will be reported.
* @see #visit(NodeTraversal, Node, Node)
*/
private void processSetCssNameMapping(NodeTraversal t, Node n, Node parent) {
Node left = n.getFirstChild();
Node arg = left.getNext();
if (verifySetCssNameMapping(t, left, arg)) {
// Translate OBJECTLIT into SubstitutionMap. All keys and
// values must be strings, or an error will be thrown.
final Map<String, String> cssNames = new HashMap<String, String>();
for (Node key = arg.getFirstChild(); key != null;
key = key.getNext()) {
Node value = key.getFirstChild();
if (!key.isStringKey()
|| value == null
|| !value.isString()) {
compiler.report(
t.makeError(n,
NON_STRING_PASSED_TO_SET_CSS_NAME_MAPPING_ERROR));
return;
}
cssNames.put(key.getString(), value.getString());
}
String styleStr = "BY_PART";
if (arg.getNext() != null) {
styleStr = arg.getNext().getString();
}
final CssRenamingMap.Style style;
try {
style = CssRenamingMap.Style.valueOf(styleStr);
} catch (IllegalArgumentException e) {
compiler.report(
t.makeError(n, INVALID_STYLE_ERROR, styleStr));
return;
}
if (style == CssRenamingMap.Style.BY_PART) {
// Make sure that no keys contain -'s
List<String> errors = new ArrayList<String>();
for (String key : cssNames.keySet()) {
if (key.contains("-")) {
errors.add(key);
}
}
if (!errors.isEmpty()) {
compiler.report(
t.makeError(n, INVALID_CSS_RENAMING_MAP, errors.toString()));
}
} else if (style == CssRenamingMap.Style.BY_WHOLE) {
// Verifying things is a lot trickier here. We just do a quick
// n^2 check over the map which makes sure that if "a-b" in
// the map, then map(a-b) = map(a)-map(b).
// To speed things up, only consider cases where len(b) <= 10
List<String> errors = new ArrayList<String>();
for (Map.Entry<String, String> b : cssNames.entrySet()) {
if (b.getKey().length() > 10) {
continue;
}
for (Map.Entry<String, String> a : cssNames.entrySet()) {
String combined = cssNames.get(a.getKey() + "-" + b.getKey());
if (combined != null && !combined.equals(a.getValue() + "-" + b.getValue())) {
errors.add("map(" + a.getKey() + "-" + b.getKey() + ") != map("
+ a.getKey() + ")-map(" + b.getKey() + ")");
}
}
}
if (!errors.isEmpty()) {
compiler.report(
t.makeError(n, INVALID_CSS_RENAMING_MAP, errors.toString()));
}
}
@SuppressWarnings("serial")
CssRenamingMap cssRenamingMap = new CssRenamingMap() {
@Override
public String get(String value) {
if (cssNames.containsKey(value)) {
return cssNames.get(value);
} else {
return value;
}
}
@Override
public CssRenamingMap.Style getStyle() {
return style;
}
};
compiler.setCssRenamingMap(cssRenamingMap);
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(NodeTraversal t, Node methodName, Node arg) {
if (!verifyLastArgumentIsString(t, methodName, arg)) {
return false;
}
if (!NodeUtil.isValidQualifiedName(
compiler.getOptions().getLanguageIn().toFeatureSet(), arg.getString())) {
compiler.report(t.makeError(arg, INVALID_PROVIDE_ERROR,
arg.getString(), compiler.getOptions().getLanguageIn().toString()));
return false;
}
return true;
}
/**
* 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 verifyDefine(NodeTraversal t,
Node expr,
Node methodName, Node args) {
// Verify first arg
Node arg = args;
if (!verifyNotNull(t, methodName, arg) || !verifyOfType(t, methodName, arg, Token.STRING)) {
return false;
}
// Verify second arg
arg = arg.getNext();
if (!args.isFromExterns()
&& (!verifyNotNull(t, methodName, arg) || !verifyIsLast(t, methodName, arg))) {
return false;
}
String name = args.getString();
if (!NodeUtil.isValidQualifiedName(
compiler.getOptions().getLanguageIn().toFeatureSet(), name)) {
compiler.report(t.makeError(args, INVALID_DEFINE_NAME_ERROR, name));
return false;
}
JSDocInfo info = expr.getFirstChild().getJSDocInfo();
if (info == null || !info.isDefine()) {
compiler.report(t.makeError(expr, MISSING_DEFINE_ANNOTATION));
return false;
}
return true;
}
/**
* Process a goog.addDependency() call and record any forward declarations.
*/
private void processAddDependency(Node n, Node parent) {
CodingConvention convention = compiler.getCodingConvention();
List<String> typeDecls =
convention.identifyTypeDeclarationCall(n);
// TODO(nnaze): Use of addDependency() should someday cause a warning
// as we migrate users to explicit goog.forwardDeclare() calls.
if (typeDecls != null) {
for (String typeDecl : typeDecls) {
compiler.forwardDeclareType(typeDecl);
}
}
// We can't modify parent, so just create a node that will
// get compiled out.
Node emptyNode = IR.number(0);
parent.replaceChild(n, emptyNode);
compiler.reportChangeToEnclosingScope(emptyNode);
}
/**
* Process a goog.forwardDeclare() call and record the specified forward
* declaration.
*/
private void processForwardDeclare(NodeTraversal t, Node n, Node parent) {
CodingConvention convention = compiler.getCodingConvention();
String typeDeclaration = null;
try {
typeDeclaration = Iterables.getOnlyElement(
convention.identifyTypeDeclarationCall(n));
} catch (Exception e) {
compiler.report(
t.makeError(
n,
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(
NodeTraversal t, Node methodName, Node arg) {
return verifyNotNull(t, methodName, arg)
&& verifyOfType(t, methodName, arg, Token.STRING)
&& verifyIsLast(t, methodName, arg);
}
/**
* @return Whether the argument checked out okay
*/
private boolean verifyNotNull(NodeTraversal t, Node methodName, Node arg) {
if (arg == null) {
compiler.report(
t.makeError(methodName,
NULL_ARGUMENT_ERROR, methodName.getQualifiedName()));
return false;
}
return true;
}
/**
* @return Whether the argument checked out okay
*/
private boolean verifyOfType(NodeTraversal t, Node methodName,
Node arg, Token desiredType) {
if (arg.getToken() != desiredType) {
compiler.report(
t.makeError(methodName,
INVALID_ARGUMENT_ERROR, methodName.getQualifiedName()));
return false;
}
return true;
}
/**
* @return Whether the argument checked out okay
*/
private boolean verifyIsLast(NodeTraversal t, Node methodName, Node arg) {
if (arg.getNext() != null) {
compiler.report(
t.makeError(methodName,
TOO_MANY_ARGUMENTS_ERROR, methodName.getQualifiedName()));
return false;
}
return true;
}
/**
* Verifies that setCssNameMapping is called with the correct methods.
*
* @return Whether the arguments checked out okay
*/
private boolean verifySetCssNameMapping(NodeTraversal t, Node methodName,
Node firstArg) {
DiagnosticType diagnostic = null;
if (firstArg == null) {
diagnostic = NULL_ARGUMENT_ERROR;
} else if (!firstArg.isObjectLit()) {
diagnostic = EXPECTED_OBJECTLIT_ERROR;
} else if (firstArg.getNext() != null) {
Node secondArg = firstArg.getNext();
if (!secondArg.isString()) {
diagnostic = EXPECTED_STRING_ERROR;
} else if (secondArg.getNext() != null) {
diagnostic = TOO_MANY_ARGUMENTS_ERROR;
}
}
if (diagnostic != null) {
compiler.report(
t.makeError(methodName,
diagnostic, methodName.getQualifiedName()));
return false;
}
return true;
}
/**
* Build up the list of strings in the externs.
*
* @param en The extern name.
* @param externStrings The list of strings to add to.
*/
private void addExternNameAndDescendants(Name en, ArrayList<String> externStrings) {
externStrings.add(en.getName());
if (en.props == null || en.isCollapsingExplicitlyDenied()) {
return;
}
for (Name p : en.props) {
addExternNameAndDescendants(p, externStrings);
}
}
/**
* 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 */));
// if it was in externs, added to externAliases
if (externStrings.contains(prefixNs))
if (!externAliases.contains(prefixNs))
externAliases.add(prefixNs);
// if the namespace of any provided classes
if (!provideds.contains(prefixNs))
provideds.add(prefixNs);
}
}
}
// -------------------------------------------------------------------------
/**
* 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. <code>var foo = {};</code>).
*/
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. <code>foo.bar = {};</code>).
*/
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);
}
}
/**
* 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("?"), "<ProcessClosurePrimitives.java>"));
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) {
if (preprocessorSymbolTable != null) {
preprocessorSymbolTable.addReference(n);
}
}
// -------------------------------------------------------------------------
/**
* 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;
}
}
}