diff options
author | Benoit Lamarche <benoitlamarche@google.com> | 2017-08-21 14:49:19 +0200 |
---|---|---|
committer | Benoit Lamarche <benoitlamarche@google.com> | 2017-08-21 14:49:19 +0200 |
commit | d483112cf7af2e256d7c9ed393e354bf51e72d5f (patch) | |
tree | 101dc16131989c0d9c8b97efbde3b3f3bb0b9513 | |
parent | fc0b1b9556a5f369193df1379d575db8c2807d3c (diff) | |
download | r8-d483112cf7af2e256d7c9ed393e354bf51e72d5f.tar.gz |
Introduce ApiLevelException as a checked exception
It extends CompilationException which is already part of the API.
Bug: 63692875
Change-Id: I89418c80793f894b3438d69af9890e70b469ded2
9 files changed, 140 insertions, 45 deletions
diff --git a/src/main/java/com/android/tools/r8/ApiLevelException.java b/src/main/java/com/android/tools/r8/ApiLevelException.java new file mode 100644 index 000000000..ec9fe6885 --- /dev/null +++ b/src/main/java/com/android/tools/r8/ApiLevelException.java @@ -0,0 +1,31 @@ +// Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. +package com.android.tools.r8; + +/** + * Exception to signal features that are not supported until a given API level. + */ +public class ApiLevelException extends CompilationException { + + public ApiLevelException( + int minApiLevel, String minApiLevelString, String unsupportedFeatures, String sourceString) { + super(makeMessage(minApiLevel, minApiLevelString, unsupportedFeatures, sourceString)); + assert minApiLevel > 0; + assert minApiLevelString != null; + assert unsupportedFeatures != null; + } + + private static String makeMessage( + int minApiLevel, String minApiLevelString, String unsupportedFeatures, String sourceString) { + String message = + unsupportedFeatures + + " are only supported starting with " + + minApiLevelString + + " (--min-api " + + minApiLevel + + ")"; + message = (sourceString != null) ? message + ": " + sourceString : message; + return message; + } +}
\ No newline at end of file diff --git a/src/main/java/com/android/tools/r8/D8.java b/src/main/java/com/android/tools/r8/D8.java index ed992a3d5..38b7acc4e 100644 --- a/src/main/java/com/android/tools/r8/D8.java +++ b/src/main/java/com/android/tools/r8/D8.java @@ -66,7 +66,7 @@ public final class D8 { * @param command D8 command. * @return the compilation result. */ - public static D8Output run(D8Command command) throws IOException { + public static D8Output run(D8Command command) throws IOException, CompilationException { InternalOptions options = command.getInternalOptions(); CompilationResult result = runForTesting(command.getInputApp(), options); assert result != null; @@ -87,7 +87,8 @@ public final class D8 { * @param executor executor service from which to get threads for multi-threaded processing. * @return the compilation result. */ - public static D8Output run(D8Command command, ExecutorService executor) throws IOException { + public static D8Output run(D8Command command, ExecutorService executor) + throws IOException, CompilationException { InternalOptions options = command.getInternalOptions(); CompilationResult result = runForTesting( command.getInputApp(), options, executor); @@ -146,7 +147,7 @@ public final class D8 { } static CompilationResult runForTesting(AndroidApp inputApp, InternalOptions options) - throws IOException { + throws IOException, CompilationException { ExecutorService executor = ThreadUtils.getExecutorService(options); try { return runForTesting(inputApp, options, executor); @@ -166,7 +167,8 @@ public final class D8 { } private static CompilationResult runForTesting( - AndroidApp inputApp, InternalOptions options, ExecutorService executor) throws IOException { + AndroidApp inputApp, InternalOptions options, ExecutorService executor) + throws IOException, CompilationException { try { assert !inputApp.hasPackageDistribution(); @@ -200,10 +202,8 @@ public final class D8 { } catch (MainDexError mainDexError) { throw new CompilationError(mainDexError.getMessageForD8()); } catch (ExecutionException e) { - if (e.getCause() instanceof CompilationError) { - throw (CompilationError) e.getCause(); - } - throw new RuntimeException(e.getMessage(), e.getCause()); + R8.unwrapExecutionException(e); + throw new AssertionError(e); // unwrapping method should have thrown } } diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java index da90e8247..f28a07a68 100644 --- a/src/main/java/com/android/tools/r8/R8.java +++ b/src/main/java/com/android/tools/r8/R8.java @@ -207,7 +207,7 @@ public class R8 { } static CompilationResult runForTesting(AndroidApp app, InternalOptions options) - throws ProguardRuleParserException, IOException { + throws ProguardRuleParserException, IOException, CompilationException { ExecutorService executor = ThreadUtils.getExecutorService(options); try { return runForTesting(app, options, executor); @@ -220,12 +220,12 @@ public class R8 { AndroidApp app, InternalOptions options, ExecutorService executor) - throws ProguardRuleParserException, IOException { + throws ProguardRuleParserException, IOException, CompilationException { return new R8(options).run(app, executor); } private CompilationResult run(AndroidApp inputApp, ExecutorService executorService) - throws IOException, ProguardRuleParserException { + throws IOException, ProguardRuleParserException, CompilationException { if (options.quiet) { System.setOut(new PrintStream(ByteStreams.nullOutputStream())); } @@ -389,10 +389,8 @@ public class R8 { } catch (MainDexError mainDexError) { throw new CompilationError(mainDexError.getMessageForR8()); } catch (ExecutionException e) { - if (e.getCause() instanceof CompilationError) { - throw (CompilationError) e.getCause(); - } - throw new RuntimeException(e.getMessage(), e.getCause()); + unwrapExecutionException(e); + throw new AssertionError(e); // unwrapping method should have thrown } finally { // Dump timings. if (options.printTimes) { @@ -401,6 +399,36 @@ public class R8 { } } + static void unwrapExecutionException(ExecutionException executionException) + throws CompilationException { + Throwable cause = executionException.getCause(); + if (cause instanceof CompilationError) { + // add original exception as suppressed exception to provide the original stack trace + cause.addSuppressed(executionException); + throw (CompilationError) cause; + } else if (cause instanceof CompilationException) { + cause.addSuppressed(executionException); + throw (CompilationException) cause; + } else if (cause instanceof RuntimeException) { + // ForkJoinPool wraps checked exceptions in RuntimeExceptions + if (cause.getCause() != null + && cause.getCause() instanceof CompilationException) { + cause.addSuppressed(executionException); + throw (CompilationException) cause.getCause(); + // ForkJoinPool sometimes uses 2 levels of RuntimeExceptions, to provide accurate stack traces + } else if (cause.getCause() != null && cause.getCause().getCause() != null + && cause.getCause().getCause() instanceof CompilationException) { + cause.addSuppressed(executionException); + throw (CompilationException) cause.getCause().getCause(); + } else { + cause.addSuppressed(executionException); + throw (RuntimeException) cause; + } + } else { + throw new RuntimeException(executionException.getMessage(), cause); + } + } + /** * Main API entry for the R8 compiler. * diff --git a/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java b/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java index 1e2e2c9c9..412b291a9 100644 --- a/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java +++ b/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java @@ -3,6 +3,10 @@ // BSD-style license that can be found in the LICENSE file. package com.android.tools.r8.dex; +import com.android.tools.r8.ApiLevelException; +import com.android.tools.r8.dex.VirtualFile.FilePerClassDistributor; +import com.android.tools.r8.dex.VirtualFile.FillFilesDistributor; +import com.android.tools.r8.dex.VirtualFile.PackageMapDistributor; import com.android.tools.r8.errors.CompilationError; import com.android.tools.r8.graph.AppInfo; import com.android.tools.r8.graph.DexAnnotation; @@ -203,7 +207,7 @@ public class ApplicationWriter { } } - private byte[] writeDexFile(VirtualFile vfile) { + private byte[] writeDexFile(VirtualFile vfile) throws ApiLevelException { FileWriter fileWriter = new FileWriter( vfile.computeMapping(application), application, appInfo, options, namingLens); diff --git a/src/main/java/com/android/tools/r8/dex/FileWriter.java b/src/main/java/com/android/tools/r8/dex/FileWriter.java index f1162a929..623787270 100644 --- a/src/main/java/com/android/tools/r8/dex/FileWriter.java +++ b/src/main/java/com/android/tools/r8/dex/FileWriter.java @@ -5,6 +5,7 @@ package com.android.tools.r8.dex; import static com.android.tools.r8.utils.LebUtils.sizeAsUleb128; +import com.android.tools.r8.ApiLevelException; import com.android.tools.r8.code.Instruction; import com.android.tools.r8.errors.CompilationError; import com.android.tools.r8.graph.AppInfo; @@ -183,7 +184,7 @@ public class FileWriter { return this; } - public byte[] generate() { + public byte[] generate() throws ApiLevelException { // Check restrictions on interface methods. checkInterfaceMethods(); @@ -268,10 +269,15 @@ public class FileWriter { Arrays.sort(methods, (DexEncodedMethod a, DexEncodedMethod b) -> a.method.compareTo(b.method)); } - private void checkInterfaceMethods() { + private void checkInterfaceMethods() throws ApiLevelException { for (DexProgramClass clazz : mapping.getClasses()) { if (clazz.isInterface()) { - clazz.forEachMethod(this::checkInterfaceMethod); + for (DexEncodedMethod method : clazz.directMethods()) { + checkInterfaceMethod(method); + } + for (DexEncodedMethod method : clazz.virtualMethods()) { + checkInterfaceMethod(method); + } } } } @@ -282,15 +288,17 @@ public class FileWriter { // -- starting with N interfaces may also have public or private // static methods, as well as public non-abstract (default) // and private instance methods. - private void checkInterfaceMethod(DexEncodedMethod method) { + private void checkInterfaceMethod(DexEncodedMethod method) throws ApiLevelException { if (application.dexItemFactory.isClassConstructor(method.method)) { return; // Class constructor is always OK. } if (method.accessFlags.isStatic()) { if (!options.canUseDefaultAndStaticInterfaceMethods()) { - throw new CompilationError("Static interface methods are only supported " - + "starting with Android N (--min-api " + Constants.ANDROID_N_API + "): " - + method.method.toSourceString()); + throw new ApiLevelException( + Constants.ANDROID_N_API, + "Android N", + "Static interface methods", + method.method.toSourceString()); } } else { @@ -300,9 +308,11 @@ public class FileWriter { } if (!method.accessFlags.isAbstract() && !method.accessFlags.isPrivate() && !options.canUseDefaultAndStaticInterfaceMethods()) { - throw new CompilationError("Default interface methods are only supported " - + "starting with Android N (--min-api " + Constants.ANDROID_N_API + "): " - + method.method.toSourceString()); + throw new ApiLevelException( + Constants.ANDROID_N_API, + "Android N", + "Default interface methods", + method.method.toSourceString()); } } @@ -310,9 +320,11 @@ public class FileWriter { if (options.canUsePrivateInterfaceMethods()) { return; } - throw new CompilationError("Private interface methods are only supported " - + "starting with Android N (--min-api " + Constants.ANDROID_N_API + "): " - + method.method.toSourceString()); + throw new ApiLevelException( + Constants.ANDROID_N_API, + "Android N", + "Private interface methods", + method.method.toSourceString()); } if (!method.accessFlags.isPublic()) { @@ -353,13 +365,21 @@ public class FileWriter { } private <T extends DexItem> void writeFixedSectionItems(T[] items, int offset, - Consumer<T> writer) { + ItemWriter<T> writer) throws ApiLevelException { assert dest.position() == offset; for (T item : items) { writer.accept(item); } } + /** + * Similar to a {@link Consumer} but throws an {@link ApiLevelException}. + */ + @FunctionalInterface + private interface ItemWriter<T> { + void accept(T t) throws ApiLevelException; + } + private <T extends DexItem> void writeItems(Collection<T> items, Consumer<Integer> offsetSetter, Consumer<T> writer) { writeItems(items, offsetSetter, writer, 1); @@ -660,7 +680,7 @@ public class FileWriter { } } - private void writeMethodHandle(DexMethodHandle methodHandle) { + private void writeMethodHandle(DexMethodHandle methodHandle) throws ApiLevelException { checkThatInvokeCustomIsAllowed(); MethodHandleType methodHandleDexType; switch (methodHandle.type) { @@ -689,7 +709,7 @@ public class FileWriter { dest.putShort((short) 0); // unused } - private void writeCallSite(DexCallSite callSite) { + private void writeCallSite(DexCallSite callSite) throws ApiLevelException { checkThatInvokeCustomIsAllowed(); assert dest.isAligned(4); dest.putInt(mixedSectionOffsets.getOffsetFor(callSite.getEncodedArray())); @@ -1339,10 +1359,13 @@ public class FileWriter { } } - private void checkThatInvokeCustomIsAllowed() { + private void checkThatInvokeCustomIsAllowed() throws ApiLevelException { if (!options.canUseInvokeCustom()) { - throw new CompilationError("Invoke-custom is unsupported before Android O (--min-api " - + Constants.ANDROID_O_API + ")"); + throw new ApiLevelException( + Constants.ANDROID_O_API, + "Android O", + "Invoke-customs", + null /* sourceString */); } } } diff --git a/src/test/java/com/android/tools/r8/RunExamplesAndroidNTest.java b/src/test/java/com/android/tools/r8/RunExamplesAndroidNTest.java index d5f3a2e52..7594b0cf2 100644 --- a/src/test/java/com/android/tools/r8/RunExamplesAndroidNTest.java +++ b/src/test/java/com/android/tools/r8/RunExamplesAndroidNTest.java @@ -130,7 +130,7 @@ public abstract class RunExamplesAndroidNTest<B> { @Test public void staticInterfaceMethodsErrorDueToMinSdk() throws Throwable { - thrown.expect(CompilationError.class); + thrown.expect(ApiLevelException.class); test("staticinterfacemethods-error-due-to-min-sdk", "interfacemethods", "StaticInterfaceMethods") .run(); @@ -146,7 +146,7 @@ public abstract class RunExamplesAndroidNTest<B> { @Test public void defaultMethodsErrorDueToMinSdk() throws Throwable { - thrown.expect(CompilationError.class); + thrown.expect(ApiLevelException.class); test("defaultmethods-error-due-to-min-sdk", "interfacemethods", "DefaultMethods") .run(); diff --git a/src/test/java/com/android/tools/r8/RunExamplesAndroidOTest.java b/src/test/java/com/android/tools/r8/RunExamplesAndroidOTest.java index 38868b6e1..e8d3c9dcb 100644 --- a/src/test/java/com/android/tools/r8/RunExamplesAndroidOTest.java +++ b/src/test/java/com/android/tools/r8/RunExamplesAndroidOTest.java @@ -109,6 +109,9 @@ public abstract class RunExamplesAndroidOTest<B> { if (compilationErrorExpected(testName)) { thrown.expect(CompilationError.class); } + if (minSdkErrorExpected(testName)) { + thrown.expect(ApiLevelException.class); + } String qualifiedMainClass = packageName + "." + mainClass; Path inputFile = Paths.get(EXAMPLE_DIR, packageName + JAR_EXTENSION); @@ -152,8 +155,10 @@ public abstract class RunExamplesAndroidOTest<B> { } private static List<String> compilationErrorExpected = - ImmutableList.of( - "invokepolymorphic-error-due-to-min-sdk", "invokecustom-error-due-to-min-sdk"); + ImmutableList.of("invokepolymorphic-error-due-to-min-sdk"); + + private static List<String> minSdkErrorExpected = + ImmutableList.of("invokecustom-error-due-to-min-sdk"); private static Map<DexVm, List<String>> failsOn = ImmutableMap.of( @@ -211,6 +216,10 @@ public abstract class RunExamplesAndroidOTest<B> { return compilationErrorExpected.contains(testName); } + boolean minSdkErrorExpected(String testName) { + return minSdkErrorExpected.contains(testName); + } + @Test public void invokeCustom() throws Throwable { test("invokecustom", "invokecustom", "InvokeCustom") diff --git a/src/test/java/com/android/tools/r8/ToolHelper.java b/src/test/java/com/android/tools/r8/ToolHelper.java index eee820f9e..663d6bd68 100644 --- a/src/test/java/com/android/tools/r8/ToolHelper.java +++ b/src/test/java/com/android/tools/r8/ToolHelper.java @@ -473,18 +473,18 @@ public class ToolHelper { } public static AndroidApp runR8(R8Command command) - throws ProguardRuleParserException, ExecutionException, IOException { + throws ProguardRuleParserException, ExecutionException, IOException, CompilationException { return runR8(command, null); } public static AndroidApp runR8(R8Command command, Consumer<InternalOptions> optionsConsumer) - throws ProguardRuleParserException, ExecutionException, IOException { + throws ProguardRuleParserException, ExecutionException, IOException, CompilationException { return runR8WithFullResult(command, optionsConsumer).androidApp; } public static CompilationResult runR8WithFullResult( R8Command command, Consumer<InternalOptions> optionsConsumer) - throws ProguardRuleParserException, ExecutionException, IOException { + throws ProguardRuleParserException, ExecutionException, IOException, CompilationException { // TODO(zerny): Should we really be adding the android library in ToolHelper? AndroidApp app = command.getInputApp(); if (app.getLibraryResourceProviders().isEmpty()) { @@ -536,12 +536,12 @@ public class ToolHelper { return runD8(D8Command.builder(app).build(), optionsConsumer); } - public static AndroidApp runD8(D8Command command) throws IOException { + public static AndroidApp runD8(D8Command command) throws IOException, CompilationException { return runD8(command, null); } public static AndroidApp runD8(D8Command command, Consumer<InternalOptions> optionsConsumer) - throws IOException { + throws IOException, CompilationException { InternalOptions options = command.getInternalOptions(); if (optionsConsumer != null) { optionsConsumer.accept(options); diff --git a/src/test/java/com/android/tools/r8/internal/CompilationTestBase.java b/src/test/java/com/android/tools/r8/internal/CompilationTestBase.java index 0b3726c86..91ecb4f92 100644 --- a/src/test/java/com/android/tools/r8/internal/CompilationTestBase.java +++ b/src/test/java/com/android/tools/r8/internal/CompilationTestBase.java @@ -57,7 +57,7 @@ public abstract class CompilationTestBase { } public AndroidApp runAndCheckVerification(D8Command command, String referenceApk) - throws IOException, ExecutionException { + throws IOException, ExecutionException, CompilationException { return checkVerification(ToolHelper.runD8(command), referenceApk); } |