blob: f6e6aeabd55f3e3f7c20056b28e76870c60bd4e7 [file] [log] [blame]
/*
* Copyright 2014 Google Inc.
*
* 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.gwt.dev.js;
import com.google.gwt.dev.jjs.SourceOrigin;
import com.google.gwt.dev.jjs.ast.JDeclaredType;
import com.google.gwt.dev.jjs.ast.JField;
import com.google.gwt.dev.jjs.ast.JMethod;
import com.google.gwt.dev.jjs.ast.JProgram;
import com.google.gwt.dev.jjs.impl.JavaToJavaScriptMap;
import com.google.gwt.dev.js.ast.JsBinaryOperation;
import com.google.gwt.dev.js.ast.JsBinaryOperator;
import com.google.gwt.dev.js.ast.JsContext;
import com.google.gwt.dev.js.ast.JsExprStmt;
import com.google.gwt.dev.js.ast.JsExpression;
import com.google.gwt.dev.js.ast.JsFunction;
import com.google.gwt.dev.js.ast.JsModVisitor;
import com.google.gwt.dev.js.ast.JsName;
import com.google.gwt.dev.js.ast.JsNameRef;
import com.google.gwt.dev.js.ast.JsObjectLiteral;
import com.google.gwt.dev.js.ast.JsProgram;
import com.google.gwt.dev.js.ast.JsStatement;
import com.google.gwt.dev.js.ast.JsVars;
import com.google.gwt.dev.js.ast.JsVars.JsVar;
import com.google.gwt.dev.util.Util;
import com.google.gwt.thirdparty.guava.common.collect.Lists;
import com.google.gwt.thirdparty.guava.common.collect.Maps;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
* A compiler pass that creates a namespace for each Java package
* with at least one global variable or function.
*
* <p>Prerequisite: JsVarRefs must be resolved.</p>
*/
public class JsNamespaceChooser {
public static void exec(JProgram jprogram, JsProgram jsprogram, JavaToJavaScriptMap jjsmap) {
new JsNamespaceChooser(jprogram, jsprogram, jjsmap).execImpl();
}
private final JProgram jprogram;
private final JsProgram jsprogram;
private final JavaToJavaScriptMap jjsmap;
/**
* The namespaces to be added to the program.
*/
private final Map<String, JsName> packageToNamespace = Maps.newLinkedHashMap();
private JsNamespaceChooser(JProgram jprogram, JsProgram jsprogram, JavaToJavaScriptMap jjsmap) {
this.jsprogram = jsprogram;
this.jprogram = jprogram;
this.jjsmap = jjsmap;
}
private void execImpl() {
// First pass: visit each top-level statement in the program and move it if possible.
// (This isn't a standard visitor because we don't want to recurse.)
List<JsStatement> globalStatements = jsprogram.getGlobalBlock().getStatements();
List<JsStatement> after = Lists.newArrayList();
for (JsStatement before : globalStatements) {
if (before instanceof JsVars) {
for (JsVar var : ((JsVars) before)) {
JsStatement replacement = visitGlobalVar(var);
if (replacement != null) {
after.add(replacement);
}
}
continue;
}
if (before instanceof JsExprStmt) {
JsExprStmt expressionStatement = (JsExprStmt) before;
if (expressionStatement.getExpression() instanceof JsFunction) {
JsExpression transformedFunction =
visitGlobalFunction((JsFunction) expressionStatement.getExpression());
expressionStatement.setExpression(transformedFunction);
}
}
after.add(before);
}
after.addAll(0, createNamespaceInitializers(packageToNamespace.values()));
globalStatements.clear();
globalStatements.addAll(after);
// Second pass: fix all references for moved names.
new NameFixer().accept(jsprogram);
}
/**
* Moves a global variable to a namespace if possible.
* (The references must still be fixed up.)
* @return the new initializer or null to delete it
*/
private JsStatement visitGlobalVar(JsVar x) {
JsName name = x.getName();
if (!moveName(name)) {
// We can't move it, but let's put the initializer on a separate line for readability.
JsVars vars = new JsVars(x.getSourceInfo());
vars.add(x);
return vars;
}
// Convert the initializer from a var to an assignment.
JsNameRef newName = name.makeRef(x.getSourceInfo());
JsExpression init = x.getInitExpr();
if (init == null) {
// It's undefined so we don't need to initialize it at all.
// (The namespace is sufficient.)
return null;
}
JsBinaryOperation assign = new JsBinaryOperation(x.getSourceInfo(),
JsBinaryOperator.ASG, newName, init);
return assign.makeStmt();
}
/**
* Moves a global function to a namespace if possible.
* (References must still be fixed up.)
* @return the new function definition.
*/
private JsExpression visitGlobalFunction(JsFunction func) {
JsName name = func.getName();
if (name == null || !moveName(name)) {
return func; // no change
}
// Convert the function statement into an assignment taking a named function expression:
// a.b = function b() { ... }
// The function also keeps its unqualified name for better stack traces in some browsers.
// Note: for reserving names, currently we pretend that 'b' is in global scope to avoid
// any name conflicts. It is actually two different names in two scopes; the 'b' in 'a.b'
// is in the 'a' namespace scope and the function name is in a separate scope containing
// just the function. We don't model either scope in the GWT compiler yet.
JsNameRef newName = name.makeRef(func.getSourceInfo());
JsBinaryOperation assign =
new JsBinaryOperation(func.getSourceInfo(), JsBinaryOperator.ASG, newName, func);
return assign;
}
/**
* Creates a "var = {}" statement for each namespace.
*/
private List<JsStatement> createNamespaceInitializers(Collection<JsName> namespaces) {
// Let's list them vertically for readability.
List<JsStatement> inits = Lists.newArrayList();
for (JsName name : namespaces) {
JsVar var = new JsVar(SourceOrigin.UNKNOWN, name);
var.setInitExpr(JsObjectLiteral.EMPTY);
JsVars vars = new JsVars(SourceOrigin.UNKNOWN);
vars.add(var);
inits.add(vars);
}
return inits;
}
/**
* Attempts to move the given name to a namespace. Returns true if it was changed.
* Side effects: may set the name's namespace and/or add a new mapping to
* {@link #packageToNamespace}.
*/
private boolean moveName(JsName name) {
if (name.getNamespace() != null) {
return false; // already in a namespace. (Shouldn't happen.)
}
if (!name.isObfuscatable()) {
return false; // probably a JavaScript name
}
String packageName = findPackage(name);
if (packageName == null) {
return false; // not compiled from Java
}
if (isIndexedName(name)) {
return false; // may be called directly in another pass (for example JsStackEmulator).
}
JsName namespace = packageToNamespace.get(packageName);
if (namespace == null) {
namespace = jsprogram.getScope().declareName(chooseUnusedName(packageName));
packageToNamespace.put(packageName, namespace);
}
name.setNamespace(namespace);
return true;
}
private boolean isIndexedName(JsName name) {
return jprogram != null
&& (jprogram.getIndexedMethods().contains(jjsmap.nameToMethod(name))
|| jprogram.getIndexedFields().contains(jjsmap.nameToField(name)));
}
private String chooseUnusedName(String packageName) {
String initials = initialsForPackage(packageName);
String candidate = initials;
int counter = 1;
while (jsprogram.getScope().findExistingName(candidate) != null) {
counter++;
candidate = initials + counter;
}
return candidate;
}
/**
* Find the Java package name for the given JsName, or null
* if it couldn't be determined.
*/
private String findPackage(JsName name) {
JMethod method = jjsmap.nameToMethod(name);
if (method != null) {
return findPackage(method.getEnclosingType());
}
JField field = jjsmap.nameToField(name);
if (field != null) {
return findPackage(field.getEnclosingType());
}
return null; // not found
}
private static String findPackage(JDeclaredType type) {
String packageName = Util.getPackageName(type.getName());
// Return null for the default package.
return packageName.isEmpty() ? null : packageName;
}
/**
* Find the initials of a package. For example, "java.lang" -> "jl".
*/
private static String initialsForPackage(String packageName) {
StringBuilder result = new StringBuilder();
int end = packageName.length();
boolean wasDot = true;
for (int i = 0; i < end; i++) {
char c = packageName.charAt(i);
if (c == '.') {
wasDot = true;
continue;
}
if (wasDot) {
result.append(c);
}
wasDot = false;
}
return result.toString();
}
/**
* A compiler pass that qualifies all moved names with the namespace.
* name => namespace.name
*/
private static class NameFixer extends JsModVisitor {
@Override
public void endVisit(JsNameRef x, JsContext ctx) {
if (x.getQualifier() != null || x.getName() == null) {
return;
}
JsName namespace = x.getName().getNamespace();
if (namespace == null) {
return;
}
x.setQualifier(new JsNameRef(x.getSourceInfo(), namespace));
didChange = true;
}
}
}