| /* |
| * 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; |
| } |
| } |
| } |