From 11fcdbee725a3b900ed7d6b7d09ea18f6af6b961 Mon Sep 17 00:00:00 2001 From: Roel Spilker Date: Tue, 3 Aug 2010 01:04:01 +0200 Subject: Added initial support for post-compilation byte code transformations --- src/core/lombok/core/DiagnosticsReceiver.java | 30 +++++ src/core/lombok/core/LombokNode.java | 8 +- src/core/lombok/core/PostCompiler.java | 30 +++++ .../javac/apt/InterceptingJavaFileManager.java | 104 +++++++++++++++++ .../javac/apt/InterceptingJavaFileObject.java | 129 +++++++++++++++++++++ .../javac/apt/MessagerDiagnosticsReceiver.java | 43 +++++++ src/core/lombok/javac/apt/Processor.java | 38 +++++- 7 files changed, 373 insertions(+), 9 deletions(-) create mode 100644 src/core/lombok/core/DiagnosticsReceiver.java create mode 100644 src/core/lombok/core/PostCompiler.java create mode 100644 src/core/lombok/javac/apt/InterceptingJavaFileManager.java create mode 100644 src/core/lombok/javac/apt/InterceptingJavaFileObject.java create mode 100644 src/core/lombok/javac/apt/MessagerDiagnosticsReceiver.java (limited to 'src/core/lombok') diff --git a/src/core/lombok/core/DiagnosticsReceiver.java b/src/core/lombok/core/DiagnosticsReceiver.java new file mode 100644 index 00000000..cdd16bc0 --- /dev/null +++ b/src/core/lombok/core/DiagnosticsReceiver.java @@ -0,0 +1,30 @@ +/* + * Copyright © 2009-2010 Reinier Zwitserloot and Roel Spilker. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package lombok.core; + +public interface DiagnosticsReceiver { + /** Generate a compiler error on this node. */ + void addError(String message); + + /** Generate a compiler warning on this node. */ + void addWarning(String message); +} diff --git a/src/core/lombok/core/LombokNode.java b/src/core/lombok/core/LombokNode.java index 6fe37a6b..6d0f3147 100644 --- a/src/core/lombok/core/LombokNode.java +++ b/src/core/lombok/core/LombokNode.java @@ -38,7 +38,7 @@ import lombok.core.AST.Kind; * @param N The common type of all AST nodes in the internal representation of the target platform. * For example, JCTree for javac, and ASTNode for Eclipse. */ -public abstract class LombokNode, L extends LombokNode, N> { +public abstract class LombokNode, L extends LombokNode, N> implements DiagnosticsReceiver { protected final A ast; protected final Kind kind; protected final N node; @@ -316,12 +316,6 @@ public abstract class LombokNode, L extends LombokNode remaining) { + return delegate.handleOption(current, remaining); + } + + @Override public boolean hasLocation(Location location) { + return delegate.hasLocation(location); + } + + @Override public String inferBinaryName(Location location, JavaFileObject file) { + return delegate.inferBinaryName(location, file); + } + + @Override public boolean isSameFile(FileObject a, FileObject b) { + return delegate.isSameFile(a, b); + } + + @Override public Iterable list(Location location, String packageName, Set kinds, boolean recurse) throws IOException { + return delegate.list(location, packageName, kinds, recurse); + } + + @Override public int isSupportedOption(String option) { + return delegate.isSupportedOption(option); + } +} \ No newline at end of file diff --git a/src/core/lombok/javac/apt/InterceptingJavaFileObject.java b/src/core/lombok/javac/apt/InterceptingJavaFileObject.java new file mode 100644 index 00000000..1e07440a --- /dev/null +++ b/src/core/lombok/javac/apt/InterceptingJavaFileObject.java @@ -0,0 +1,129 @@ +/* + * Copyright © 2010 Reinier Zwitserloot and Roel Spilker. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package lombok.javac.apt; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Reader; +import java.io.Writer; +import java.net.URI; + +import javax.lang.model.element.Modifier; +import javax.lang.model.element.NestingKind; +import javax.tools.JavaFileObject; + +import lombok.core.DiagnosticsReceiver; +import lombok.core.PostCompiler; + +final class InterceptingJavaFileObject implements JavaFileObject { + private final JavaFileObject delegate; + private final String className; + private final DiagnosticsReceiver diagnostics; + + public InterceptingJavaFileObject(JavaFileObject original, String className, DiagnosticsReceiver diagnostics) { + this.delegate = original; + this.className = className; + this.diagnostics = diagnostics; + } + + public OutputStream openOutputStream() throws IOException { + // Open it first to make sure we throw an exception if that fails. + final OutputStream originalStream = delegate.openOutputStream(); + + return new ByteArrayOutputStream() { + @Override public void close() throws IOException { + // no need to call super + byte[] original = toByteArray(); + byte[] copy = null; + try { + copy = PostCompiler.applyTransformations(original, className, diagnostics); + } catch (Exception e) { + diagnostics.addWarning(String.format("Error during the transformation of '%s'; no post-compilation has been applied", className)); + } + + if (copy == null) { + copy = original; + } + + // Exceptions below should bubble + originalStream.write(copy); + originalStream.close(); + } + }; + } + + public Writer openWriter() throws IOException { + throw new UnsupportedOperationException("Can't use a write for class files"); +// return original.openWriter(); + } + + + + +/////////////////////// NOTHING CHANGED BELOW ////////////////////////////////////// + + public boolean delete() { + return delegate.delete(); + } + + public Modifier getAccessLevel() { + return delegate.getAccessLevel(); + } + + public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException { + return delegate.getCharContent(ignoreEncodingErrors); + } + + public Kind getKind() { + return delegate.getKind(); + } + + public long getLastModified() { + return delegate.getLastModified(); + } + + public String getName() { + return delegate.getName(); + } + + public NestingKind getNestingKind() { + return delegate.getNestingKind(); + } + + public boolean isNameCompatible(String simpleName, Kind kind) { + return delegate.isNameCompatible(simpleName, kind); + } + + public InputStream openInputStream() throws IOException { + return delegate.openInputStream(); + } + + public Reader openReader(boolean ignoreEncodingErrors) throws IOException { + return delegate.openReader(ignoreEncodingErrors); + } + + public URI toUri() { + return delegate.toUri(); + } +} diff --git a/src/core/lombok/javac/apt/MessagerDiagnosticsReceiver.java b/src/core/lombok/javac/apt/MessagerDiagnosticsReceiver.java new file mode 100644 index 00000000..5b75b3c5 --- /dev/null +++ b/src/core/lombok/javac/apt/MessagerDiagnosticsReceiver.java @@ -0,0 +1,43 @@ +/* + * Copyright © 2009-2010 Reinier Zwitserloot and Roel Spilker. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package lombok.javac.apt; + +import javax.annotation.processing.Messager; +import javax.tools.Diagnostic.Kind; + +import lombok.core.DiagnosticsReceiver; + +public class MessagerDiagnosticsReceiver implements DiagnosticsReceiver { + private final Messager messager; + + public MessagerDiagnosticsReceiver(Messager messager) { + this.messager = messager; + } + + @Override public void addWarning(String message) { + messager.printMessage(Kind.WARNING, message); + } + + @Override public void addError(String message) { + messager.printMessage(Kind.ERROR, message); + } +} \ No newline at end of file diff --git a/src/core/lombok/javac/apt/Processor.java b/src/core/lombok/javac/apt/Processor.java index 31cedf03..ab445309 100644 --- a/src/core/lombok/javac/apt/Processor.java +++ b/src/core/lombok/javac/apt/Processor.java @@ -1,5 +1,5 @@ /* - * Copyright © 2009 Reinier Zwitserloot and Roel Spilker. + * Copyright © 2009-2010 Reinier Zwitserloot and Roel Spilker. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -21,10 +21,14 @@ */ package lombok.javac.apt; +import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.util.IdentityHashMap; +import java.util.Map; import java.util.Set; import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.Messager; import javax.annotation.processing.ProcessingEnvironment; import javax.annotation.processing.RoundEnvironment; import javax.annotation.processing.SupportedAnnotationTypes; @@ -32,14 +36,17 @@ import javax.annotation.processing.SupportedSourceVersion; import javax.lang.model.SourceVersion; import javax.lang.model.element.Element; import javax.lang.model.element.TypeElement; +import javax.tools.JavaFileManager; +import lombok.Lombok; +import lombok.core.DiagnosticsReceiver; import lombok.javac.JavacTransformer; import com.sun.source.util.TreePath; import com.sun.source.util.Trees; import com.sun.tools.javac.processing.JavacProcessingEnvironment; import com.sun.tools.javac.tree.JCTree.JCCompilationUnit; - +import com.sun.tools.javac.util.Context; /** * This Annotation Processor is the standard injection mechanism for lombok-enabling the javac compiler. @@ -54,6 +61,7 @@ import com.sun.tools.javac.tree.JCTree.JCCompilationUnit; @SupportedAnnotationTypes("*") @SupportedSourceVersion(SourceVersion.RELEASE_6) public class Processor extends AbstractProcessor { + private JavacProcessingEnvironment processingEnv; private JavacTransformer transformer; private Trees trees; @@ -62,9 +70,35 @@ public class Processor extends AbstractProcessor { @Override public void init(ProcessingEnvironment procEnv) { super.init(procEnv); this.processingEnv = (JavacProcessingEnvironment) procEnv; + placePostCompileHook(); transformer = new JavacTransformer(procEnv.getMessager()); trees = Trees.instance(procEnv); } + + private void placePostCompileHook() { + Context context = processingEnv.getContext(); + + try { + Method keyMethod = Context.class.getDeclaredMethod("key", Class.class); + keyMethod.setAccessible(true); + Object key = keyMethod.invoke(context, JavaFileManager.class); + Field htField = Context.class.getDeclaredField("ht"); + htField.setAccessible(true); + @SuppressWarnings("unchecked") + Map ht = (Map) htField.get(context); + final JavaFileManager originalFiler = (JavaFileManager) ht.get(key); + + if (!(originalFiler instanceof InterceptingJavaFileManager)) { + final Messager messager = processingEnv.getMessager(); + DiagnosticsReceiver receiver = new MessagerDiagnosticsReceiver(messager); + + JavaFileManager newFiler = new InterceptingJavaFileManager(originalFiler, receiver); + ht.put(key, newFiler); + } + } catch (Exception e) { + throw Lombok.sneakyThrow(e); + } + } /** {@inheritDoc} */ @Override public boolean process(Set annotations, RoundEnvironment roundEnv) { -- cgit From 40466c3e2d7d26a43b58f008986fd0b84c986b27 Mon Sep 17 00:00:00 2001 From: Roel Spilker Date: Tue, 3 Aug 2010 02:54:03 +0200 Subject: Hey, the sneaky throws remover works, at least when run separately. --- src/core/lombok/bytecode/SneakyThrowsRemover.java | 80 ++++++++++++++++++++++ src/core/lombok/core/DiagnosticsReceiver.java | 11 +++ src/core/lombok/core/PostCompiler.java | 38 +++++++++- .../lombok/core/PostCompilerTransformation.java | 5 ++ 4 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 src/core/lombok/bytecode/SneakyThrowsRemover.java create mode 100644 src/core/lombok/core/PostCompilerTransformation.java (limited to 'src/core/lombok') diff --git a/src/core/lombok/bytecode/SneakyThrowsRemover.java b/src/core/lombok/bytecode/SneakyThrowsRemover.java new file mode 100644 index 00000000..b816cd54 --- /dev/null +++ b/src/core/lombok/bytecode/SneakyThrowsRemover.java @@ -0,0 +1,80 @@ +package lombok.bytecode; + +import lombok.core.DiagnosticsReceiver; +import lombok.core.PostCompilerTransformation; + +import org.mangosdk.spi.ProviderFor; +import org.objectweb.asm.ClassAdapter; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.MethodAdapter; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.commons.JSRInlinerAdapter; + +@ProviderFor(PostCompilerTransformation.class) +public class SneakyThrowsRemover implements PostCompilerTransformation { + private static class FixedClassWriter extends ClassWriter { + FixedClassWriter(ClassReader classReader, int flags) { + super(classReader, flags); + } + + @Override protected String getCommonSuperClass(String type1, String type2) { + //By default, ASM will attempt to live-load the class types, which will fail if meddling with classes in an + //environment with custom classloaders, such as Equinox. It's just an optimization; returning Object is always legal. + try { + return super.getCommonSuperClass(type1, type2); + } catch (Exception e) { + return "java/lang/Object"; + } + } + } + + protected byte[] fixJSRInlining(byte[] byteCode) { + ClassReader reader = new ClassReader(byteCode); + ClassWriter writer = new FixedClassWriter(reader, 0); + + ClassVisitor visitor = new ClassAdapter(writer) { + @Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { + return new JSRInlinerAdapter(super.visitMethod(access, name, desc, signature, exceptions), access, name, desc, signature, exceptions); + } + }; + + reader.accept(visitor, 0); + return writer.toByteArray(); + } + + @Override public byte[] applyTransformations(byte[] original, String className, DiagnosticsReceiver diagnostics) { + byte[] fixedByteCode = fixJSRInlining(original); + + ClassReader reader = new ClassReader(fixedByteCode); + ClassWriter writer = new FixedClassWriter(reader, ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES); + + reader.accept(new ClassAdapter(writer) { + @Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { + return new SneakyThrowsRemoverVisitor(super.visitMethod(access, name, desc, signature, exceptions)); + } + }, 0); + return writer.toByteArray(); + } + + private class SneakyThrowsRemoverVisitor extends MethodAdapter { + SneakyThrowsRemoverVisitor(MethodVisitor mv) { + super(mv); + } + + @Override public void visitMethodInsn(int opcode, String owner, String name, String desc) { + boolean hit = true; + if (hit && opcode != Opcodes.INVOKESTATIC) hit = false; + if (hit && !"sneakyThrow".equals(name)) hit = false; + if (hit && !"lombok/Lombok".equals(owner)) hit = false; + if (hit && !"(Ljava/lang/Throwable;)Ljava/lang/RuntimeException;".equals(desc)) hit = false; + if (hit) { + super.visitInsn(Opcodes.ATHROW); + } else { + super.visitMethodInsn(opcode, owner, name, desc); + } + } + } +} diff --git a/src/core/lombok/core/DiagnosticsReceiver.java b/src/core/lombok/core/DiagnosticsReceiver.java index cdd16bc0..7e9acd80 100644 --- a/src/core/lombok/core/DiagnosticsReceiver.java +++ b/src/core/lombok/core/DiagnosticsReceiver.java @@ -22,6 +22,17 @@ package lombok.core; public interface DiagnosticsReceiver { + DiagnosticsReceiver CONSOLE = new DiagnosticsReceiver() { + + @Override public void addError(String message) { + System.err.println(message); + } + + @Override public void addWarning(String message) { + System.out.println(message); + } + }; + /** Generate a compiler error on this node. */ void addError(String message); diff --git a/src/core/lombok/core/PostCompiler.java b/src/core/lombok/core/PostCompiler.java index 882430fc..a473b45f 100644 --- a/src/core/lombok/core/PostCompiler.java +++ b/src/core/lombok/core/PostCompiler.java @@ -21,10 +21,46 @@ */ package lombok.core; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + public final class PostCompiler { private PostCompiler() {/* prevent instantiation*/}; + private static List transformations; + public static byte[] applyTransformations(byte[] original, String className, DiagnosticsReceiver diagnostics) { - return original; + init(diagnostics); + byte[] previous = original; + for (PostCompilerTransformation transformation : transformations) { + try { + byte[] next = transformation.applyTransformations(previous, className, diagnostics); + if (next != null) { + previous = next; + } + } catch (Exception e) { + diagnostics.addWarning(String.format("Error during the transformation of '%s'; post-compiler '%s' caused an exception: %s", className, transformation.getClass().getName(), e.getMessage())); + } + } + return previous; + } + + private static synchronized void init(DiagnosticsReceiver diagnostics) { + if (transformations != null) return; + transformations = new ArrayList(); + try { + Iterator discovered = SpiLoadUtil.findServices(PostCompilerTransformation.class).iterator(); + while (discovered.hasNext()) { + try { + transformations.add(discovered.next()); + } catch (Exception e) { + diagnostics.addWarning("Error during loading post-compile transformers: " + e.getMessage()); + } + } + } catch (IOException e) { + diagnostics.addWarning("Could not load post-compile transformers: " + e.getMessage()); + } } } diff --git a/src/core/lombok/core/PostCompilerTransformation.java b/src/core/lombok/core/PostCompilerTransformation.java new file mode 100644 index 00000000..c174b97f --- /dev/null +++ b/src/core/lombok/core/PostCompilerTransformation.java @@ -0,0 +1,5 @@ +package lombok.core; + +public interface PostCompilerTransformation { + byte[] applyTransformations(byte[] original, String className, DiagnosticsReceiver diagnostics); +} -- cgit From 57bb88a7691dd89029517a79393a62e6cdedc3ce Mon Sep 17 00:00:00 2001 From: Roel Spilker Date: Tue, 3 Aug 2010 03:41:07 +0200 Subject: Post Compiler now works, including the sneaky throws remover. Also added a lombok app for it. Not yet in eclipse, though --- src/core/lombok/bytecode/PostCompilerApp.java | 152 +++++++++++++++++++++ src/core/lombok/bytecode/SneakyThrowsRemover.java | 66 ++++++--- src/core/lombok/core/PostCompiler.java | 14 +- .../lombok/core/PostCompilerTransformation.java | 21 +++ 4 files changed, 222 insertions(+), 31 deletions(-) create mode 100644 src/core/lombok/bytecode/PostCompilerApp.java (limited to 'src/core/lombok') diff --git a/src/core/lombok/bytecode/PostCompilerApp.java b/src/core/lombok/bytecode/PostCompilerApp.java new file mode 100644 index 00000000..3a413d67 --- /dev/null +++ b/src/core/lombok/bytecode/PostCompilerApp.java @@ -0,0 +1,152 @@ +/* + * Copyright © 2010 Reinier Zwitserloot and Roel Spilker. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package lombok.bytecode; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import lombok.core.DiagnosticsReceiver; +import lombok.core.LombokApp; +import lombok.core.PostCompiler; + +import org.mangosdk.spi.ProviderFor; + +import com.zwitserloot.cmdreader.CmdReader; +import com.zwitserloot.cmdreader.Description; +import com.zwitserloot.cmdreader.InvalidCommandLineException; +import com.zwitserloot.cmdreader.Mandatory; +import com.zwitserloot.cmdreader.Sequential; +import com.zwitserloot.cmdreader.Shorthand; + +@ProviderFor(LombokApp.class) +public class PostCompilerApp implements LombokApp { + @Override public List getAppAliases() { + return Arrays.asList("post", "postcompile"); + } + + @Override public String getAppDescription() { + return "Runs registered post compiler handlers to against existing class files, modifying them in the process."; + } + + @Override public String getAppName() { + return "post-compile"; + } + + public static class CmdArgs { + @Sequential + @Mandatory + @Description("paths to class files to be converted. If a directory is named, all files (recursively) in that directory will be converted.") + private List classFiles = new ArrayList(); + + @Shorthand("v") + @Description("Prints lots of status information as the post compiler runs") + boolean verbose = false; + + @Shorthand({"h", "?"}) + @Description("Shows this help text") + boolean help = false; + } + + @Override public int runApp(List raw) throws Exception { + CmdReader reader = CmdReader.of(CmdArgs.class); + CmdArgs args; + try { + args = reader.make(raw.toArray(new String[0])); + if (args.help) { + System.out.println(reader.generateCommandLineHelp("java -jar lombok.jar post-compile")); + return 0; + } + } catch (InvalidCommandLineException e) { + System.err.println(e.getMessage()); + System.err.println(reader.generateCommandLineHelp("java -jar lombok.jar post-compile")); + return 1; + } + + List filesToProcess = new ArrayList(); + for (String f : args.classFiles) addFiles(filesToProcess, f); + + int filesVisited = 0, filesTouched = 0; + for (File file : filesToProcess) { + if (!file.exists() || !file.isFile()) { + System.out.printf("Cannot find file '%s'\n", file); + continue; + } + filesVisited++; + if (args.verbose) System.out.println("Processing " + file.getAbsolutePath()); + byte[] original = readFile(file); + byte[] clone = original.clone(); + byte[] transformed = PostCompiler.applyTransformations(clone, file.toString(), DiagnosticsReceiver.CONSOLE); + if (clone != transformed && !Arrays.equals(clone, transformed)) { + filesTouched++; + if (args.verbose) System.out.println("Rewriting " + file.getAbsolutePath()); + writeFile(file, transformed); + } + } + + if (args.verbose) { + System.out.printf("Total files visited: %d total files changed: %d\n", filesVisited, filesTouched); + } + + return filesVisited == 0 ? 1 : 0; + } + + private void addFiles(List filesToProcess, String f) { + File file = new File(f); + if (file.isDirectory()) { + addRecursively(filesToProcess, file); + } else { + filesToProcess.add(file); + } + } + + private void addRecursively(List filesToProcess, File file) { + for (File f : file.listFiles()) { + if (f.isDirectory()) addRecursively(filesToProcess, f); + else if (f.getName().endsWith(".class")) filesToProcess.add(f); + } + } + + private static byte[] readFile(File file) throws IOException { + byte[] buffer = new byte[1024]; + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + FileInputStream fileInputStream = new FileInputStream(file); + while (true) { + int read = fileInputStream.read(buffer); + if (read == -1) break; + bytes.write(buffer, 0, read); + } + fileInputStream.close(); + return bytes.toByteArray(); + } + + private static void writeFile(File file, byte[] transformed) throws IOException { + FileOutputStream out = new FileOutputStream(file); + out.write(transformed); + out.close(); + } +} diff --git a/src/core/lombok/bytecode/SneakyThrowsRemover.java b/src/core/lombok/bytecode/SneakyThrowsRemover.java index b816cd54..02a220c3 100644 --- a/src/core/lombok/bytecode/SneakyThrowsRemover.java +++ b/src/core/lombok/bytecode/SneakyThrowsRemover.java @@ -1,5 +1,28 @@ +/* + * Copyright © 2010 Reinier Zwitserloot and Roel Spilker. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ package lombok.bytecode; +import java.util.concurrent.atomic.AtomicBoolean; + import lombok.core.DiagnosticsReceiver; import lombok.core.PostCompilerTransformation; @@ -51,30 +74,33 @@ public class SneakyThrowsRemover implements PostCompilerTransformation { ClassReader reader = new ClassReader(fixedByteCode); ClassWriter writer = new FixedClassWriter(reader, ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES); + final AtomicBoolean changesMade = new AtomicBoolean(); + + class SneakyThrowsRemoverVisitor extends MethodAdapter { + SneakyThrowsRemoverVisitor(MethodVisitor mv) { + super(mv); + } + + @Override public void visitMethodInsn(int opcode, String owner, String name, String desc) { + boolean hit = true; + if (hit && opcode != Opcodes.INVOKESTATIC) hit = false; + if (hit && !"sneakyThrow".equals(name)) hit = false; + if (hit && !"lombok/Lombok".equals(owner)) hit = false; + if (hit && !"(Ljava/lang/Throwable;)Ljava/lang/RuntimeException;".equals(desc)) hit = false; + if (hit) { + changesMade.set(true); + super.visitInsn(Opcodes.ATHROW); + } else { + super.visitMethodInsn(opcode, owner, name, desc); + } + } + } + reader.accept(new ClassAdapter(writer) { @Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { return new SneakyThrowsRemoverVisitor(super.visitMethod(access, name, desc, signature, exceptions)); } }, 0); - return writer.toByteArray(); - } - - private class SneakyThrowsRemoverVisitor extends MethodAdapter { - SneakyThrowsRemoverVisitor(MethodVisitor mv) { - super(mv); - } - - @Override public void visitMethodInsn(int opcode, String owner, String name, String desc) { - boolean hit = true; - if (hit && opcode != Opcodes.INVOKESTATIC) hit = false; - if (hit && !"sneakyThrow".equals(name)) hit = false; - if (hit && !"lombok/Lombok".equals(owner)) hit = false; - if (hit && !"(Ljava/lang/Throwable;)Ljava/lang/RuntimeException;".equals(desc)) hit = false; - if (hit) { - super.visitInsn(Opcodes.ATHROW); - } else { - super.visitMethodInsn(opcode, owner, name, desc); - } - } + return changesMade.get() ? writer.toByteArray() : null; } } diff --git a/src/core/lombok/core/PostCompiler.java b/src/core/lombok/core/PostCompiler.java index a473b45f..07cfc04d 100644 --- a/src/core/lombok/core/PostCompiler.java +++ b/src/core/lombok/core/PostCompiler.java @@ -22,8 +22,7 @@ package lombok.core; import java.io.IOException; -import java.util.ArrayList; -import java.util.Iterator; +import java.util.Collections; import java.util.List; public final class PostCompiler { @@ -49,17 +48,10 @@ public final class PostCompiler { private static synchronized void init(DiagnosticsReceiver diagnostics) { if (transformations != null) return; - transformations = new ArrayList(); try { - Iterator discovered = SpiLoadUtil.findServices(PostCompilerTransformation.class).iterator(); - while (discovered.hasNext()) { - try { - transformations.add(discovered.next()); - } catch (Exception e) { - diagnostics.addWarning("Error during loading post-compile transformers: " + e.getMessage()); - } - } + transformations = SpiLoadUtil.readAllFromIterator(SpiLoadUtil.findServices(PostCompilerTransformation.class, PostCompilerTransformation.class.getClassLoader())); } catch (IOException e) { + transformations = Collections.emptyList(); diagnostics.addWarning("Could not load post-compile transformers: " + e.getMessage()); } } diff --git a/src/core/lombok/core/PostCompilerTransformation.java b/src/core/lombok/core/PostCompilerTransformation.java index c174b97f..e0770f53 100644 --- a/src/core/lombok/core/PostCompilerTransformation.java +++ b/src/core/lombok/core/PostCompilerTransformation.java @@ -1,3 +1,24 @@ +/* + * Copyright © 2010 Reinier Zwitserloot and Roel Spilker. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ package lombok.core; public interface PostCompilerTransformation { -- cgit From 53650295f04edfbb7a48d4bdf3c790b11a5beeca Mon Sep 17 00:00:00 2001 From: Roel Spilker Date: Thu, 5 Aug 2010 00:29:38 +0200 Subject: Remove the double-athrow --- src/core/lombok/bytecode/SneakyThrowsRemover.java | 98 +++++++++++++++++++++++ 1 file changed, 98 insertions(+) (limited to 'src/core/lombok') diff --git a/src/core/lombok/bytecode/SneakyThrowsRemover.java b/src/core/lombok/bytecode/SneakyThrowsRemover.java index 02a220c3..4d69ab34 100644 --- a/src/core/lombok/bytecode/SneakyThrowsRemover.java +++ b/src/core/lombok/bytecode/SneakyThrowsRemover.java @@ -27,10 +27,13 @@ import lombok.core.DiagnosticsReceiver; import lombok.core.PostCompilerTransformation; import org.mangosdk.spi.ProviderFor; +import org.objectweb.asm.AnnotationVisitor; +import org.objectweb.asm.Attribute; import org.objectweb.asm.ClassAdapter; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.Label; import org.objectweb.asm.MethodAdapter; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; @@ -77,6 +80,8 @@ public class SneakyThrowsRemover implements PostCompilerTransformation { final AtomicBoolean changesMade = new AtomicBoolean(); class SneakyThrowsRemoverVisitor extends MethodAdapter { + boolean justAddedAthrow = false; + SneakyThrowsRemoverVisitor(MethodVisitor mv) { super(mv); } @@ -89,11 +94,104 @@ public class SneakyThrowsRemover implements PostCompilerTransformation { if (hit && !"(Ljava/lang/Throwable;)Ljava/lang/RuntimeException;".equals(desc)) hit = false; if (hit) { changesMade.set(true); + justAddedAthrow = true; super.visitInsn(Opcodes.ATHROW); } else { super.visitMethodInsn(opcode, owner, name, desc); } } + + @Override public void visitInsn(int opcode) { + if (!justAddedAthrow || opcode != Opcodes.ATHROW) { + super.visitInsn(opcode); + } + justAddedAthrow = false; + } + + @Override public AnnotationVisitor visitAnnotation(String desc, boolean visible) { + justAddedAthrow = false; + return super.visitAnnotation(desc, visible); + } + + @Override public AnnotationVisitor visitAnnotationDefault() { + justAddedAthrow = false; + return super.visitAnnotationDefault(); + } + + @Override public void visitAttribute(Attribute attr) { + justAddedAthrow = false; + super.visitAttribute(attr); + } + + @Override public void visitFieldInsn(int opcode, String owner, String name, String desc) { + justAddedAthrow = false; + super.visitFieldInsn(opcode, owner, name, desc); + } + + @Override public void visitIincInsn(int var, int increment) { + justAddedAthrow = false; + super.visitIincInsn(var, increment); + } + + @Override public void visitIntInsn(int opcode, int operand) { + justAddedAthrow = false; + super.visitIntInsn(opcode, operand); + } + + @Override public void visitJumpInsn(int opcode, Label label) { + justAddedAthrow = false; + super.visitJumpInsn(opcode, label); + } + + @Override public void visitLabel(Label label) { + justAddedAthrow = false; + super.visitLabel(label); + } + + @Override public void visitLdcInsn(Object cst) { + justAddedAthrow = false; + super.visitLdcInsn(cst); + } + + @Override public void visitLocalVariable(String name, String desc, String signature, Label start, Label end, int index) { + justAddedAthrow = false; + super.visitLocalVariable(name, desc, signature, start, end, index); + } + + @Override public void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels) { + justAddedAthrow = false; + super.visitLookupSwitchInsn(dflt, keys, labels); + } + + @Override public void visitMultiANewArrayInsn(String desc, int dims) { + justAddedAthrow = false; + super.visitMultiANewArrayInsn(desc, dims); + } + + @Override public AnnotationVisitor visitParameterAnnotation(int parameter, String desc, boolean visible) { + justAddedAthrow = false; + return super.visitParameterAnnotation(parameter, desc, visible); + } + + @Override public void visitTableSwitchInsn(int min, int max, Label dflt, Label[] labels) { + justAddedAthrow = false; + super.visitTableSwitchInsn(min, max, dflt, labels); + } + + @Override public void visitTryCatchBlock(Label start, Label end, Label handler, String type) { + justAddedAthrow = false; + super.visitTryCatchBlock(start, end, handler, type); + } + + @Override public void visitTypeInsn(int opcode, String type) { + justAddedAthrow = false; + super.visitTypeInsn(opcode, type); + } + + @Override public void visitVarInsn(int opcode, int var) { + justAddedAthrow = false; + super.visitVarInsn(opcode, var); + } } reader.accept(new ClassAdapter(writer) { -- cgit From d41a282e50a8348b904951893d98239095a7ccab Mon Sep 17 00:00:00 2001 From: Roel Spilker Date: Thu, 5 Aug 2010 23:44:48 +0200 Subject: Moved wrapOutputStream to PostCompiler --- src/core/lombok/core/PostCompiler.java | 25 ++++++++++++++++++++++ .../javac/apt/InterceptingJavaFileObject.java | 25 +--------------------- 2 files changed, 26 insertions(+), 24 deletions(-) (limited to 'src/core/lombok') diff --git a/src/core/lombok/core/PostCompiler.java b/src/core/lombok/core/PostCompiler.java index 07cfc04d..52568286 100644 --- a/src/core/lombok/core/PostCompiler.java +++ b/src/core/lombok/core/PostCompiler.java @@ -21,7 +21,9 @@ */ package lombok.core; +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.OutputStream; import java.util.Collections; import java.util.List; @@ -55,4 +57,27 @@ public final class PostCompiler { diagnostics.addWarning("Could not load post-compile transformers: " + e.getMessage()); } } + + public static OutputStream wrapOutputStream(final OutputStream originalStream, final String className, final DiagnosticsReceiver diagnostics) throws IOException { + return new ByteArrayOutputStream() { + @Override public void close() throws IOException { + // no need to call super + byte[] original = toByteArray(); + byte[] copy = null; + try { + copy = applyTransformations(original, className, diagnostics); + } catch (Exception e) { + diagnostics.addWarning(String.format("Error during the transformation of '%s'; no post-compilation has been applied", className)); + } + + if (copy == null) { + copy = original; + } + + // Exceptions below should bubble + originalStream.write(copy); + originalStream.close(); + } + }; + } } diff --git a/src/core/lombok/javac/apt/InterceptingJavaFileObject.java b/src/core/lombok/javac/apt/InterceptingJavaFileObject.java index 1e07440a..c0d3565c 100644 --- a/src/core/lombok/javac/apt/InterceptingJavaFileObject.java +++ b/src/core/lombok/javac/apt/InterceptingJavaFileObject.java @@ -21,7 +21,6 @@ */ package lombok.javac.apt; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -48,29 +47,7 @@ final class InterceptingJavaFileObject implements JavaFileObject { } public OutputStream openOutputStream() throws IOException { - // Open it first to make sure we throw an exception if that fails. - final OutputStream originalStream = delegate.openOutputStream(); - - return new ByteArrayOutputStream() { - @Override public void close() throws IOException { - // no need to call super - byte[] original = toByteArray(); - byte[] copy = null; - try { - copy = PostCompiler.applyTransformations(original, className, diagnostics); - } catch (Exception e) { - diagnostics.addWarning(String.format("Error during the transformation of '%s'; no post-compilation has been applied", className)); - } - - if (copy == null) { - copy = original; - } - - // Exceptions below should bubble - originalStream.write(copy); - originalStream.close(); - } - }; + return PostCompiler.wrapOutputStream(delegate.openOutputStream(), className, diagnostics); } public Writer openWriter() throws IOException { -- cgit From b14eef7eed8703824773467606f3be0c03a04b33 Mon Sep 17 00:00:00 2001 From: Roel Spilker Date: Thu, 5 Aug 2010 23:46:34 +0200 Subject: Created utility class to casually inspect class files on the usage of classes, fields and methods --- src/core/lombok/bytecode/ClassFileMetaData.java | 244 +++++++++++++++++++++ src/core/lombok/bytecode/PostCompilerApp.java | 6 +- src/core/lombok/bytecode/SneakyThrowsRemover.java | 2 + test/bytecode/resource/Bar.java | 3 + test/bytecode/resource/Baz.java | 2 + test/bytecode/resource/Buux.java | 10 + test/bytecode/resource/Foo.java | 9 + .../src/lombok/bytecode/ClassFileMetaDataTest.java | 196 +++++++++++++++++ 8 files changed, 469 insertions(+), 3 deletions(-) create mode 100644 src/core/lombok/bytecode/ClassFileMetaData.java create mode 100644 test/bytecode/resource/Bar.java create mode 100644 test/bytecode/resource/Baz.java create mode 100644 test/bytecode/resource/Buux.java create mode 100644 test/bytecode/resource/Foo.java create mode 100644 test/bytecode/src/lombok/bytecode/ClassFileMetaDataTest.java (limited to 'src/core/lombok') diff --git a/src/core/lombok/bytecode/ClassFileMetaData.java b/src/core/lombok/bytecode/ClassFileMetaData.java new file mode 100644 index 00000000..df50969a --- /dev/null +++ b/src/core/lombok/bytecode/ClassFileMetaData.java @@ -0,0 +1,244 @@ +/* + * Copyright © 2010 Reinier Zwitserloot and Roel Spilker. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package lombok.bytecode; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class ClassFileMetaData { + + private static final byte UTF8 = 1; + private static final byte INTEGER = 3; + private static final byte FLOAT = 4; + private static final byte LONG = 5; + private static final byte DOUBLE = 6; + private static final byte CLASS = 7; + private static final byte STRING = 8; + private static final byte FIELD = 9; + private static final byte METHOD = 10; + private static final byte IMETHOD = 11; + private static final byte NAME_TYPE = 12; + + private static final int NOT_FOUND = -1; + + private final byte[] byteCode; + + private final int maxPoolSize; + private final int[] offsets; + private final byte[] types; + private final String[] utf8s; + private final int endOfPool; + + public ClassFileMetaData(byte[] byteCode) { + this.byteCode = byteCode; + + maxPoolSize = readValue(8) + 1; + offsets = new int[maxPoolSize]; + types = new byte[maxPoolSize]; + utf8s = new String[maxPoolSize]; + int position = 10; + for (int i = 1; i < maxPoolSize - 1; i++) { + byte type = byteCode[position]; + types[i] = type; + position++; + offsets[i] = position; + switch (type) { + case UTF8: + int length = readValue(position); + position += 2; + utf8s[i] = decodeString(position, length); + position += length; + break; + case CLASS: + case STRING: + position += 2; + break; + case INTEGER: + case FLOAT: + case FIELD: + case METHOD: + case IMETHOD: + case NAME_TYPE: + position += 4; + break; + case LONG: + case DOUBLE: + position += 8; + break; + case 0: + break; + default: + throw new AssertionError("Unknown constant pool type " + type); + } + } + endOfPool = position; + } + + private String decodeString(int pos, int size) { + int end = pos + size; + + // the resulting string might be smaller + StringBuilder result = new StringBuilder(size); + while (pos < end) { + int first = (byteCode[pos++] & 0xFF); + if (first < 0x80) { + result.append((char)first); + } else if ((first & 0xE0) == 0xC0) { + int x = (first & 0x1F) << 6; + int y = (byteCode[pos++] & 0x3F); + result.append((char)(x | y)); + } else { + int x = (first & 0x0F) << 12; + int y = (byteCode[pos++] & 0x3F) << 6; + int z = (byteCode[pos++] & 0x3F); + result.append((char)(x | y | z)); + } + } + return result.toString(); + } + + public boolean containsUtf8(String value) { + return findUtf8(value) != NOT_FOUND; + } + + public boolean usesClass(String className) { + return findClass(className) != NOT_FOUND; + } + + public boolean usesField(String className, String fieldName) { + int classIndex = findClass(className); + if (classIndex == NOT_FOUND) return false; + int fieldNameIndex = findUtf8(fieldName); + if (fieldNameIndex == NOT_FOUND) return false; + + for (int i = 1; i < maxPoolSize; i++) { + if (types[i] == FIELD && readValue(offsets[i]) == classIndex) { + int nameAndTypeIndex = readValue(offsets[i] + 2); + if (readValue(offsets[nameAndTypeIndex]) == fieldNameIndex) return true; + } + } + return false; + } + + public boolean usesMethod(String className, String methodName) { + int classIndex = findClass(className); + if (classIndex == NOT_FOUND) return false; + int methodNameIndex = findUtf8(methodName); + if (methodNameIndex == NOT_FOUND) return false; + + for (int i = 1; i < maxPoolSize; i++) { + if (isMethod(i) && readValue(offsets[i]) == classIndex) { + int nameAndTypeIndex = readValue(offsets[i] + 2); + if (readValue(offsets[nameAndTypeIndex]) == methodNameIndex) return true; + } + } + return false; + } + + public boolean usesMethod(String className, String methodName, String descriptor) { + int classIndex = findClass(className); + if (classIndex == NOT_FOUND) return false; + int nameAndTypeIndex = findNameAndType(methodName, descriptor); + if (nameAndTypeIndex == NOT_FOUND) return false; + + for (int i = 1; i < maxPoolSize; i++) { + if (isMethod(i) && + readValue(offsets[i]) == classIndex && + readValue(offsets[i] + 2) == nameAndTypeIndex) return true; + } + return false; + } + + public boolean containsStringConstant(String value) { + int index = findUtf8(value); + if (index == NOT_FOUND) return false; + for (int i = 1; i < maxPoolSize; i++) { + if (types[i] == STRING && readValue(offsets[i]) == index) return true; + } + return false; + } + + public String getClassName() { + return getClassName(readValue(endOfPool + 2)); + } + + public String getSuperClassName() { + return getClassName(readValue(endOfPool + 4)); + } + + public List getInterfaces() { + int size = readValue(endOfPool + 6); + if (size == 0) return Collections.emptyList(); + + List result = new ArrayList(); + for (int i = 0; i < size; i++) { + result.add(getClassName(readValue(endOfPool + 8 + (i * 2)))); + } + return result; + } + + private String getClassName(int classIndex) { + if (classIndex < 1) return null; + return utf8s[readValue(offsets[classIndex])]; + } + + private boolean isMethod(int i) { + byte type = types[i]; + return type == METHOD || type == IMETHOD; + } + + private int findNameAndType(String name, String descriptor) { + int nameIndex = findUtf8(name); + if (nameIndex == NOT_FOUND) return NOT_FOUND; + int descriptorIndex = findUtf8(descriptor); + if (descriptorIndex == NOT_FOUND) return NOT_FOUND; + for (int i = 1; i < maxPoolSize; i++) { + if (types[i] == NAME_TYPE && + readValue(offsets[i]) == nameIndex && + readValue(offsets[i] + 2) == descriptorIndex) return i; + } + return NOT_FOUND; + } + + private int findUtf8(String value) { + for (int i = 1; i < maxPoolSize; i++) { + if (value.equals(utf8s[i])) { + return i; + } + } + return NOT_FOUND; + } + + private int findClass(String className) { + int index = findUtf8(className); + if (index == -1) return NOT_FOUND; + for (int i = 1; i < maxPoolSize; i++) { + if (types[i] == CLASS && readValue(offsets[i]) == index) return i; + } + return NOT_FOUND; + } + + private int readValue(int position) { + return ((byteCode[position] & 0xFF) << 8) | (byteCode[position + 1] & 0xFF); + } +} \ No newline at end of file diff --git a/src/core/lombok/bytecode/PostCompilerApp.java b/src/core/lombok/bytecode/PostCompilerApp.java index 3a413d67..e1b177ba 100644 --- a/src/core/lombok/bytecode/PostCompilerApp.java +++ b/src/core/lombok/bytecode/PostCompilerApp.java @@ -130,8 +130,8 @@ public class PostCompilerApp implements LombokApp { else if (f.getName().endsWith(".class")) filesToProcess.add(f); } } - - private static byte[] readFile(File file) throws IOException { + + static byte[] readFile(File file) throws IOException { byte[] buffer = new byte[1024]; ByteArrayOutputStream bytes = new ByteArrayOutputStream(); FileInputStream fileInputStream = new FileInputStream(file); @@ -144,7 +144,7 @@ public class PostCompilerApp implements LombokApp { return bytes.toByteArray(); } - private static void writeFile(File file, byte[] transformed) throws IOException { + static void writeFile(File file, byte[] transformed) throws IOException { FileOutputStream out = new FileOutputStream(file); out.write(transformed); out.close(); diff --git a/src/core/lombok/bytecode/SneakyThrowsRemover.java b/src/core/lombok/bytecode/SneakyThrowsRemover.java index 4d69ab34..fde29924 100644 --- a/src/core/lombok/bytecode/SneakyThrowsRemover.java +++ b/src/core/lombok/bytecode/SneakyThrowsRemover.java @@ -72,6 +72,8 @@ public class SneakyThrowsRemover implements PostCompilerTransformation { } @Override public byte[] applyTransformations(byte[] original, String className, DiagnosticsReceiver diagnostics) { + if (!new ClassFileMetaData(original).usesMethod("lombok/Lombok", "sneakyThrow")) return null; + byte[] fixedByteCode = fixJSRInlining(original); ClassReader reader = new ClassReader(fixedByteCode); diff --git a/test/bytecode/resource/Bar.java b/test/bytecode/resource/Bar.java new file mode 100644 index 00000000..13cbf425 --- /dev/null +++ b/test/bytecode/resource/Bar.java @@ -0,0 +1,3 @@ +public interface Bar extends java.util.RandomAccess, java.util.Map { + String getName(); +} \ No newline at end of file diff --git a/test/bytecode/resource/Baz.java b/test/bytecode/resource/Baz.java new file mode 100644 index 00000000..2525ed00 --- /dev/null +++ b/test/bytecode/resource/Baz.java @@ -0,0 +1,2 @@ +public interface Baz { +} \ No newline at end of file diff --git a/test/bytecode/resource/Buux.java b/test/bytecode/resource/Buux.java new file mode 100644 index 00000000..33b352e2 --- /dev/null +++ b/test/bytecode/resource/Buux.java @@ -0,0 +1,10 @@ +public class Buux extends java.util.ArrayList { + public Buux() { + super(7); + addSomething(); + } + + public void addSomething() { + super.add("H\u3404l\0"); + } +} \ No newline at end of file diff --git a/test/bytecode/resource/Foo.java b/test/bytecode/resource/Foo.java new file mode 100644 index 00000000..95a2c820 --- /dev/null +++ b/test/bytecode/resource/Foo.java @@ -0,0 +1,9 @@ +public class Foo implements java.util.RandomAccess { + private static final String ONE = "Eén"; + + { + String value = toString(); + System.out.print(value); + System.out.print("Two" + "Four"); + } +} \ No newline at end of file diff --git a/test/bytecode/src/lombok/bytecode/ClassFileMetaDataTest.java b/test/bytecode/src/lombok/bytecode/ClassFileMetaDataTest.java new file mode 100644 index 00000000..a2fa919f --- /dev/null +++ b/test/bytecode/src/lombok/bytecode/ClassFileMetaDataTest.java @@ -0,0 +1,196 @@ +/* + * Copyright © 2010 Reinier Zwitserloot and Roel Spilker. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package lombok.bytecode; + +import static org.junit.Assert.*; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.io.StringWriter; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import javax.tools.JavaCompiler; +import javax.tools.SimpleJavaFileObject; +import javax.tools.ToolProvider; +import javax.tools.JavaCompiler.CompilationTask; + +import lombok.Lombok; + +import org.junit.Test; + +public class ClassFileMetaDataTest { + + private ClassFileMetaData foo = create(new File("test/bytecode/resource/Foo.java")); + private ClassFileMetaData bar = create(new File("test/bytecode/resource/Bar.java")); + private ClassFileMetaData baz = create(new File("test/bytecode/resource/Baz.java")); + private ClassFileMetaData buux = create(new File("test/bytecode/resource/Buux.java")); + + @Test + public void testGetClassName() { + assertTrue(foo.containsUtf8("Foo")); + assertEquals("Foo", foo.getClassName()); + + assertTrue(bar.containsUtf8("Bar")); + assertEquals("Bar", bar.getClassName()); + + assertTrue(baz.containsUtf8("Baz")); + assertEquals("Baz", baz.getClassName()); + } + + @Test + public void testGetSuperClassName() { + assertTrue(foo.containsUtf8("java/lang/Object")); + assertEquals("java/lang/Object", foo.getSuperClassName()); + + assertEquals("java/lang/Object", bar.getSuperClassName()); + assertEquals("java/lang/Object", baz.getSuperClassName()); + + assertEquals("java/util/ArrayList", buux.getSuperClassName()); + } + + + + @Test + public void testUsesClass() { + assertTrue(foo.usesClass("java/lang/System")); +// assertTrue(foo.usesClass("java/lang/String")); + } + + @Test + public void testUsesField() { + assertTrue(foo.usesField("java/lang/System", "out")); + } + + @Test + public void testUsesMethodWithName() { + assertTrue(foo.usesMethod("java/io/PrintStream", "print")); + + assertTrue(buux.usesMethod("java/util/ArrayList", "")); + assertTrue(buux.usesMethod("java/util/ArrayList", "add")); + assertTrue(buux.usesMethod("Buux", "addSomething")); + } + + @Test + public void testUsesMethodWithNameAndDescriptor() { + assertTrue(foo.usesMethod("java/io/PrintStream", "print", "(Ljava/lang/String;)V")); + + assertTrue(buux.usesMethod("java/util/ArrayList", "", "(I)V")); + assertTrue(buux.usesMethod("java/util/ArrayList", "add", "(Ljava/lang/Object;)Z")); + assertTrue(buux.usesMethod("Buux", "addSomething", "()V")); + } + + @Test + public void testGetInterfaces() { + assertTrue(foo.containsUtf8("java/util/RandomAccess")); + + List fooInterfaces = foo.getInterfaces(); + assertEquals(1, fooInterfaces.size()); + assertEquals("java/util/RandomAccess", fooInterfaces.get(0)); + + assertTrue(bar.containsUtf8("java/util/RandomAccess")); + assertTrue(bar.containsUtf8("java/util/Map")); + + List barInterfaces = bar.getInterfaces(); + assertEquals(2, barInterfaces.size()); + assertEquals("java/util/RandomAccess", barInterfaces.get(0)); + assertEquals("java/util/Map", barInterfaces.get(1)); + } + + @Test + public void testContainsStringConstant() { + assertTrue(foo.containsStringConstant("Eén")); + assertTrue(foo.containsStringConstant("TwoFour")); + + assertTrue(buux.containsStringConstant("H\u3404l\0")); + } + + private ClassFileMetaData create(File file) { + return new ClassFileMetaData(compile(file)); + } + + private byte[] compile(File file) { + try { + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + File tempDir = getTempDir(); + tempDir.mkdirs(); + List options = Arrays.asList("-nowarn", "-proc:none", "-d", tempDir.getAbsolutePath()); + StringWriter captureWarnings = new StringWriter(); + CompilationTask task = compiler.getTask(captureWarnings, null, null, options, null, Collections.singleton(new ContentBasedJavaFileObject(file.getPath(), readFileAsString(file)))); + assertTrue(task.call()); + return PostCompilerApp.readFile(new File(tempDir, file.getName().replaceAll("\\.java$", ".class"))); + } catch (Exception e) { + throw Lombok.sneakyThrow(e); + } + } + + private File getTempDir() { + String[] rawDirs = { + System.getProperty("java.io.tmpdir"), + "/tmp", + "C:\\Windows\\Temp" + }; + + for (String dir : rawDirs) { + if (dir == null) continue; + File f = new File(dir); + if (!f.isDirectory()) continue; + return new File(f, "lombok.bytecode-test"); + } + + return new File("./build/tmp"); + } + + static class ContentBasedJavaFileObject extends SimpleJavaFileObject { + private final String content; + + protected ContentBasedJavaFileObject(String name, String content) { + super(new File(name).toURI(), Kind.SOURCE); + this.content = content; + } + + @Override + public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException { + return content; + } + } + + private String readFileAsString(File file) { + try { + BufferedReader reader = new BufferedReader(new FileReader(file)); + StringWriter writer = new StringWriter(); + String line = reader.readLine(); + while(line != null) { + writer.append(line).append("\n"); + line = reader.readLine(); + } + reader.close(); + writer.close(); + return writer.toString(); + } catch (Exception e) { + throw Lombok.sneakyThrow(e); + } + } +} -- cgit From 2683c24ee96fd7228198512f5cfcb2fd0b0cfabd Mon Sep 17 00:00:00 2001 From: Roel Spilker Date: Sat, 7 Aug 2010 22:27:32 +0200 Subject: Fixed some bugs in reading the constant pool and added tests --- src/core/lombok/bytecode/ClassFileMetaData.java | 122 ++++++++++++++++++++- test/bytecode/resource/Foo.java | 11 ++ .../src/lombok/bytecode/ClassFileMetaDataTest.java | 76 +++++++++++-- 3 files changed, 199 insertions(+), 10 deletions(-) (limited to 'src/core/lombok') diff --git a/src/core/lombok/bytecode/ClassFileMetaData.java b/src/core/lombok/bytecode/ClassFileMetaData.java index df50969a..693a9ad5 100644 --- a/src/core/lombok/bytecode/ClassFileMetaData.java +++ b/src/core/lombok/bytecode/ClassFileMetaData.java @@ -40,6 +40,7 @@ public class ClassFileMetaData { private static final byte NAME_TYPE = 12; private static final int NOT_FOUND = -1; + private static final int START_OF_CONSTANT_POOL = 8; private final byte[] byteCode; @@ -52,12 +53,12 @@ public class ClassFileMetaData { public ClassFileMetaData(byte[] byteCode) { this.byteCode = byteCode; - maxPoolSize = readValue(8) + 1; + maxPoolSize = readValue(START_OF_CONSTANT_POOL); offsets = new int[maxPoolSize]; types = new byte[maxPoolSize]; utf8s = new String[maxPoolSize]; int position = 10; - for (int i = 1; i < maxPoolSize - 1; i++) { + for (int i = 1; i < maxPoolSize; i++) { byte type = byteCode[position]; types[i] = type; position++; @@ -84,6 +85,7 @@ public class ClassFileMetaData { case LONG: case DOUBLE: position += 8; + i++; break; case 0: break; @@ -178,6 +180,65 @@ public class ClassFileMetaData { return false; } + public boolean containsLong(long value) { + for (int i = 1; i < maxPoolSize; i++) { + if (types[i] == LONG && readLong(i) == value) return true; + } + return false; + } + + public boolean containsDouble(double value) { + boolean isNan = Double.isNaN(value); + for (int i = 1; i < maxPoolSize; i++) { + if (types[i] == DOUBLE) { + double d = readDouble(i); + if (d == value || (isNan && Double.isNaN(d))) return true; + } + } + return false; + } + + public boolean containsInteger(int value) { + for (int i = 1; i < maxPoolSize; i++) { + if (types[i] == INTEGER && readInteger(i) == value) return true; + } + return false; + } + + public boolean containsFloat(float value) { + boolean isNan = Float.isNaN(value); + for (int i = 1; i < maxPoolSize; i++) { + if (types[i] == FLOAT) { + float f = readFloat(i); + if (f == value || (isNan && Float.isNaN(f))) return true; + } + } + return false; + } + + private long readLong(int index) { + int pos = offsets[index]; + return ((long)read32(pos)) << 32 | read32(pos + 4); + } + + private double readDouble(int index) { + int pos = offsets[index]; + long bits = ((long)read32(pos)) << 32 | (read32(pos + 4) & 0x00000000FFFFFFFF); + return Double.longBitsToDouble(bits); + } + + private long readInteger(int index) { + return read32(offsets[index]); + } + + private float readFloat(int index) { + return Float.intBitsToFloat(read32(offsets[index])); + } + + private int read32(int pos) { + return (byteCode[pos] & 0xFF) << 24 | (byteCode[pos + 1] & 0xFF) << 16 | (byteCode[pos + 2] & 0xFF) << 8 | (byteCode[pos + 3] &0xFF); + } + public String getClassName() { return getClassName(readValue(endOfPool + 2)); } @@ -197,6 +258,63 @@ public class ClassFileMetaData { return result; } + public String poolContent() { + StringBuilder result = new StringBuilder(); + for (int i = 1; i < maxPoolSize; i++) { + result.append(String.format("#%02d: ", i)); + int pos = offsets[i]; + switch(types[i]) { + case UTF8: + result.append("Utf8 ").append(utf8s[i]); + break; + case CLASS: + result.append("Class ").append(getClassName(i)); + break; + case STRING: + result.append("String \"").append(utf8s[readValue(pos)]).append("\""); + break; + case INTEGER: + result.append("int ").append(readInteger(i)); + break; + case FLOAT: + result.append("float ").append(readFloat(i)); + break; + case FIELD: + appendAccess(result.append("Field "), i); + break; + case METHOD: + case IMETHOD: + appendAccess(result.append("Method "), i); + break; + case NAME_TYPE: + appendNameAndType(result.append("Name&Type "), i); + break; + case LONG: + result.append("long ").append(readLong(i)); + break; + case DOUBLE: + result.append("double ").append(readDouble(i)); + break; + case 0: + result.append("(cont.)"); + break; + } + result.append("\n"); + } + return result.toString(); + } + + private void appendAccess(StringBuilder result, int index) { + int pos = offsets[index]; + result.append(getClassName(readValue(pos))).append("."); + appendNameAndType(result, readValue(pos + 2)); + } + + private void appendNameAndType(StringBuilder result, int index) { + int pos = offsets[index]; + result.append(utf8s[readValue(pos)]).append(":").append(utf8s[readValue(pos + 2)]); + } + private String getClassName(int classIndex) { if (classIndex < 1) return null; return utf8s[readValue(offsets[classIndex])]; diff --git a/test/bytecode/resource/Foo.java b/test/bytecode/resource/Foo.java index 95a2c820..2726026c 100644 --- a/test/bytecode/resource/Foo.java +++ b/test/bytecode/resource/Foo.java @@ -1,5 +1,16 @@ public class Foo implements java.util.RandomAccess { + private static final long LONG = 123L; private static final String ONE = "Eén"; + private static final int INT = 123; + private static final double DOUBLE = 1.23; + private static final double DOUBLE_NAN = Double.NaN; + private static final double DOUBLE_INF = Double.POSITIVE_INFINITY; + private static final double DOUBLE_NEG_INF = Double.NEGATIVE_INFINITY; + + private static final float FLOAT = 1.23F; + private static final float FLOAT_NAN = Float.NaN; + private static final float FLOAT_INF = Float.POSITIVE_INFINITY; + private static final float FLOAT_NEG_INF = Float.NEGATIVE_INFINITY; { String value = toString(); diff --git a/test/bytecode/src/lombok/bytecode/ClassFileMetaDataTest.java b/test/bytecode/src/lombok/bytecode/ClassFileMetaDataTest.java index a2fa919f..191ec70d 100644 --- a/test/bytecode/src/lombok/bytecode/ClassFileMetaDataTest.java +++ b/test/bytecode/src/lombok/bytecode/ClassFileMetaDataTest.java @@ -43,10 +43,24 @@ import org.junit.Test; public class ClassFileMetaDataTest { - private ClassFileMetaData foo = create(new File("test/bytecode/resource/Foo.java")); - private ClassFileMetaData bar = create(new File("test/bytecode/resource/Bar.java")); - private ClassFileMetaData baz = create(new File("test/bytecode/resource/Baz.java")); - private ClassFileMetaData buux = create(new File("test/bytecode/resource/Buux.java")); + private static ClassFileMetaData foo = create(new File("test/bytecode/resource/Foo.java")); + private static ClassFileMetaData bar = create(new File("test/bytecode/resource/Bar.java")); + private static ClassFileMetaData baz = create(new File("test/bytecode/resource/Baz.java")); + private static ClassFileMetaData buux = create(new File("test/bytecode/resource/Buux.java")); + +// @Test +// public void dump() { +// byte[] bytes = compile(new File("test/bytecode/resource/Foo.java")); +// int count = 0; +// for (byte b : bytes) { +// System.out.printf("%02x ", (b & 0xFF)); +// count++; +// if (count % 20 == 0) System.out.println(); +// } +// System.out.println(); +// System.out.println(); +// System.out.println(foo.poolContent()); +// } @Test public void testGetClassName() { @@ -125,13 +139,59 @@ public class ClassFileMetaDataTest { assertTrue(foo.containsStringConstant("TwoFour")); assertTrue(buux.containsStringConstant("H\u3404l\0")); + + assertFalse(foo.containsStringConstant("Seven")); + } + + @Test + public void testContainsDouble() { + assertTrue(foo.containsDouble(1.23)); + assertTrue(foo.containsDouble(Double.NaN)); + assertTrue(foo.containsDouble(Double.POSITIVE_INFINITY)); + assertTrue(foo.containsDouble(Double.NEGATIVE_INFINITY)); + + assertFalse(foo.containsDouble(1.0)); + assertFalse(buux.containsDouble(1.0)); + assertFalse(buux.containsDouble(Double.NaN)); + assertFalse(buux.containsDouble(Double.POSITIVE_INFINITY)); + assertFalse(buux.containsDouble(Double.NEGATIVE_INFINITY)); + } + + @Test + public void testContainsFloat() { + assertTrue(foo.containsFloat(1.23F)); + assertTrue(foo.containsFloat(Float.NaN)); + assertTrue(foo.containsFloat(Float.POSITIVE_INFINITY)); + assertTrue(foo.containsFloat(Float.NEGATIVE_INFINITY)); + + assertFalse(foo.containsFloat(1.0F)); + assertFalse(buux.containsFloat(1.0F)); + assertFalse(buux.containsFloat(Float.NaN)); + assertFalse(buux.containsFloat(Float.POSITIVE_INFINITY)); + assertFalse(buux.containsFloat(Float.NEGATIVE_INFINITY)); + } + + @Test + public void testContainsInteger() { + assertTrue(foo.containsInteger(123)); + + assertFalse(foo.containsInteger(1)); + assertFalse(buux.containsInteger(1)); + } + + @Test + public void testContainsLong() { + assertTrue(foo.containsLong(123)); + + assertFalse(foo.containsLong(1)); + assertFalse(buux.containsLong(1)); } - private ClassFileMetaData create(File file) { + private static ClassFileMetaData create(File file) { return new ClassFileMetaData(compile(file)); } - private byte[] compile(File file) { + private static byte[] compile(File file) { try { JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); File tempDir = getTempDir(); @@ -146,7 +206,7 @@ public class ClassFileMetaDataTest { } } - private File getTempDir() { + private static File getTempDir() { String[] rawDirs = { System.getProperty("java.io.tmpdir"), "/tmp", @@ -177,7 +237,7 @@ public class ClassFileMetaDataTest { } } - private String readFileAsString(File file) { + private static String readFileAsString(File file) { try { BufferedReader reader = new BufferedReader(new FileReader(file)); StringWriter writer = new StringWriter(); -- cgit From b0ec7d5389e7b2e7b1ddf7c743f0ff093d55c1f1 Mon Sep 17 00:00:00 2001 From: Roel Spilker Date: Sun, 15 Aug 2010 22:27:56 +0200 Subject: Added error/warning to the printed messages. --- src/core/lombok/core/DiagnosticsReceiver.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) (limited to 'src/core/lombok') diff --git a/src/core/lombok/core/DiagnosticsReceiver.java b/src/core/lombok/core/DiagnosticsReceiver.java index 7e9acd80..2adfadaf 100644 --- a/src/core/lombok/core/DiagnosticsReceiver.java +++ b/src/core/lombok/core/DiagnosticsReceiver.java @@ -23,13 +23,12 @@ package lombok.core; public interface DiagnosticsReceiver { DiagnosticsReceiver CONSOLE = new DiagnosticsReceiver() { - @Override public void addError(String message) { - System.err.println(message); + System.err.println("Error: " + message); } @Override public void addWarning(String message) { - System.out.println(message); + System.out.println("Warning: " + message); } }; -- cgit