blob: 359b938c379a2b31e80d6e0ff407583df506045d [file] [log] [blame]
/*
* Copyright 2015 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.jjs.impl;
import com.google.gwt.dev.jjs.ast.Context;
import com.google.gwt.dev.jjs.ast.JBlock;
import com.google.gwt.dev.jjs.ast.JFieldRef;
import com.google.gwt.dev.jjs.ast.JLocal;
import com.google.gwt.dev.jjs.ast.JMethod;
import com.google.gwt.dev.jjs.ast.JMethodBody;
import com.google.gwt.dev.jjs.ast.JModVisitor;
import com.google.gwt.dev.jjs.ast.JParameter;
import com.google.gwt.dev.jjs.ast.JProgram;
import com.google.gwt.dev.jjs.ast.JVariable;
import com.google.gwt.dev.jjs.ast.JVariableRef;
import com.google.gwt.dev.jjs.ast.JVisitor;
import com.google.gwt.thirdparty.guava.common.collect.ImmutableSet;
import com.google.gwt.thirdparty.guava.common.collect.LinkedHashMultimap;
import com.google.gwt.thirdparty.guava.common.collect.Maps;
import com.google.gwt.thirdparty.guava.common.collect.Multimap;
import com.google.gwt.thirdparty.guava.common.collect.Sets;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
/**
* The GWT Java AST might contain different local variables with the same name in the same
* scope. This fixup pass renames variables in the case they clash in a scope.
*/
public class NameClashesFixer {
private static class FixNameClashesVisitor extends JVisitor {
/**
* Represents the scope tree defined by nested statement blocks. It is a temporary structure to
* track local variable lifetimes.
*/
private static class Scope {
private Scope parent;
// Keeps track what names are used in children.
private Set<String> usedInChildScope = Sets.newHashSet();
// Keeps track what names have this scope as its lifetime.
private Set<String> namesInThisScope = Sets.newHashSet();
/**
* The depth at which this scope is in the tree.
*/
private int level;
private Scope() {
this.parent = null;
this.level = 0;
}
private Scope(Scope parent) {
this.parent = parent;
this.level = parent.level + 1;
}
private static Scope getInnermostEnclosingScope(Scope thisScope, Scope thatScope) {
if (thisScope == null) {
return thatScope;
}
if (thatScope == null) {
return thisScope;
}
if (thisScope == thatScope) {
return thisScope;
}
if (thisScope.level > thatScope.level) {
return getInnermostEnclosingScope(thatScope, thisScope);
}
if (thisScope.level == thatScope.level) {
return getInnermostEnclosingScope(thisScope.parent, thatScope.parent);
}
return getInnermostEnclosingScope(thisScope, thatScope.parent);
}
private void addChildUsage(String name) {
usedInChildScope.add(name);
if (parent != null) {
parent.addChildUsage(name);
}
}
protected void addUsedName(String name) {
namesInThisScope.add(name);
if (parent != null) {
parent.addChildUsage(name);
}
}
private boolean isUsedInParent(String name) {
return namesInThisScope.contains(name) ||
(parent != null && parent.isUsedInParent(name));
}
protected boolean isConflictingName(String name) {
return usedInChildScope.contains(name) || isUsedInParent(name);
}
}
private Scope currentScope;
private Map<JVariable, Scope> scopesByLocal;
private Multimap<String, JVariable> localsByName;
@Override
public boolean visit(JMethodBody x, Context ctx) {
// Start constructing the scope tree.
currentScope = new Scope();
scopesByLocal = Maps.newHashMap();
localsByName = LinkedHashMultimap.create();
return true;
}
@Override
public boolean visit(JBlock x, Context ctx) {
currentScope = new Scope(currentScope);
return true;
}
@Override
public void endVisit(JBlock x, Context ctx) {
currentScope = currentScope.parent;
}
@Override
public void endVisit(JVariableRef x, Context ctx) {
// We use the a block scope as a proxy for a lifetime which is safe to do albeit non optimal.
//
// Keep track of the scope that encloses a variable lifetime. E.g. assume the following code.
// { // scope 1
// { // scope 1.1
// ... a... b...
// { // scope 1.1.1
// ... a ...
// }
// }
// { // scope 1.2
// ... b...
// }
// }
// Scope 1.1 is the innermost scope that encloses the lifetime of variable a and
// scope 1 is the innermost scope that encloses the lifetime of variable b.
if (x instanceof JFieldRef) {
// Skip fields as they are always qualified in JavaScript and their name resolution logic
// is in {@link CreateNameAndScopesVisitor}.
return;
}
JVariable local = x.getTarget();
Scope oldVariableScope = scopesByLocal.get(local);
Scope newVariableScope = Scope.getInnermostEnclosingScope(oldVariableScope, currentScope);
newVariableScope.addUsedName(local.getName());
if (newVariableScope != oldVariableScope) {
scopesByLocal.put(local, newVariableScope);
}
localsByName.put(local.getName(), local);
}
@Override
public void endVisit(JMethodBody x, Context ctx) {
// Fix clashing variables here. Two locals are clashing if they have the same name and their
// computed lifetimes are intersecting. By using the scope to model lifetimes two variables
// clash if their computed scopes are nested.
for (String name : localsByName.keySet()) {
Collection<JVariable> localSet = localsByName.get(name);
if (localSet.size() == 1) {
continue;
}
JVariable[] locals = localSet.toArray(new JVariable[localSet.size()]);
// TODO(rluble): remove n^2 behaviour in conflict checking.
// In practice each method has only a handful of locals so this process is not expected
// to be a performance problem.
for (int i = 0; i < locals.length; i++) {
// See if local i conflicts with any local j > i
for (int j = i + 1; j < locals.length; j++) {
Scope iLocalScope = scopesByLocal.get(locals[i]);
Scope jLocalScope = scopesByLocal.get(locals[j]);
Scope commonAncestor = Scope.getInnermostEnclosingScope(iLocalScope, jLocalScope);
if (commonAncestor != iLocalScope && commonAncestor != jLocalScope) {
// no conflict
continue;
}
// conflicting locals => find a unique name rename local i to it;
int n = 0;
String baseName = locals[i].getName();
String newName;
do {
// The active namer will clean up these potentially long names.
newName = baseName + n++;
} while (iLocalScope.isConflictingName(newName));
locals[i].setName(newName);
iLocalScope.addUsedName(newName);
// There is no need to update the localsByNameMap as newNames are always guaranteed to
// be clash free.
break;
}
}
}
// Only valid for the duration of one method body visit/endVisit pair.
currentScope = null;
scopesByLocal = null;
localsByName = null;
}
}
public static void exec(JProgram program) {
// Rename variables that should not be shadowed.
new JModVisitor() {
@Override
public boolean visit(JMethod method, Context ctx) {
return !method.isJsniMethod() && method.isJsMethodVarargs();
}
@Override
public void endVisit(JLocal variable, Context ctx) {
maybeRename(variable);
}
@Override
public void endVisit(JParameter parameter, Context ctx) {
maybeRename(parameter);
}
}.accept(program);
// Resolve clashes.
new FixNameClashesVisitor().accept(program);
}
private static Set<String> unshadowableNames = ImmutableSet.of("arguments");
private static void maybeRename(JVariable variable) {
// In normal scenarios local variables from our Java programs will shadow existing JavaScript
// variables. The only exception is the implicit "arguments" variable which should not be
// shadowed.
// There is no need to care about collisions here as the JVariable object is what defines a
// variable, not its name; FixNameClashesVisitor, run immediately after, will ensure that names
// do not clash.
if (unshadowableNames.contains(variable.getName())) {
variable.setName("_" + variable.getName());
}
}
private NameClashesFixer() {
}
}