| /* |
| * Copyright 2008 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.javac; |
| |
| import com.google.gwt.core.ext.TreeLogger; |
| import com.google.gwt.dev.javac.asmbridge.EmptyVisitor; |
| import com.google.gwt.dev.jjs.ast.JDeclaredType; |
| import com.google.gwt.dev.jjs.ast.JProgram; |
| import com.google.gwt.dev.util.DiskCache; |
| import com.google.gwt.dev.util.StringInterningObjectInputStream; |
| import com.google.gwt.dev.util.Util; |
| import com.google.gwt.dev.util.collect.HashMap; |
| |
| import org.eclipse.jdt.core.compiler.CategorizedProblem; |
| import org.objectweb.asm.ClassReader; |
| import org.objectweb.asm.Opcodes; |
| |
| import java.io.ByteArrayInputStream; |
| import java.io.IOException; |
| import java.io.ObjectInputStream; |
| import java.io.Serializable; |
| import java.net.URL; |
| import java.net.URLConnection; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.Comparator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.regex.Pattern; |
| |
| /** |
| * Encapsulates the state of a single active compilation unit in a particular |
| * module. State is accumulated throughout the life cycle of the containing |
| * module and may be invalidated at certain times and recomputed. |
| */ |
| public abstract class CompilationUnit implements Serializable { |
| |
| /** |
| * Encapsulates the functionality to find all nested classes of this class |
| * that have compiler-generated names. All class bytes are loaded from the |
| * disk and then analyzed using ASM. |
| */ |
| static class GeneratedClassnameFinder { |
| private static class AnonymousClassVisitor extends EmptyVisitor { |
| /* |
| * array of classNames of inner clases that aren't synthetic classes. |
| */ |
| List<String> classNames = new ArrayList<String>(); |
| |
| int expectCode; |
| int sawCode; |
| |
| public AnonymousClassVisitor() { |
| |
| this.mv = new org.objectweb.asm.MethodVisitor(Opcodes.ASM7, this.mv) { |
| @Override |
| public void visitCode() { |
| ++sawCode; |
| } |
| }; |
| } |
| public List<String> getInnerClassNames() { |
| return classNames; |
| } |
| |
| /** |
| * Return whether or not the class file visited was well-formed. |
| * Currently, this only checks that all non-abstract, non-native methods |
| * have a Code attribute. |
| */ |
| public boolean isWellFormed() { |
| return expectCode == sawCode; |
| } |
| |
| @Override |
| public void visitInnerClass(String name, String outerName, String innerName, int access) { |
| if ((access & Opcodes.ACC_SYNTHETIC) == 0) { |
| classNames.add(name); |
| } |
| } |
| |
| @Override |
| public org.objectweb.asm.MethodVisitor visitMethod(int access, String name, String desc, |
| String signature, |
| String[] exceptions) { |
| if ((access & (Opcodes.ACC_ABSTRACT | Opcodes.ACC_NATIVE)) == 0) { |
| ++expectCode; |
| } |
| return super.visitMethod(access, name, desc, signature, exceptions); |
| } |
| } |
| |
| private final List<String> classesToScan; |
| private final TreeLogger logger; |
| private final String mainClass; |
| private String mainUrlBase = null; |
| |
| GeneratedClassnameFinder(TreeLogger logger, String mainClass) { |
| assert mainClass != null; |
| this.mainClass = mainClass; |
| classesToScan = new ArrayList<String>(); |
| classesToScan.add(mainClass); |
| this.logger = logger; |
| } |
| |
| List<String> getClassNames() { |
| // using a list because presumably there will not be many generated |
| // classes |
| List<String> allGeneratedClasses = new ArrayList<String>(); |
| for (int i = 0; i < classesToScan.size(); i++) { |
| String lookupName = classesToScan.get(i); |
| byte classBytes[] = getClassBytes(lookupName); |
| if (classBytes == null) { |
| /* |
| * Weird case: javac might generate a name and reference the class in |
| * the bytecode but decide later that the class is unnecessary. In the |
| * bytecode, a null is passed for the class. |
| */ |
| continue; |
| } |
| |
| AnonymousClassVisitor cv = new AnonymousClassVisitor(); |
| new ClassReader(classBytes).accept(cv, 0); |
| if (!cv.isWellFormed()) { |
| /* |
| * Weird case #2: As of OpenJDK 7, javac in the above case now does |
| * generate a class file, but an incomplete one that fails to load |
| * with a ClassFormatError "Absent Code attribute in method that |
| * is not native or abstract in class file" error. |
| */ |
| continue; |
| } |
| |
| /* |
| * Add the class to the list only if it can be loaded to get around the |
| * javac weirdness issue where javac refers a class but does not |
| * generate it. |
| */ |
| if (isClassnameGenerated(lookupName) && !allGeneratedClasses.contains(lookupName)) { |
| allGeneratedClasses.add(lookupName); |
| } |
| List<String> innerClasses = cv.getInnerClassNames(); |
| for (String innerClass : innerClasses) { |
| // The innerClass has to be an inner class of the lookupName |
| if (!innerClass.startsWith(mainClass + "$")) { |
| continue; |
| } |
| /* |
| * TODO (amitmanjhi): consider making this a Set if necessary for |
| * performance |
| */ |
| // add the class to classes |
| if (!classesToScan.contains(innerClass)) { |
| classesToScan.add(innerClass); |
| } |
| } |
| } |
| Collections.sort(allGeneratedClasses, new GeneratedClassnameComparator()); |
| return allGeneratedClasses; |
| } |
| |
| /* |
| * Load classBytes from disk. Check if the classBytes are loaded from the |
| * same location as the location of the mainClass. |
| */ |
| private byte[] getClassBytes(String slashedName) { |
| URL url = Thread.currentThread().getContextClassLoader().getResource(slashedName + ".class"); |
| if (url == null) { |
| if (logger.isLoggable(TreeLogger.DEBUG)) { |
| logger.log(TreeLogger.DEBUG, "Unable to find " + slashedName + " on the classPath"); |
| } |
| return null; |
| } |
| String urlStr = url.toExternalForm(); |
| if (slashedName.equals(mainClass)) { |
| // initialize the mainUrlBase for later use. |
| mainUrlBase = urlStr.substring(0, urlStr.lastIndexOf('/')); |
| } else { |
| assert mainUrlBase != null; |
| if (!mainUrlBase.equals(urlStr.substring(0, urlStr.lastIndexOf('/')))) { |
| if (logger.isLoggable(TreeLogger.DEBUG)) { |
| logger.log(TreeLogger.DEBUG, "Found " + slashedName + " at " + urlStr |
| + " The base location is different from that of " + mainUrlBase + " Not loading"); |
| } |
| return null; |
| } |
| } |
| |
| // url != null, we found it on the class path. |
| try { |
| URLConnection conn = url.openConnection(); |
| return Util.readURLConnectionAsBytes(conn); |
| } catch (IOException ignored) { |
| if (logger.isLoggable(TreeLogger.DEBUG)) { |
| logger.log(TreeLogger.DEBUG, "Unable to load " + urlStr + ", in trying to load " |
| + slashedName); |
| } |
| // Fall through. |
| } |
| return null; |
| } |
| } |
| |
| public static final Comparator<CompilationUnit> COMPARATOR = new Comparator<CompilationUnit>() { |
| @Override |
| public int compare(CompilationUnit o1, CompilationUnit o2) { |
| return o1.getResourcePath().compareTo(o2.getResourcePath()); |
| } |
| }; |
| |
| protected static final DiskCache diskCache = DiskCache.INSTANCE; |
| |
| private static final Pattern GENERATED_CLASSNAME_PATTERN = Pattern.compile(".+\\$\\d.*"); |
| |
| /** |
| * Checks if the class names is generated. Accepts any classes whose names |
| * match .+$\d.* (handling named classes within anonymous classes and multiple |
| * named classes of the same name in a class, but in different methods). |
| * Checks if the class or any of its enclosing classes are anonymous or |
| * synthetic. |
| * <p> |
| * If new compilers have different conventions for anonymous and synthetic |
| * classes, this code needs to be updated. |
| * </p> |
| * |
| * @param className name of the class to be checked. |
| * @return true iff class or any of its enclosing classes are anonymous or |
| * synthetic. |
| */ |
| @Deprecated |
| public static boolean isClassnameGenerated(String className) { |
| return GENERATED_CLASSNAME_PATTERN.matcher(className).matches(); |
| } |
| |
| /** |
| * Map from the className in javac to the className in jdt. String represents |
| * the part of className after the compilation unit name. Emma-specific. |
| */ |
| private transient Map<String, String> anonymousClassMap = null; |
| |
| /** |
| * Returns the unit as an instance of {@link CachedCompilationUnit}, making a |
| * copy if necessary. |
| */ |
| public abstract CachedCompilationUnit asCachedCompilationUnit(); |
| |
| @Deprecated |
| public final boolean constructAnonymousClassMappings(TreeLogger logger) { |
| /* |
| * Check if the unit has one or more classes with generated names. 'javac' |
| * below refers to the compiler that was used to compile the java files on |
| * disk. Returns true if our heuristic for constructing the anonymous class |
| * mappings worked. |
| */ |
| anonymousClassMap = new HashMap<String, String>(); |
| for (String topLevelClass : getTopLevelClasses()) { |
| // Generate a mapping for each top-level class separately |
| List<String> javacClasses = |
| new GeneratedClassnameFinder(logger, topLevelClass).getClassNames(); |
| List<String> jdtClasses = getJdtClassNames(topLevelClass); |
| if (javacClasses.size() != jdtClasses.size()) { |
| anonymousClassMap = Collections.emptyMap(); |
| return false; |
| } |
| int size = javacClasses.size(); |
| for (int i = 0; i < size; i++) { |
| if (!javacClasses.get(i).equals(jdtClasses.get(i))) { |
| anonymousClassMap.put(javacClasses.get(i), jdtClasses.get(i)); |
| } |
| } |
| } |
| return true; |
| } |
| |
| @Deprecated |
| public final boolean createdClassMapping() { |
| return anonymousClassMap != null; |
| } |
| |
| /** |
| * Overridden to finalize; always returns object identity. |
| */ |
| @Override |
| public final boolean equals(Object obj) { |
| return super.equals(obj); |
| } |
| |
| @Deprecated |
| public final Map<String, String> getAnonymousClassMap() { |
| /* |
| * Return an empty map so that class-rewriter does not need to check for |
| * null. A null value indicates that anonymousClassMap was never created |
| * which is the case for many units. An example is a class containing jsni |
| * units but no inner classes. |
| */ |
| if (anonymousClassMap == null) { |
| return Collections.emptyMap(); |
| } |
| return anonymousClassMap; |
| } |
| |
| /** |
| * Returns all contained classes. |
| */ |
| public abstract Collection<CompiledClass> getCompiledClasses(); |
| |
| public abstract List<JsniMethod> getJsniMethods(); |
| |
| /** |
| * Returns the last modified time of the compilation unit. |
| */ |
| public abstract long getLastModified(); |
| |
| /** |
| * @return a way to lookup method argument names for this compilation unit. |
| */ |
| public abstract MethodArgNamesLookup getMethodArgs(); |
| |
| /** |
| * This is the resource location from the classpath or some deterministic |
| * virtual location (in the case of generators or mock data) where the source |
| * for this unit originated. This should be unique for each unit compiled to |
| * create a module. |
| * |
| * @see com.google.gwt.dev.resource.Resource#getLocation() |
| */ |
| public abstract String getResourceLocation(); |
| |
| /** |
| * Returns the full abstract path of the resource. If a resource has been |
| * re-rooted, this path should include any path prefix that was stripped. |
| * |
| * @see com.google.gwt.dev.resource.Resource#getPath() |
| * @see com.google.gwt.dev.resource.Resource#getPathPrefix() |
| */ |
| public abstract String getResourcePath(); |
| |
| /** |
| * Returns the source name of the top level public type. |
| */ |
| public abstract String getTypeName(); |
| |
| /** |
| * Returns a copy of the GWT AST types in this unit.<br /> |
| * |
| * Callers are free to modify the returned type instances without worrying about modifying the |
| * original CompilationUnit instances. It is this protection that makes it safe to reuse a single |
| * UnitCache and the same CompilationUnit instances across multiple compiles within the same |
| * process. |
| */ |
| public List<JDeclaredType> getTypes() { |
| try { |
| byte[] bytes = getTypesSerialized(); |
| ObjectInputStream ois = new StringInterningObjectInputStream(new ByteArrayInputStream(bytes)); |
| return JProgram.deserializeTypes(ois); |
| } catch (IOException e) { |
| throw new RuntimeException("Unexpected IOException on in-memory stream", e); |
| } catch (ClassNotFoundException e) { |
| throw new RuntimeException("Unexpected error deserializing AST for '" + getTypeName() + "'", |
| e); |
| } |
| } |
| |
| /** |
| * Returns the GWT AST types in this unit in serialized form. |
| */ |
| public abstract byte[] getTypesSerialized(); |
| |
| @Deprecated |
| public final boolean hasAnonymousClasses() { |
| for (CompiledClass cc : getCompiledClasses()) { |
| if (isAnonymousClass(cc)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Overridden to finalize; always returns identity hash code. |
| */ |
| @Override |
| public final int hashCode() { |
| return super.hashCode(); |
| } |
| |
| /** |
| * Returns <code>true</code> if this unit had errors. |
| */ |
| public abstract boolean isError(); |
| |
| /** |
| * Returns <code>true</code> if this unit was generated by a |
| * {@link com.google.gwt.core.ext.Generator}. |
| */ |
| @Deprecated |
| public abstract boolean isGenerated(); |
| |
| /** |
| * |
| * @return true if the Compilation Unit is from a super-source. |
| */ |
| @Deprecated |
| public abstract boolean isSuperSource(); |
| |
| /** |
| * Overridden to finalize; always returns {@link #getResourceLocation()}. |
| */ |
| @Override |
| public final String toString() { |
| return getResourceLocation(); |
| } |
| |
| /** |
| * The canonical serialized form of a CompilatinUnit is |
| * {@link CachedCompilationUnit}. |
| */ |
| protected final Object writeReplace() { |
| return asCachedCompilationUnit(); |
| } |
| |
| /** |
| * Returns the content ID for the source with which this unit was compiled. |
| */ |
| abstract ContentId getContentId(); |
| |
| /** |
| * The set of dependencies on other classes. |
| */ |
| abstract Dependencies getDependencies(); |
| |
| abstract CategorizedProblem[] getProblems(); |
| |
| /** |
| * Returns true if this compilation unit has any JsInterop root types (i.e. type with an export or |
| * a JsType). |
| */ |
| abstract boolean hasJsInteropRootType(); |
| |
| private List<String> getJdtClassNames(String topLevelClass) { |
| List<String> classNames = new ArrayList<String>(); |
| for (CompiledClass cc : getCompiledClasses()) { |
| if (isAnonymousClass(cc) && cc.getInternalName().startsWith(topLevelClass + "$")) { |
| classNames.add(cc.getInternalName()); |
| } |
| } |
| Collections.sort(classNames, new GeneratedClassnameComparator()); |
| return classNames; |
| } |
| |
| private List<String> getTopLevelClasses() { |
| List<String> topLevelClasses = new ArrayList<String>(); |
| for (CompiledClass cc : getCompiledClasses()) { |
| if (cc.getEnclosingClass() == null) { |
| topLevelClasses.add(cc.getInternalName()); |
| } |
| } |
| return topLevelClasses; |
| } |
| |
| /** |
| * TODO(amitmanjhi): what is the difference between an anonymous and local |
| * class for our purposes? All our unit tests pass whether or not we do the |
| * additional {@link #isClassnameGenerated} check. We either need to find the |
| * real difference and add a unit test, or else simply this. |
| */ |
| private boolean isAnonymousClass(CompiledClass cc) { |
| return cc.isLocal() && isClassnameGenerated(cc.getInternalName()); |
| } |
| } |