From 5eeeb403d0a551bebb2583727b275bd2bde5512c Mon Sep 17 00:00:00 2001 From: Eugene Zhuravlev Date: Thu, 16 Feb 2012 17:04:43 +0100 Subject: [PATCH] cross-compilation: when instrumenting notnulls, resolve agains target jdk classes rather than self runtime --- .../java/InstrumentationClassFinder.java | 192 +++++++++++++++ .../jps/incremental/java/JavaBuilder.java | 232 +++++------------- .../jps/incremental/java/OutputFilesSink.java | 141 +++++++++++ .../src/com/intellij/util/lang/ClassPath.java | 2 +- 4 files changed, 401 insertions(+), 166 deletions(-) create mode 100644 jps/jps-builders/src/org/jetbrains/jps/incremental/java/InstrumentationClassFinder.java create mode 100644 jps/jps-builders/src/org/jetbrains/jps/incremental/java/OutputFilesSink.java diff --git a/jps/jps-builders/src/org/jetbrains/jps/incremental/java/InstrumentationClassFinder.java b/jps/jps-builders/src/org/jetbrains/jps/incremental/java/InstrumentationClassFinder.java new file mode 100644 index 000000000000..07bf43b5ee98 --- /dev/null +++ b/jps/jps-builders/src/org/jetbrains/jps/incremental/java/InstrumentationClassFinder.java @@ -0,0 +1,192 @@ +package org.jetbrains.jps.incremental.java; + +import com.intellij.util.lang.ClassPath; +import org.jetbrains.jps.javac.OutputFileObject; +import org.objectweb.asm.Attribute; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.commons.EmptyVisitor; +import sun.misc.Resource; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; + +/** + * @author Eugene Zhuravlev + * Date: 2/16/12 + */ +class InstrumentationClassFinder { + private static final PseudoClass[] EMPTY_PSEUDOCLASS_ARRAY = new PseudoClass[0]; + private static final String CLASS_RESOURCE_EXTENSION = ".class"; + private final Map myLoaded = new HashMap(); // className -> class object + private final ClassPath myPlatformClasspath; + private final ClassPath myClasspath; + private final OutputFilesSink myCompiled; + + public InstrumentationClassFinder(final URL[] platformClasspath, final URL[] classpath, OutputFilesSink compiled) { + myCompiled = compiled; + myPlatformClasspath = new ClassPath(platformClasspath, true, false); + myClasspath = new ClassPath(classpath, true, false); + } + + public PseudoClass loadClass(final String internalName) throws IOException, ClassNotFoundException{ + final PseudoClass aClass = myLoaded.get(internalName); + if (aClass != null) { + return aClass; + } + + InputStream is = null; + // first look into platformCp + final String resourceName = internalName + CLASS_RESOURCE_EXTENSION; + Resource resource = myPlatformClasspath.getResource(resourceName, false); + if (resource != null) { + is = resource.getInputStream(); + } + // second look into memory and classspath + if (is == null) { + final OutputFileObject.Content content = myCompiled.lookupClassBytes(internalName.replace("/", ".")); + if (content != null) { + is = new ByteArrayInputStream(content.getBuffer(), content.getOffset(), content.getLength()); + } + } + + if (is == null) { + resource = myClasspath.getResource(resourceName, false); + if (resource != null) { + is = resource.getInputStream(); + } + } + + if (is == null) { + throw new ClassNotFoundException("Class not found: " + internalName); + } + + try { + final PseudoClass result = loadPseudoClass(is); + myLoaded.put(internalName, result); + return result; + } + finally { + is.close(); + } + } + + private PseudoClass loadPseudoClass(InputStream is) throws IOException { + final ClassReader reader = new ClassReader(is); + final V visitor = new V(); + + reader.accept(visitor, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES); + + return new PseudoClass(visitor.myName, visitor.mySuperclassName, visitor.myInterfaces, visitor.myIsInterface); + } + + public final class PseudoClass { + private final String myName; + private final String mySuperClass; + private final String[] myInterfaces; + private final boolean isInterface; + + private PseudoClass(final String name, final String superClass, final String[] interfaces, final boolean anInterface) { + myName = name; + mySuperClass = superClass; + myInterfaces = interfaces; + isInterface = anInterface; + } + + public PseudoClass getSuperClass() throws IOException, ClassNotFoundException { + final String superClass = mySuperClass; + return superClass != null? loadClass(superClass) : null; + } + + private PseudoClass[] getInterfaces() throws IOException, ClassNotFoundException { + if (myInterfaces == null) { + return EMPTY_PSEUDOCLASS_ARRAY; + } + + final PseudoClass[] result = new PseudoClass[myInterfaces.length]; + + for (int i = 0; i < result.length; i++) { + result[i] = loadClass(myInterfaces[i]); + } + + return result; + } + + public boolean equals (final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + return getName().equals(((PseudoClass)o).getName()); + } + + private boolean isSubclassOf(final PseudoClass x) throws IOException, ClassNotFoundException { + for (PseudoClass c = this; c != null; c = c.getSuperClass()) { + final PseudoClass superClass = c.getSuperClass(); + + if (superClass != null && superClass.equals(x)) { + return true; + } + } + + return false; + } + + private boolean implementsInterface(final PseudoClass x) throws IOException, ClassNotFoundException { + for (PseudoClass c = this; c != null; c = c.getSuperClass()) { + final PseudoClass[] tis = c.getInterfaces(); + for (final PseudoClass ti : tis) { + if (ti.equals(x) || ti.implementsInterface(x)) { + return true; + } + } + } + return false; + } + + public boolean isAssignableFrom(final PseudoClass x) throws IOException, ClassNotFoundException { + if (this.equals(x)) { + return true; + } + if (x.isSubclassOf(this)) { + return true; + } + if (x.implementsInterface(this)) { + return true; + } + if (x.isInterface() && getName().equals("java/lang/Object")) { + return true; + } + return false; + } + + public boolean isInterface() { + return isInterface; + } + + public String getName() { + return myName; + } + } + + private static class V extends EmptyVisitor { + public String mySuperclassName = null; + public String[] myInterfaces = null; + public String myName = null; + public boolean myIsInterface = false; + + public void visitAttribute(Attribute attr) { + super.visitAttribute(attr); + } + + public void visit(int version, int access, String pName, String signature, String pSuperName, String[] pInterfaces) { + mySuperclassName = pSuperName; + myInterfaces = pInterfaces; + myName = pName; + myIsInterface = (access & Opcodes.ACC_INTERFACE) > 0; + } + } +} diff --git a/jps/jps-builders/src/org/jetbrains/jps/incremental/java/JavaBuilder.java b/jps/jps-builders/src/org/jetbrains/jps/incremental/java/JavaBuilder.java index d37ffe7dd213..8fd4a694af7e 100644 --- a/jps/jps-builders/src/org/jetbrains/jps/incremental/java/JavaBuilder.java +++ b/jps/jps-builders/src/org/jetbrains/jps/incremental/java/JavaBuilder.java @@ -25,7 +25,6 @@ import org.jetbrains.jps.api.RequestFuture; import org.jetbrains.jps.incremental.*; import org.jetbrains.jps.incremental.messages.BuildMessage; import org.jetbrains.jps.incremental.messages.CompilerMessage; -import org.jetbrains.jps.incremental.messages.FileGeneratedEvent; import org.jetbrains.jps.incremental.messages.ProgressMessage; import org.jetbrains.jps.incremental.storage.BuildDataManager; import org.jetbrains.jps.incremental.storage.SourceToFormMapping; @@ -268,14 +267,15 @@ public class JavaBuilder extends ModuleLevelBuilder { context.checkCanceled(); - final ClassLoader compiledClassesLoader = createInstrumentationClassLoader(classpath, platformCp, chunkSourcePath, outputSink); + final InstrumentationClassFinder finder = createInstrumentationClassFinder(platformCp, classpath, outputSink); context.checkCanceled(); if (!forms.isEmpty()) { try { context.processMessage(new ProgressMessage("Instrumenting forms [" + chunkName + "]")); - instrumentForms(context, chunk, chunkSourcePath, compiledClassesLoader, forms, outputSink); + final ClassLoader loader = createInstrumentationClassLoader(platformCp, classpath, chunkSourcePath, outputSink); + instrumentForms(context, chunk, chunkSourcePath, loader, finder, forms, outputSink); } finally { context.processMessage(new ProgressMessage("Finished instrumenting forms [" + chunkName + "]")); @@ -287,7 +287,7 @@ public class JavaBuilder extends ModuleLevelBuilder { if (addNotNullAssertions) { try { context.processMessage(new ProgressMessage("Adding NotNull assertions [" + chunkName + "]")); - instrumentNotNull(context, outputSink, compiledClassesLoader); + instrumentNotNull(context, outputSink, finder); } finally { context.processMessage(new ProgressMessage("Finished adding NotNull assertions [" + chunkName + "]")); @@ -485,8 +485,37 @@ public class JavaBuilder extends ModuleLevelBuilder { return heapSize; } - private static ClassLoader createInstrumentationClassLoader(Collection classpath, - Collection platformCp, + private static InstrumentationClassFinder createInstrumentationClassFinder(Collection platformCp, + Collection classpath, + OutputFilesSink outputSink) throws MalformedURLException { + final URL[] platformUrls = new URL[platformCp.size()]; + int index = 0; + for (File file : platformCp) { + platformUrls[index++] = file.toURI().toURL(); + } + + final URL[] urls = new URL[classpath.size() + 1]; + index = 0; + for (File file : classpath) { + urls[index++] = file.toURI().toURL(); + } + urls[index++] = getResourcePath(GridConstraints.class).toURI().toURL(); // forms_rt.jar + //urls.add(getResourcePath(CellConstraints.class).toURI().toURL()); // jgoodies-forms + + final Map compiled = new HashMap(); + for (OutputFileObject fileObject : outputSink.getFileObjects()) { + final String name = fileObject.getClassName(); + if (name != null) { + final OutputFileObject.Content content = fileObject.getContent(); + if (content != null) { + compiled.put(name, content.toByteArray()); + } + } + } + return new InstrumentationClassFinder(platformUrls, urls, outputSink); + } + + private static ClassLoader createInstrumentationClassLoader(Collection platformCp, Collection classpath, Map chunkSourcePath, OutputFilesSink outputSink) throws MalformedURLException { final List urls = new ArrayList(); @@ -605,14 +634,14 @@ public class JavaBuilder extends ModuleLevelBuilder { } // todo: probably instrument other NotNull-like annotations defined in project settings? - private static void instrumentNotNull(CompileContext context, OutputFilesSink sink, final ClassLoader loader) { + private static void instrumentNotNull(CompileContext context, OutputFilesSink sink, final InstrumentationClassFinder finder) { for (final OutputFileObject fileObject : sink.getFileObjects()) { final OutputFileObject.Content originalContent = fileObject.getContent(); final ClassReader reader = new ClassReader(originalContent.getBuffer(), originalContent.getOffset(), originalContent.getLength()); final int version = getClassFileVersion(reader); if (version >= Opcodes.V1_5) { boolean success = false; - final ClassWriter writer = new InstrumenterClassWriter(getAsmClassWriterFlags(version), loader); + final ClassWriter writer = new InstrumenterClassWriter(getAsmClassWriterFlags(version), finder); try { final NotNullVerifyingInstrumenter instrumenter = new NotNullVerifyingInstrumenter(writer); reader.accept(instrumenter, 0); @@ -645,7 +674,7 @@ public class JavaBuilder extends ModuleLevelBuilder { ModuleChunk chunk, final Map chunkSourcePath, final ClassLoader loader, - Collection formsToInstrument, + final InstrumentationClassFinder finder, Collection formsToInstrument, OutputFilesSink outputSink) throws ProjectBuildException { final Map class2form = new HashMap(); @@ -686,13 +715,12 @@ public class JavaBuilder extends ModuleLevelBuilder { final File alreadyProcessedForm = class2form.get(classToBind); if (alreadyProcessedForm != null) { - context.processMessage(new CompilerMessage(BUILDER_NAME, BuildMessage.Kind.WARNING, formFile.getAbsolutePath() + - ": The form is bound to the class " + - classToBind + - ".\nAnother form " + - alreadyProcessedForm.getAbsolutePath() + - " is also bound to this class", - formFile.getAbsolutePath())); + context.processMessage( + new CompilerMessage( + BUILDER_NAME, BuildMessage.Kind.WARNING, + formFile.getAbsolutePath() + ": The form is bound to the class " + classToBind + ".\nAnother form " + alreadyProcessedForm.getAbsolutePath() + " is also bound to this class", + formFile.getAbsolutePath()) + ); continue; } @@ -705,7 +733,7 @@ public class JavaBuilder extends ModuleLevelBuilder { new ClassReader(originalContent.getBuffer(), originalContent.getOffset(), originalContent.getLength()); final int version = getClassFileVersion(classReader); - final InstrumenterClassWriter classWriter = new InstrumenterClassWriter(getAsmClassWriterFlags(version), loader); + final InstrumenterClassWriter classWriter = new InstrumenterClassWriter(getAsmClassWriterFlags(version), finder); final AsmCodeGenerator codeGenerator = new AsmCodeGenerator(rootContainer, loader, nestedFormsLoader, false, classWriter); final byte[] patchedBytes = codeGenerator.patchClass(classReader); if (patchedBytes != null) { @@ -858,165 +886,39 @@ public class JavaBuilder extends ModuleLevelBuilder { } } - private static class OutputFilesSink implements OutputFileConsumer { - private final CompileContext myContext; - private final Set mySuccessfullyCompiled = new HashSet(); - private final Set myProblematic = new HashSet(); - private final List myFileObjects = new ArrayList(); - private final Map myCompiledClasses = new HashMap(); - - public OutputFilesSink(CompileContext context) { - myContext = context; - } - - public void save(final @NotNull OutputFileObject fileObject) { - final String className = fileObject.getClassName(); - if (className != null) { - final OutputFileObject.Content content = fileObject.getContent(); - if (content != null) { - synchronized (myCompiledClasses) { - myCompiledClasses.put(className, fileObject); - } - } - } - - synchronized (myFileObjects) { - myFileObjects.add(fileObject); - } - } - - @Nullable - public OutputFileObject.Content lookupClassBytes(String className) { - synchronized (myCompiledClasses) { - final OutputFileObject object = myCompiledClasses.get(className); - return object != null ? object.getContent() : null; - } - } - - public List getFileObjects() { - return Collections.unmodifiableList(myFileObjects); - } - - public void writePendingData() { - try { - if (!myFileObjects.isEmpty()) { - final FileGeneratedEvent event = new FileGeneratedEvent(); - try { - for (OutputFileObject fileObject : myFileObjects) { - try { - writeToDisk(fileObject); - final File rootFile = fileObject.getOutputRoot(); - if (rootFile != null) { - event.add(rootFile.getPath(), fileObject.getRelativePath()); - } - } - catch (IOException e) { - myContext.processMessage(new CompilerMessage(BUILDER_NAME, BuildMessage.Kind.ERROR, e.getMessage())); - } - } - } - finally { - myContext.processMessage(event); - } - } - } - finally { - myFileObjects.clear(); - myCompiledClasses.clear(); - } - } - - public Set getSuccessfullyCompiled() { - return Collections.unmodifiableSet(mySuccessfullyCompiled); - } - - private void writeToDisk(@NotNull OutputFileObject fileObject) throws IOException { - final File file = fileObject.getFile(); - final OutputFileObject.Content content = fileObject.getContent(); - if (content == null) { - throw new IOException("Missing content for file " + file); - } - - try { - _writeToFile(file, content); - } - catch (IOException e) { - // assuming the reason is non-existing parent - final File parentFile = file.getParentFile(); - if (parentFile == null) { - throw e; - } - if (!parentFile.mkdirs()) { - throw e; - } - // second attempt - _writeToFile(file, content); - } - - final File source = fileObject.getSourceFile(); - if (source != null && !myProblematic.contains(source)) { - mySuccessfullyCompiled.add(source); - final String className = fileObject.getClassName(); - if (className != null) { - myContext.processMessage(new ProgressMessage("Compiled " + className)); - } - } - } - - private static void _writeToFile(final File file, OutputFileObject.Content content) throws IOException { - final OutputStream stream = new BufferedOutputStream(new FileOutputStream(file)); - try { - stream.write(content.getBuffer(), content.getOffset(), content.getLength()); - } - finally { - stream.close(); - } - } - - public void markError(OutputFileObject outputClassFile) { - final File source = outputClassFile.getSourceFile(); - if (source != null) { - myProblematic.add(source); - } - } - } - public static class InstrumenterClassWriter extends ClassWriter { - private final ClassLoader myClassLoader; + private final InstrumentationClassFinder myFinder; - public InstrumenterClassWriter(int flags, final ClassLoader pseudoLoader) { + public InstrumenterClassWriter(int flags, final InstrumentationClassFinder finder) { super(flags); - myClassLoader = pseudoLoader; + myFinder = finder; } protected String getCommonSuperClass(final String type1, final String type2) { - Class c, d; try { - //c = Class.forName(type1.replace('/', '.'), true, myClassLoader); - //d = Class.forName(type2.replace('/', '.'), true, myClassLoader); - c = myClassLoader.loadClass(type1.replace('/', '.')); - d = myClassLoader.loadClass(type2.replace('/', '.')); + final InstrumentationClassFinder.PseudoClass cls1 = myFinder.loadClass(type1); + final InstrumentationClassFinder.PseudoClass cls2 = myFinder.loadClass(type2); + if (cls1.isAssignableFrom(cls2)) { + return cls1.getName(); + } + if (cls2.isAssignableFrom(cls1)) { + return cls2.getName(); + } + if (cls1.isInterface() || cls2.isInterface()) { + return "java/lang/Object"; + } + else { + InstrumentationClassFinder.PseudoClass c = cls1; + do { + c = c.getSuperClass(); + } + while (!c.isAssignableFrom(cls2)); + return c.getName(); + } } catch (Exception e) { throw new RuntimeException(e.toString(), e); } - if (c.isAssignableFrom(d)) { - return type1; - } - if (d.isAssignableFrom(c)) { - return type2; - } - if (c.isInterface() || d.isInterface()) { - return "java/lang/Object"; - } - else { - do { - c = c.getSuperclass(); - } - while (!c.isAssignableFrom(d)); - - return c.getName().replace('.', '/'); - } } } diff --git a/jps/jps-builders/src/org/jetbrains/jps/incremental/java/OutputFilesSink.java b/jps/jps-builders/src/org/jetbrains/jps/incremental/java/OutputFilesSink.java new file mode 100644 index 000000000000..ea1a68e72b27 --- /dev/null +++ b/jps/jps-builders/src/org/jetbrains/jps/incremental/java/OutputFilesSink.java @@ -0,0 +1,141 @@ +package org.jetbrains.jps.incremental.java; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.jps.incremental.CompileContext; +import org.jetbrains.jps.incremental.messages.BuildMessage; +import org.jetbrains.jps.incremental.messages.CompilerMessage; +import org.jetbrains.jps.incremental.messages.FileGeneratedEvent; +import org.jetbrains.jps.incremental.messages.ProgressMessage; +import org.jetbrains.jps.javac.OutputFileConsumer; +import org.jetbrains.jps.javac.OutputFileObject; + +import java.io.*; +import java.util.*; + +/** +* @author Eugene Zhuravlev +* Date: 2/16/12 +*/ +class OutputFilesSink implements OutputFileConsumer { + private final CompileContext myContext; + private final Set mySuccessfullyCompiled = new HashSet(); + private final Set myProblematic = new HashSet(); + private final List myFileObjects = new ArrayList(); + private final Map myCompiledClasses = new HashMap(); + + public OutputFilesSink(CompileContext context) { + myContext = context; + } + + public void save(final @NotNull OutputFileObject fileObject) { + final String className = fileObject.getClassName(); + if (className != null) { + final OutputFileObject.Content content = fileObject.getContent(); + if (content != null) { + synchronized (myCompiledClasses) { + myCompiledClasses.put(className, fileObject); + } + } + } + + synchronized (myFileObjects) { + myFileObjects.add(fileObject); + } + } + + @Nullable + public OutputFileObject.Content lookupClassBytes(String className) { + synchronized (myCompiledClasses) { + final OutputFileObject object = myCompiledClasses.get(className); + return object != null ? object.getContent() : null; + } + } + + public List getFileObjects() { + return Collections.unmodifiableList(myFileObjects); + } + + public void writePendingData() { + try { + if (!myFileObjects.isEmpty()) { + final FileGeneratedEvent event = new FileGeneratedEvent(); + try { + for (OutputFileObject fileObject : myFileObjects) { + try { + writeToDisk(fileObject); + final File rootFile = fileObject.getOutputRoot(); + if (rootFile != null) { + event.add(rootFile.getPath(), fileObject.getRelativePath()); + } + } + catch (IOException e) { + myContext.processMessage(new CompilerMessage(JavaBuilder.BUILDER_NAME, BuildMessage.Kind.ERROR, e.getMessage())); + } + } + } + finally { + myContext.processMessage(event); + } + } + } + finally { + myFileObjects.clear(); + myCompiledClasses.clear(); + } + } + + public Set getSuccessfullyCompiled() { + return Collections.unmodifiableSet(mySuccessfullyCompiled); + } + + private void writeToDisk(@NotNull OutputFileObject fileObject) throws IOException { + final File file = fileObject.getFile(); + final OutputFileObject.Content content = fileObject.getContent(); + if (content == null) { + throw new IOException("Missing content for file " + file); + } + + try { + _writeToFile(file, content); + } + catch (IOException e) { + // assuming the reason is non-existing parent + final File parentFile = file.getParentFile(); + if (parentFile == null) { + throw e; + } + if (!parentFile.mkdirs()) { + throw e; + } + // second attempt + _writeToFile(file, content); + } + + final File source = fileObject.getSourceFile(); + if (source != null && !myProblematic.contains(source)) { + mySuccessfullyCompiled.add(source); + final String className = fileObject.getClassName(); + if (className != null) { + myContext.processMessage(new ProgressMessage("Compiled " + className)); + } + } + } + + private static void _writeToFile(final File file, OutputFileObject.Content content) throws IOException { + final OutputStream stream = new BufferedOutputStream(new FileOutputStream(file)); + try { + stream.write(content.getBuffer(), content.getOffset(), content.getLength()); + } + finally { + stream.close(); + } + } + + public void markError(OutputFileObject outputClassFile) { + final File source = outputClassFile.getSourceFile(); + if (source != null) { + myProblematic.add(source); + } + } +} diff --git a/platform/util/src/com/intellij/util/lang/ClassPath.java b/platform/util/src/com/intellij/util/lang/ClassPath.java index 563517ee623e..fd772093d215 100644 --- a/platform/util/src/com/intellij/util/lang/ClassPath.java +++ b/platform/util/src/com/intellij/util/lang/ClassPath.java @@ -35,7 +35,7 @@ import java.net.URISyntaxException; import java.net.URL; import java.util.*; -class ClassPath { +public class ClassPath { private final Stack myUrls = new Stack(); private final ArrayList myLoaders = new ArrayList(); private final HashMap myLoadersMap = new HashMap();