summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIvan Gavrilovic <gavra@google.com>2018-05-08 02:26:09 -0700
committerandroid-build-merger <android-build-merger@google.com>2018-05-08 02:26:09 -0700
commit2b50d295f5acc8ddf8924cd6536dfbfe45965ade (patch)
tree74deac1e16e97c2c13f226cd4635cd65abf19303
parent301a69dfe6fbb59072b6c1af278ec31c10cbdf35 (diff)
parent6beb00b4744298d2ef28b6590c31b6848885b28d (diff)
downloaddesugar-2b50d295f5acc8ddf8924cd6536dfbfe45965ade.tar.gz
Merge remote-tracking branch upstream-master into master am: 9d2aa11004android-o-mr1-iot-release-1.0.4android-o-mr1-iot-release-1.0.3
am: 6beb00b474 Change-Id: I5f801929d952fac02c0b652fb9003295f4bf7820
-rw-r--r--java/com/google/devtools/build/android/desugar/BytecodeTypeInference.java1
-rw-r--r--java/com/google/devtools/build/android/desugar/ClassReaderFactory.java3
-rw-r--r--java/com/google/devtools/build/android/desugar/ClassVsInterface.java1
-rw-r--r--java/com/google/devtools/build/android/desugar/CoreLibraryInvocationRewriter.java96
-rw-r--r--java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java521
-rw-r--r--java/com/google/devtools/build/android/desugar/CorePackageRenamer.java45
-rw-r--r--java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java272
-rw-r--r--java/com/google/devtools/build/android/desugar/Desugar.java265
-rw-r--r--java/com/google/devtools/build/android/desugar/EmulatedInterfaceRewriter.java92
-rw-r--r--java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java54
-rw-r--r--java/com/google/devtools/build/android/desugar/Java7Compatibility.java1
-rw-r--r--java/com/google/devtools/build/android/desugar/LambdaClassFixer.java1
-rw-r--r--java/com/google/devtools/build/android/desugar/LambdaClassMaker.java12
-rw-r--r--java/com/google/devtools/build/android/desugar/LambdaDesugaring.java1
-rw-r--r--java/com/google/devtools/build/android/desugar/LongCompareMethodRewriter.java20
-rw-r--r--java/com/google/devtools/build/android/desugar/ObjectsRequireNonNullMethodRewriter.java31
-rw-r--r--java/com/google/devtools/build/android/desugar/TryWithResourcesRewriter.java59
-rw-r--r--java/com/google/devtools/build/android/desugar/io/BitFlags.java (renamed from java/com/google/devtools/build/android/desugar/BitFlags.java)4
-rw-r--r--java/com/google/devtools/build/android/desugar/io/CoreLibraryRewriter.java (renamed from java/com/google/devtools/build/android/desugar/CoreLibraryRewriter.java)33
-rw-r--r--java/com/google/devtools/build/android/desugar/io/DirectoryInputFileProvider.java (renamed from java/com/google/devtools/build/android/desugar/DirectoryInputFileProvider.java)2
-rw-r--r--java/com/google/devtools/build/android/desugar/io/DirectoryOutputFileProvider.java (renamed from java/com/google/devtools/build/android/desugar/DirectoryOutputFileProvider.java)4
-rw-r--r--java/com/google/devtools/build/android/desugar/io/FieldInfo.java (renamed from java/com/google/devtools/build/android/desugar/FieldInfo.java)4
-rw-r--r--java/com/google/devtools/build/android/desugar/io/HeaderClassLoader.java (renamed from java/com/google/devtools/build/android/desugar/HeaderClassLoader.java)7
-rw-r--r--java/com/google/devtools/build/android/desugar/io/IndexedInputs.java (renamed from java/com/google/devtools/build/android/desugar/IndexedInputs.java)4
-rw-r--r--java/com/google/devtools/build/android/desugar/io/InputFileProvider.java (renamed from java/com/google/devtools/build/android/desugar/InputFileProvider.java)17
-rw-r--r--java/com/google/devtools/build/android/desugar/io/OutputFileProvider.java (renamed from java/com/google/devtools/build/android/desugar/OutputFileProvider.java)17
-rw-r--r--java/com/google/devtools/build/android/desugar/io/ThrowingClassLoader.java27
-rw-r--r--java/com/google/devtools/build/android/desugar/io/ZipInputFileProvider.java (renamed from java/com/google/devtools/build/android/desugar/ZipInputFileProvider.java)2
-rw-r--r--java/com/google/devtools/build/android/desugar/io/ZipOutputFileProvider.java (renamed from java/com/google/devtools/build/android/desugar/ZipOutputFileProvider.java)4
-rw-r--r--java/com/google/devtools/build/android/desugar/scan/KeepReference.java51
-rw-r--r--java/com/google/devtools/build/android/desugar/scan/KeepScanner.java303
-rw-r--r--java/com/google/devtools/build/android/desugar/scan/PrefixReferenceScanner.java405
-rw-r--r--java/com/google/devtools/common/options/Converters.java129
-rw-r--r--java/com/google/devtools/common/options/InvocationPolicyEnforcer.java33
-rw-r--r--java/com/google/devtools/common/options/Option.java18
-rw-r--r--java/com/google/devtools/common/options/OptionDefinition.java7
-rw-r--r--java/com/google/devtools/common/options/OptionFilterDescriptions.java3
-rw-r--r--java/com/google/devtools/common/options/OptionInstanceOrigin.java12
-rw-r--r--java/com/google/devtools/common/options/OptionMetadataTag.java12
-rw-r--r--java/com/google/devtools/common/options/OptionPriority.java77
-rw-r--r--java/com/google/devtools/common/options/OptionValueDescription.java70
-rw-r--r--java/com/google/devtools/common/options/OptionsBase.java3
-rw-r--r--java/com/google/devtools/common/options/OptionsParser.java28
-rw-r--r--java/com/google/devtools/common/options/OptionsParserImpl.java116
-rw-r--r--java/com/google/devtools/common/options/OptionsProvider.java12
-rw-r--r--java/com/google/devtools/common/options/ParsedOptionDescription.java53
-rw-r--r--java/com/google/devtools/common/options/processor/OptionProcessor.java46
-rw-r--r--test/java/com/google/devtools/build/android/desugar/CoreLibrarySupportTest.java455
-rw-r--r--test/java/com/google/devtools/build/android/desugar/CorePackageRenamerTest.java104
-rw-r--r--test/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixerTest.java8
-rw-r--r--test/java/com/google/devtools/build/android/desugar/DesugarJava8FunctionalTest.java8
-rw-r--r--test/java/com/google/devtools/build/android/desugar/DesugarTryWithResourcesFunctionalTest.java30
-rw-r--r--test/java/com/google/devtools/build/android/desugar/Java7CompatibilityTest.java1
-rw-r--r--test/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriterTest.java1
-rw-r--r--test/java/com/google/devtools/build/android/desugar/b72690624_testdata.jarbin0 -> 3088 bytes
-rw-r--r--test/java/com/google/devtools/build/android/desugar/dependencies/MetadataCollectorTest.java6
-rw-r--r--test/java/com/google/devtools/build/android/desugar/desugar_unused_synthetic_close_resource_method_golden.txt8
-rw-r--r--test/java/com/google/devtools/build/android/desugar/io/FieldInfoTest.java (renamed from test/java/com/google/devtools/build/android/desugar/FieldInfoTest.java)2
-rw-r--r--test/java/com/google/devtools/build/android/desugar/io/IndexedInputsTest.java (renamed from test/java/com/google/devtools/build/android/desugar/IndexedInputsTest.java)2
-rw-r--r--test/java/com/google/devtools/build/android/desugar/runtime/ThrowableExtensionTestUtility.java11
-rwxr-xr-xtest/java/com/google/devtools/build/android/desugar/scan/test_keep_scanner.sh27
-rw-r--r--test/java/com/google/devtools/build/android/desugar/scan/testdata/CollectionReferences.java64
-rw-r--r--test/java/com/google/devtools/build/android/desugar/scan/testdata/OverlappingCollectionReferences.java49
-rw-r--r--test/java/com/google/devtools/build/android/desugar/scan/testdata_golden.txt62
-rw-r--r--test/java/com/google/devtools/build/android/desugar/testdata/ClassUsingTryWithResources.java14
-rw-r--r--test/java/com/google/devtools/build/android/desugar/testdata/java8/InterfaceWithInheritedMethods.java44
-rw-r--r--test/java/com/google/devtools/build/android/desugar/testdata_desugared_for_disabling_twr_with_large_minsdkversion_jar_toc_golden.txt1
-rw-r--r--test/java/com/google/devtools/build/android/desugar/testdata_desugared_for_try_with_resources_jar_toc_golden.txt1
-rw-r--r--test/java/com/google/devtools/build/android/desugar/testdata_desugared_jar_toc_golden.txt1
-rw-r--r--test/java/com/google/devtools/build/android/desugar/testdata_desugared_java8_jar_toc_golden.txt4
-rw-r--r--test/java/com/google/devtools/build/android/desugar/testdata_desugared_without_lambda_desugared_jar_toc_golden.txt1
-rw-r--r--test/java/com/google/devtools/build/android/desugar/unused_closed_resource.jarbin0 -> 739 bytes
72 files changed, 3365 insertions, 517 deletions
diff --git a/java/com/google/devtools/build/android/desugar/BytecodeTypeInference.java b/java/com/google/devtools/build/android/desugar/BytecodeTypeInference.java
index 783069f..ce36071 100644
--- a/java/com/google/devtools/build/android/desugar/BytecodeTypeInference.java
+++ b/java/com/google/devtools/build/android/desugar/BytecodeTypeInference.java
@@ -19,6 +19,7 @@ import static com.google.common.base.Preconditions.checkState;
import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.android.desugar.io.BitFlags;
import java.util.ArrayList;
import java.util.Optional;
import javax.annotation.Nullable;
diff --git a/java/com/google/devtools/build/android/desugar/ClassReaderFactory.java b/java/com/google/devtools/build/android/desugar/ClassReaderFactory.java
index bae5251..aff9bab 100644
--- a/java/com/google/devtools/build/android/desugar/ClassReaderFactory.java
+++ b/java/com/google/devtools/build/android/desugar/ClassReaderFactory.java
@@ -13,6 +13,9 @@
// limitations under the License.
package com.google.devtools.build.android.desugar;
+import com.google.devtools.build.android.desugar.io.CoreLibraryRewriter;
+import com.google.devtools.build.android.desugar.io.IndexedInputs;
+import com.google.devtools.build.android.desugar.io.InputFileProvider;
import java.io.IOException;
import java.io.InputStream;
import javax.annotation.Nullable;
diff --git a/java/com/google/devtools/build/android/desugar/ClassVsInterface.java b/java/com/google/devtools/build/android/desugar/ClassVsInterface.java
index cb62deb..2724454 100644
--- a/java/com/google/devtools/build/android/desugar/ClassVsInterface.java
+++ b/java/com/google/devtools/build/android/desugar/ClassVsInterface.java
@@ -16,6 +16,7 @@ package com.google.devtools.build.android.desugar;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
+import com.google.devtools.build.android.desugar.io.BitFlags;
import java.util.HashMap;
import javax.annotation.Nullable;
import org.objectweb.asm.ClassReader;
diff --git a/java/com/google/devtools/build/android/desugar/CoreLibraryInvocationRewriter.java b/java/com/google/devtools/build/android/desugar/CoreLibraryInvocationRewriter.java
new file mode 100644
index 0000000..381a344
--- /dev/null
+++ b/java/com/google/devtools/build/android/desugar/CoreLibraryInvocationRewriter.java
@@ -0,0 +1,96 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.desugar;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+
+/**
+ * Rewriter of default and static interface methods defined in some core libraries.
+ *
+ * <p>This is conceptually similar to call site rewriting in {@link InterfaceDesugaring} but here
+ * we're doing it for certain bootclasspath methods and in particular for invokeinterface and
+ * invokevirtual, which are ignored in regular {@link InterfaceDesugaring}.
+ */
+public class CoreLibraryInvocationRewriter extends ClassVisitor {
+
+ private final CoreLibrarySupport support;
+
+ public CoreLibraryInvocationRewriter(ClassVisitor cv, CoreLibrarySupport support) {
+ super(Opcodes.ASM6, cv);
+ this.support = support;
+ }
+
+ @Override
+ public MethodVisitor visitMethod(
+ int access, String name, String desc, String signature, String[] exceptions) {
+ MethodVisitor result = super.visitMethod(access, name, desc, signature, exceptions);
+ return result != null ? new CoreLibraryMethodInvocationRewriter(result) : null;
+ }
+
+ private class CoreLibraryMethodInvocationRewriter extends MethodVisitor {
+ public CoreLibraryMethodInvocationRewriter(MethodVisitor mv) {
+ super(Opcodes.ASM6, mv);
+ }
+
+ @Override
+ public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
+ Class<?> coreInterface =
+ support.getCoreInterfaceRewritingTarget(opcode, owner, name, desc, itf);
+
+ if (coreInterface != null) {
+ String coreInterfaceName = coreInterface.getName().replace('.', '/');
+ name =
+ InterfaceDesugaring.normalizeInterfaceMethodName(
+ name, name.startsWith("lambda$"), opcode == Opcodes.INVOKESTATIC);
+ if (opcode == Opcodes.INVOKESTATIC) {
+ checkState(owner.equals(coreInterfaceName));
+ } else {
+ desc = InterfaceDesugaring.companionDefaultMethodDescriptor(coreInterfaceName, desc);
+ }
+
+ if (opcode == Opcodes.INVOKESTATIC || opcode == Opcodes.INVOKESPECIAL) {
+ checkArgument(itf || opcode == Opcodes.INVOKESPECIAL,
+ "Expected interface to rewrite %s.%s : %s", owner, name, desc);
+ owner = coreInterface.isInterface()
+ ? InterfaceDesugaring.getCompanionClassName(coreInterfaceName)
+ : checkNotNull(support.getMoveTarget(coreInterfaceName, name));
+ } else {
+ checkState(coreInterface.isInterface());
+ owner = coreInterfaceName + "$$Dispatch";
+ }
+
+ opcode = Opcodes.INVOKESTATIC;
+ itf = false;
+ } else {
+ String newOwner = support.getMoveTarget(owner, name);
+ if (newOwner != null) {
+ if (opcode != Opcodes.INVOKESTATIC) {
+ // assuming a static method
+ desc = InterfaceDesugaring.companionDefaultMethodDescriptor(owner, desc);
+ opcode = Opcodes.INVOKESTATIC;
+ }
+ owner = newOwner;
+ itf = false; // assuming a class
+ }
+ }
+ super.visitMethodInsn(opcode, owner, name, desc, itf);
+ }
+ }
+}
diff --git a/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java b/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java
new file mode 100644
index 0000000..f247074
--- /dev/null
+++ b/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java
@@ -0,0 +1,521 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.desugar;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+import static java.util.stream.Stream.concat;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.LinkedHashMultimap;
+import com.google.common.collect.Multimap;
+import com.google.devtools.build.android.desugar.io.BitFlags;
+import com.google.devtools.build.android.desugar.io.CoreLibraryRewriter;
+import com.google.errorprone.annotations.Immutable;
+import java.lang.reflect.Method;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import javax.annotation.Nullable;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.Label;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.Type;
+
+/**
+ * Helper that keeps track of which core library classes and methods we want to rewrite.
+ */
+class CoreLibrarySupport {
+
+ private static final Object[] EMPTY_FRAME = new Object[0];
+ private static final String[] EMPTY_LIST = new String[0];
+
+ private final CoreLibraryRewriter rewriter;
+ private final ClassLoader targetLoader;
+ /** Internal name prefixes that we want to move to a custom package. */
+ private final ImmutableSet<String> renamedPrefixes;
+ private final ImmutableSet<String> excludeFromEmulation;
+ /** Internal names of interfaces whose default and static interface methods we'll emulate. */
+ private final ImmutableSet<Class<?>> emulatedInterfaces;
+ /** Map from {@code owner#name} core library members to their new owners. */
+ private final ImmutableMap<String, String> memberMoves;
+
+ /** For the collection of definitions of emulated default methods (deterministic iteration). */
+ private final Multimap<String, EmulatedMethod> emulatedDefaultMethods =
+ LinkedHashMultimap.create();
+
+ public CoreLibrarySupport(
+ CoreLibraryRewriter rewriter,
+ ClassLoader targetLoader,
+ List<String> renamedPrefixes,
+ List<String> emulatedInterfaces,
+ List<String> memberMoves,
+ List<String> excludeFromEmulation) {
+ this.rewriter = rewriter;
+ this.targetLoader = targetLoader;
+ checkArgument(
+ renamedPrefixes.stream().allMatch(prefix -> prefix.startsWith("java/")), renamedPrefixes);
+ this.renamedPrefixes = ImmutableSet.copyOf(renamedPrefixes);
+ this.excludeFromEmulation = ImmutableSet.copyOf(excludeFromEmulation);
+
+ ImmutableSet.Builder<Class<?>> classBuilder = ImmutableSet.builder();
+ for (String itf : emulatedInterfaces) {
+ checkArgument(itf.startsWith("java/util/"), itf);
+ Class<?> clazz = loadFromInternal(rewriter.getPrefix() + itf);
+ checkArgument(clazz.isInterface(), itf);
+ classBuilder.add(clazz);
+ }
+ this.emulatedInterfaces = classBuilder.build();
+
+ // We can call isRenamed and rename below b/c we initialized the necessary fields above
+ // Use LinkedHashMap to tolerate identical duplicates
+ LinkedHashMap<String, String> movesBuilder = new LinkedHashMap<>();
+ Splitter splitter = Splitter.on("->").trimResults().omitEmptyStrings();
+ for (String move : memberMoves) {
+ List<String> pair = splitter.splitToList(move);
+ checkArgument(pair.size() == 2, "Doesn't split as expected: %s", move);
+ checkArgument(pair.get(0).startsWith("java/"), "Unexpected member: %s", move);
+ int sep = pair.get(0).indexOf('#');
+ checkArgument(sep > 0 && sep == pair.get(0).lastIndexOf('#'), "invalid member: %s", move);
+ checkArgument(!isRenamedCoreLibrary(pair.get(0).substring(0, sep)),
+ "Original renamed, no need to move it: %s", move);
+ checkArgument(isRenamedCoreLibrary(pair.get(1)), "Target not renamed: %s", move);
+ checkArgument(!this.excludeFromEmulation.contains(pair.get(0)),
+ "Retargeted invocation %s shouldn't overlap with excluded", move);
+
+ String value = renameCoreLibrary(pair.get(1));
+ String existing = movesBuilder.put(pair.get(0), value);
+ checkArgument(existing == null || existing.equals(value),
+ "Two move destinations %s and %s configured for %s", existing, value, pair.get(0));
+ }
+ this.memberMoves = ImmutableMap.copyOf(movesBuilder);
+ }
+
+ public boolean isRenamedCoreLibrary(String internalName) {
+ String unprefixedName = rewriter.unprefix(internalName);
+ if (!unprefixedName.startsWith("java/") || renamedPrefixes.isEmpty()) {
+ return false; // shortcut
+ }
+ // Rename any classes desugar might generate under java/ (for emulated interfaces) as well as
+ // configured prefixes
+ return looksGenerated(unprefixedName)
+ || renamedPrefixes.stream().anyMatch(prefix -> unprefixedName.startsWith(prefix));
+ }
+
+ public String renameCoreLibrary(String internalName) {
+ internalName = rewriter.unprefix(internalName);
+ return (internalName.startsWith("java/"))
+ ? "j$/" + internalName.substring(/* cut away "java/" prefix */ 5)
+ : internalName;
+ }
+
+ @Nullable
+ public String getMoveTarget(String owner, String name) {
+ return memberMoves.get(rewriter.unprefix(owner) + '#' + name);
+ }
+
+ /**
+ * Returns {@code true} for java.* classes or interfaces that are subtypes of emulated interfaces.
+ * Note that implies that this method always returns {@code false} for user-written classes.
+ */
+ public boolean isEmulatedCoreClassOrInterface(String internalName) {
+ return getEmulatedCoreClassOrInterface(internalName) != null;
+ }
+
+ /** Includes the given method definition in any applicable core interface emulation logic. */
+ public void registerIfEmulatedCoreInterface(
+ int access,
+ String owner,
+ String name,
+ String desc,
+ String[] exceptions) {
+ Class<?> emulated = getEmulatedCoreClassOrInterface(owner);
+ if (emulated == null) {
+ return;
+ }
+ checkArgument(emulated.isInterface(), "Shouldn't be called for a class: %s.%s", owner, name);
+ checkArgument(
+ BitFlags.noneSet(
+ access,
+ Opcodes.ACC_ABSTRACT | Opcodes.ACC_NATIVE | Opcodes.ACC_STATIC | Opcodes.ACC_BRIDGE),
+ "Should only be called for default methods: %s.%s", owner, name);
+ emulatedDefaultMethods.put(
+ name + ":" + desc, EmulatedMethod.create(access, emulated, name, desc, exceptions));
+ }
+
+ /**
+ * If the given invocation needs to go through a companion class of an emulated or renamed
+ * core interface, this methods returns that interface. This is a helper method for
+ * {@link CoreLibraryInvocationRewriter}.
+ *
+ * <p>This method can only return non-{@code null} if {@code owner} is a core library type.
+ * It usually returns an emulated interface, unless the given invocation is a super-call to a
+ * core class's implementation of an emulated method that's being moved (other implementations
+ * of emulated methods in core classes are ignored). In that case the class is returned and the
+ * caller can use {@link #getMoveTarget} to find out where to redirect the invokespecial to.
+ */
+ // TODO(kmb): Rethink this API and consider combining it with getMoveTarget().
+ @Nullable
+ public Class<?> getCoreInterfaceRewritingTarget(
+ int opcode, String owner, String name, String desc, boolean itf) {
+ if (looksGenerated(owner)) {
+ // Regular desugaring handles generated classes, no emulation is needed
+ return null;
+ }
+ if (!itf && opcode == Opcodes.INVOKESTATIC) {
+ // Ignore static invocations on classes--they never need rewriting (unless moved but that's
+ // handled separately).
+ return null;
+ }
+ if ("<init>".equals(name)) {
+ return null; // Constructors aren't rewritten
+ }
+
+ Class<?> clazz;
+ if (isRenamedCoreLibrary(owner)) {
+ // For renamed invocation targets we just need to do what InterfaceDesugaring does, that is,
+ // only worry about invokestatic and invokespecial interface invocations; nothing to do for
+ // classes and invokeinterface. InterfaceDesugaring ignores bootclasspath interfaces,
+ // so we have to do its work here for renamed interfaces.
+ if (itf
+ && (opcode == Opcodes.INVOKESTATIC || opcode == Opcodes.INVOKESPECIAL)) {
+ clazz = loadFromInternal(owner);
+ } else {
+ return null;
+ }
+ } else {
+ // If not renamed, see if the owner needs emulation.
+ clazz = getEmulatedCoreClassOrInterface(owner);
+ if (clazz == null) {
+ return null;
+ }
+ }
+ checkArgument(itf == clazz.isInterface(), "%s expected to be interface: %s", owner, itf);
+
+ if (opcode == Opcodes.INVOKESTATIC) {
+ // Static interface invocation always goes to the given owner
+ checkState(itf); // we should've bailed out above.
+ return clazz;
+ }
+
+ // See if the invoked method is a default method, which will need rewriting. For invokespecial
+ // we can only get here if its a default method, and invokestatic we handled above.
+ Method callee = findInterfaceMethod(clazz, name, desc);
+ if (callee != null && callee.isDefault()) {
+ if (isExcluded(callee)) {
+ return null;
+ }
+
+ if (!itf && opcode == Opcodes.INVOKESPECIAL) {
+ // See if the invoked implementation is moved; note we ignore all other overrides in classes
+ Class<?> impl = clazz; // we know clazz is not an interface because !itf
+ while (impl != null) {
+ String implName = impl.getName().replace('.', '/');
+ if (getMoveTarget(implName, name) != null) {
+ return impl;
+ }
+ impl = impl.getSuperclass();
+ }
+ }
+
+ Class<?> result = callee.getDeclaringClass();
+ if (isRenamedCoreLibrary(result.getName().replace('.', '/'))
+ || emulatedInterfaces.stream().anyMatch(emulated -> emulated.isAssignableFrom(result))) {
+ return result;
+ }
+ // We get here if the declaring class is a supertype of an emulated interface. In that case
+ // use the emulated interface instead (since we don't desugar the supertype). Fail in case
+ // there are multiple possibilities.
+ Iterator<Class<?>> roots =
+ emulatedInterfaces
+ .stream()
+ .filter(
+ emulated -> emulated.isAssignableFrom(clazz) && result.isAssignableFrom(emulated))
+ .iterator();
+ checkState(roots.hasNext()); // must exist
+ Class<?> substitute = roots.next();
+ checkState(!roots.hasNext(), "Ambiguous emulation substitute: %s", callee);
+ return substitute;
+ } else {
+ checkArgument(!itf || opcode != Opcodes.INVOKESPECIAL,
+ "Couldn't resolve interface super call %s.super.%s : %s", owner, name, desc);
+ }
+ return null;
+ }
+
+ /**
+ * Returns the given class if it's a core library class or interface with emulated default
+ * methods. This is equivalent to calling {@link #isEmulatedCoreClassOrInterface} and then
+ * just loading the class (using the target class loader).
+ */
+ public Class<?> getEmulatedCoreClassOrInterface(String internalName) {
+ if (looksGenerated(internalName)) {
+ // Regular desugaring handles generated classes, no emulation is needed
+ return null;
+ }
+ {
+ String unprefixedOwner = rewriter.unprefix(internalName);
+ if (!unprefixedOwner.startsWith("java/util/") || isRenamedCoreLibrary(unprefixedOwner)) {
+ return null;
+ }
+ }
+
+ Class<?> clazz = loadFromInternal(internalName);
+ if (emulatedInterfaces.stream().anyMatch(itf -> itf.isAssignableFrom(clazz))) {
+ return clazz;
+ }
+ return null;
+ }
+
+ public void makeDispatchHelpers(GeneratedClassStore store) {
+ HashMap<Class<?>, ClassVisitor> dispatchHelpers = new HashMap<>();
+ for (Collection<EmulatedMethod> group : emulatedDefaultMethods.asMap().values()) {
+ checkState(!group.isEmpty());
+ Class<?> root = group
+ .stream()
+ .map(EmulatedMethod::owner)
+ .max(DefaultMethodClassFixer.SubtypeComparator.INSTANCE)
+ .get();
+ checkState(group.stream().map(m -> m.owner()).allMatch(o -> root.isAssignableFrom(o)),
+ "Not a single unique method: %s", group);
+ String methodName = group.stream().findAny().get().name();
+
+ ImmutableList<Class<?>> customOverrides = findCustomOverrides(root, methodName);
+
+ for (EmulatedMethod methodDefinition : group) {
+ Class<?> owner = methodDefinition.owner();
+ ClassVisitor dispatchHelper = dispatchHelpers.computeIfAbsent(owner, clazz -> {
+ String className = clazz.getName().replace('.', '/') + "$$Dispatch";
+ ClassVisitor result = store.add(className);
+ result.visit(
+ Opcodes.V1_7,
+ // Must be public so dispatch methods can be called from anywhere
+ Opcodes.ACC_SYNTHETIC | Opcodes.ACC_PUBLIC,
+ className,
+ /*signature=*/ null,
+ "java/lang/Object",
+ EMPTY_LIST);
+ return result;
+ });
+
+ // Types to check for before calling methodDefinition's companion, sub- before super-types
+ ImmutableList<Class<?>> typechecks =
+ concat(group.stream().map(EmulatedMethod::owner), customOverrides.stream())
+ .filter(o -> o != owner && owner.isAssignableFrom(o))
+ .distinct() // should already be but just in case
+ .sorted(DefaultMethodClassFixer.SubtypeComparator.INSTANCE)
+ .collect(ImmutableList.toImmutableList());
+ makeDispatchHelperMethod(dispatchHelper, methodDefinition, typechecks);
+ }
+ }
+ }
+
+ private ImmutableList<Class<?>> findCustomOverrides(Class<?> root, String methodName) {
+ ImmutableList.Builder<Class<?>> customOverrides = ImmutableList.builder();
+ for (ImmutableMap.Entry<String, String> move : memberMoves.entrySet()) {
+ // move.getKey is a string <owner>#<name> which we validated in the constructor.
+ // We need to take the string apart here to compare owner and name separately.
+ if (!methodName.equals(move.getKey().substring(move.getKey().indexOf('#') + 1))) {
+ continue;
+ }
+ Class<?> target =
+ loadFromInternal(
+ rewriter.getPrefix() + move.getKey().substring(0, move.getKey().indexOf('#')));
+ if (!root.isAssignableFrom(target)) {
+ continue;
+ }
+ checkState(!target.isInterface(), "can't move emulated interface method: %s", move);
+ customOverrides.add(target);
+ }
+ return customOverrides.build();
+ }
+
+ private void makeDispatchHelperMethod(
+ ClassVisitor helper, EmulatedMethod method, ImmutableList<Class<?>> typechecks) {
+ checkArgument(method.owner().isInterface());
+ String owner = method.owner().getName().replace('.', '/');
+ Type methodType = Type.getMethodType(method.descriptor());
+ String companionDesc =
+ InterfaceDesugaring.companionDefaultMethodDescriptor(owner, method.descriptor());
+ MethodVisitor dispatchMethod =
+ helper.visitMethod(
+ method.access() | Opcodes.ACC_STATIC,
+ method.name(),
+ companionDesc,
+ /*signature=*/ null, // signature is invalid due to extra "receiver" argument
+ method.exceptions().toArray(EMPTY_LIST));
+
+
+ dispatchMethod.visitCode();
+ {
+ // See if the receiver might come with its own implementation of the method, and call it.
+ // We do this by testing for the interface type created by EmulatedInterfaceRewriter
+ Label fallthrough = new Label();
+ String emulationInterface = renameCoreLibrary(owner);
+ dispatchMethod.visitVarInsn(Opcodes.ALOAD, 0); // load "receiver"
+ dispatchMethod.visitTypeInsn(Opcodes.INSTANCEOF, emulationInterface);
+ dispatchMethod.visitJumpInsn(Opcodes.IFEQ, fallthrough);
+ dispatchMethod.visitVarInsn(Opcodes.ALOAD, 0); // load "receiver"
+ dispatchMethod.visitTypeInsn(Opcodes.CHECKCAST, emulationInterface);
+
+ visitLoadArgs(dispatchMethod, methodType, 1 /* receiver already loaded above */);
+ dispatchMethod.visitMethodInsn(
+ Opcodes.INVOKEINTERFACE,
+ emulationInterface,
+ method.name(),
+ method.descriptor(),
+ /*itf=*/ true);
+ dispatchMethod.visitInsn(methodType.getReturnType().getOpcode(Opcodes.IRETURN));
+
+ dispatchMethod.visitLabel(fallthrough);
+ // Trivial frame for the branch target: same empty stack as before
+ dispatchMethod.visitFrame(Opcodes.F_SAME, 0, EMPTY_FRAME, 0, EMPTY_FRAME);
+ }
+
+ // Next, check for subtypes with specialized implementations and call them
+ for (Class<?> tested : typechecks) {
+ Label fallthrough = new Label();
+ String testedName = tested.getName().replace('.', '/');
+ // In case of a class this must be a member move; for interfaces use the companion.
+ String target =
+ tested.isInterface()
+ ? InterfaceDesugaring.getCompanionClassName(testedName)
+ : checkNotNull(memberMoves.get(rewriter.unprefix(testedName) + '#' + method.name()));
+ dispatchMethod.visitVarInsn(Opcodes.ALOAD, 0); // load "receiver"
+ dispatchMethod.visitTypeInsn(Opcodes.INSTANCEOF, testedName);
+ dispatchMethod.visitJumpInsn(Opcodes.IFEQ, fallthrough);
+ dispatchMethod.visitVarInsn(Opcodes.ALOAD, 0); // load "receiver"
+ dispatchMethod.visitTypeInsn(Opcodes.CHECKCAST, testedName); // make verifier happy
+
+ visitLoadArgs(dispatchMethod, methodType, 1 /* receiver already loaded above */);
+ dispatchMethod.visitMethodInsn(
+ Opcodes.INVOKESTATIC,
+ target,
+ method.name(),
+ InterfaceDesugaring.companionDefaultMethodDescriptor(testedName, method.descriptor()),
+ /*itf=*/ false);
+ dispatchMethod.visitInsn(methodType.getReturnType().getOpcode(Opcodes.IRETURN));
+
+ dispatchMethod.visitLabel(fallthrough);
+ // Trivial frame for the branch target: same empty stack as before
+ dispatchMethod.visitFrame(Opcodes.F_SAME, 0, EMPTY_FRAME, 0, EMPTY_FRAME);
+ }
+
+ // Call static type's default implementation in companion class
+ dispatchMethod.visitVarInsn(Opcodes.ALOAD, 0); // load "receiver"
+ visitLoadArgs(dispatchMethod, methodType, 1 /* receiver already loaded above */);
+ dispatchMethod.visitMethodInsn(
+ Opcodes.INVOKESTATIC,
+ InterfaceDesugaring.getCompanionClassName(owner),
+ method.name(),
+ companionDesc,
+ /*itf=*/ false);
+ dispatchMethod.visitInsn(methodType.getReturnType().getOpcode(Opcodes.IRETURN));
+
+ dispatchMethod.visitMaxs(0, 0);
+ dispatchMethod.visitEnd();
+ }
+
+ private boolean isExcluded(Method method) {
+ String unprefixedOwner =
+ rewriter.unprefix(method.getDeclaringClass().getName().replace('.', '/'));
+ return excludeFromEmulation.contains(unprefixedOwner + "#" + method.getName());
+ }
+
+ private Class<?> loadFromInternal(String internalName) {
+ try {
+ return targetLoader.loadClass(internalName.replace('/', '.'));
+ } catch (ClassNotFoundException e) {
+ throw (NoClassDefFoundError) new NoClassDefFoundError().initCause(e);
+ }
+ }
+
+ private static Method findInterfaceMethod(Class<?> clazz, String name, String desc) {
+ return collectImplementedInterfaces(clazz, new LinkedHashSet<>())
+ .stream()
+ // search more subtypes before supertypes
+ .sorted(DefaultMethodClassFixer.SubtypeComparator.INSTANCE)
+ .map(itf -> findMethod(itf, name, desc))
+ .filter(Objects::nonNull)
+ .findFirst()
+ .orElse((Method) null);
+ }
+
+ private static Method findMethod(Class<?> clazz, String name, String desc) {
+ for (Method m : clazz.getMethods()) {
+ if (m.getName().equals(name) && Type.getMethodDescriptor(m).equals(desc)) {
+ return m;
+ }
+ }
+ return null;
+ }
+
+ private static Set<Class<?>> collectImplementedInterfaces(Class<?> clazz, Set<Class<?>> dest) {
+ if (clazz.isInterface()) {
+ if (!dest.add(clazz)) {
+ return dest;
+ }
+ } else if (clazz.getSuperclass() != null) {
+ collectImplementedInterfaces(clazz.getSuperclass(), dest);
+ }
+
+ for (Class<?> itf : clazz.getInterfaces()) {
+ collectImplementedInterfaces(itf, dest);
+ }
+ return dest;
+ }
+
+ /**
+ * Emits instructions to load a method's parameters as arguments of a method call assumed to have
+ * compatible descriptor, starting at the given local variable slot.
+ */
+ private static void visitLoadArgs(MethodVisitor dispatchMethod, Type neededType, int slot) {
+ for (Type arg : neededType.getArgumentTypes()) {
+ dispatchMethod.visitVarInsn(arg.getOpcode(Opcodes.ILOAD), slot);
+ slot += arg.getSize();
+ }
+ }
+
+ /** Checks whether the given class is (likely) generated by desugar itself. */
+ private static boolean looksGenerated(String owner) {
+ return owner.contains("$$Lambda$") || owner.endsWith("$$CC") || owner.endsWith("$$Dispatch");
+ }
+
+ @AutoValue
+ @Immutable
+ abstract static class EmulatedMethod {
+ public static EmulatedMethod create(
+ int access, Class<?> owner, String name, String desc, @Nullable String[] exceptions) {
+ return new AutoValue_CoreLibrarySupport_EmulatedMethod(access, owner, name, desc,
+ exceptions != null ? ImmutableList.copyOf(exceptions) : ImmutableList.of());
+ }
+
+ abstract int access();
+ abstract Class<?> owner();
+ abstract String name();
+ abstract String descriptor();
+ abstract ImmutableList<String> exceptions();
+ }
+}
diff --git a/java/com/google/devtools/build/android/desugar/CorePackageRenamer.java b/java/com/google/devtools/build/android/desugar/CorePackageRenamer.java
new file mode 100644
index 0000000..5f1dc2e
--- /dev/null
+++ b/java/com/google/devtools/build/android/desugar/CorePackageRenamer.java
@@ -0,0 +1,45 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.desugar;
+
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.commons.ClassRemapper;
+import org.objectweb.asm.commons.Remapper;
+
+/**
+ * A visitor that renames packages so configured using {@link CoreLibrarySupport}..
+ */
+class CorePackageRenamer extends ClassRemapper {
+
+ public CorePackageRenamer(ClassVisitor cv, CoreLibrarySupport support) {
+ super(cv, new CorePackageRemapper(support));
+ }
+
+ private static final class CorePackageRemapper extends Remapper {
+ private final CoreLibrarySupport support;
+
+ private CorePackageRemapper(CoreLibrarySupport support) {
+ this.support = support;
+ }
+
+ public boolean isRenamed(String owner) {
+ return support.isRenamedCoreLibrary(owner);
+ }
+
+ @Override
+ public String map(String typeName) {
+ return isRenamed(typeName) ? support.renameCoreLibrary(typeName) : typeName;
+ }
+ }
+}
diff --git a/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java b/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java
index 1aaf0b6..1de48bf 100644
--- a/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java
+++ b/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java
@@ -18,11 +18,15 @@ import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.android.desugar.io.BitFlags;
+import java.lang.reflect.Method;
+import java.util.Arrays;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.TreeSet;
+import javax.annotation.Nullable;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
@@ -44,6 +48,7 @@ public class DefaultMethodClassFixer extends ClassVisitor {
private final ClassReaderFactory bootclasspath;
private final ClassLoader targetLoader;
private final DependencyCollector depsCollector;
+ @Nullable private final CoreLibrarySupport coreLibrarySupport;
private final HashSet<String> instanceMethods = new HashSet<>();
private boolean isInterface;
@@ -57,10 +62,12 @@ public class DefaultMethodClassFixer extends ClassVisitor {
ClassVisitor dest,
ClassReaderFactory classpath,
DependencyCollector depsCollector,
+ @Nullable CoreLibrarySupport coreLibrarySupport,
ClassReaderFactory bootclasspath,
ClassLoader targetLoader) {
super(Opcodes.ASM6, dest);
this.classpath = classpath;
+ this.coreLibrarySupport = coreLibrarySupport;
this.bootclasspath = bootclasspath;
this.targetLoader = targetLoader;
this.depsCollector = depsCollector;
@@ -88,7 +95,9 @@ public class DefaultMethodClassFixer extends ClassVisitor {
@Override
public void visitEnd() {
- if (!isInterface && defaultMethodsDefined(directInterfaces)) {
+ if (!isInterface
+ && (mayNeedInterfaceStubsForEmulatedSuperclass()
+ || defaultMethodsDefined(directInterfaces))) {
// Inherited methods take precedence over default methods, so visit all superclasses and
// figure out what methods they declare before stubbing in any missing default methods.
recordInheritedMethods();
@@ -190,8 +199,14 @@ public class DefaultMethodClassFixer extends ClassVisitor {
return super.visitMethod(access, name, desc, signature, exceptions);
}
+ private boolean mayNeedInterfaceStubsForEmulatedSuperclass() {
+ return coreLibrarySupport != null
+ && !coreLibrarySupport.isEmulatedCoreClassOrInterface(internalName)
+ && coreLibrarySupport.isEmulatedCoreClassOrInterface(superName);
+ }
+
private void stubMissingDefaultAndBridgeMethods() {
- TreeSet<Class<?>> allInterfaces = new TreeSet<>(InterfaceComparator.INSTANCE);
+ TreeSet<Class<?>> allInterfaces = new TreeSet<>(SubtypeComparator.INSTANCE);
for (String direct : directInterfaces) {
// Loading ensures all transitively implemented interfaces can be loaded, which is necessary
// to produce correct default method stubs in all cases. We could do without classloading but
@@ -203,19 +218,55 @@ public class DefaultMethodClassFixer extends ClassVisitor {
}
Class<?> superclass = loadFromInternal(superName);
+ boolean mayNeedStubsForSuperclass = mayNeedInterfaceStubsForEmulatedSuperclass();
+ if (mayNeedStubsForSuperclass) {
+ // Collect interfaces inherited from emulated superclasses as well, to handle things like
+ // extending AbstractList without explicitly implementing List.
+ for (Class<?> clazz = superclass; clazz != null; clazz = clazz.getSuperclass()) {
+ for (Class<?> itf : superclass.getInterfaces()) {
+ collectInterfaces(itf, allInterfaces);
+ }
+ }
+ }
for (Class<?> interfaceToVisit : allInterfaces) {
// if J extends I, J is allowed to redefine I's default methods. The comparator we used
// above makes sure we visit J before I in that case so we can use J's definition.
- if (superclass != null && interfaceToVisit.isAssignableFrom(superclass)) {
- // superclass already implements this interface, so we must skip it. The superclass will
- // be similarly rewritten or comes from the bootclasspath; either way we don't need to and
- // shouldn't stub default methods for this interface.
+ if (!mayNeedStubsForSuperclass && interfaceToVisit.isAssignableFrom(superclass)) {
+ // superclass is also rewritten and already implements this interface, so we _must_ skip it.
continue;
}
- stubMissingDefaultAndBridgeMethods(interfaceToVisit.getName().replace('.', '/'));
+ stubMissingDefaultAndBridgeMethods(
+ interfaceToVisit.getName().replace('.', '/'), mayNeedStubsForSuperclass);
}
}
+ private void stubMissingDefaultAndBridgeMethods(
+ String implemented, boolean mayNeedStubsForSuperclass) {
+ ClassReader bytecode;
+ boolean isBootclasspath;
+ if (bootclasspath.isKnown(implemented)) {
+ if (coreLibrarySupport != null
+ && (coreLibrarySupport.isRenamedCoreLibrary(implemented)
+ || coreLibrarySupport.isEmulatedCoreClassOrInterface(implemented))) {
+ bytecode = checkNotNull(bootclasspath.readIfKnown(implemented), implemented);
+ isBootclasspath = true;
+ } else {
+ // Default methods from interfaces on the bootclasspath that we're not renaming or emulating
+ // are assumed available at runtime, so just ignore them.
+ return;
+ }
+ } else {
+ bytecode =
+ checkNotNull(
+ classpath.readIfKnown(implemented),
+ "Couldn't find interface %s implemented by %s", implemented, internalName);
+ isBootclasspath = false;
+ }
+ bytecode.accept(
+ new DefaultMethodStubber(isBootclasspath, mayNeedStubsForSuperclass),
+ ClassReader.SKIP_DEBUG);
+ }
+
private Class<?> loadFromInternal(String internalName) {
try {
return targetLoader.loadClass(internalName.replace('/', '.'));
@@ -236,7 +287,8 @@ public class DefaultMethodClassFixer extends ClassVisitor {
}
private void recordInheritedMethods() {
- InstanceMethodRecorder recorder = new InstanceMethodRecorder();
+ InstanceMethodRecorder recorder =
+ new InstanceMethodRecorder(mayNeedInterfaceStubsForEmulatedSuperclass());
String internalName = superName;
while (internalName != null) {
ClassReader bytecode = bootclasspath.readIfKnown(internalName);
@@ -313,26 +365,38 @@ public class DefaultMethodClassFixer extends ClassVisitor {
*/
private boolean defaultMethodsDefined(ImmutableList<String> interfaces) {
for (String implemented : interfaces) {
+ ClassReader bytecode;
if (bootclasspath.isKnown(implemented)) {
- continue;
- }
- ClassReader bytecode = classpath.readIfKnown(implemented);
- if (bytecode == null) {
- // Interface isn't on the classpath, which indicates incomplete classpaths. Record missing
- // dependency so we can check it later. If we don't check then we may get runtime failures
- // or wrong behavior from default methods that should've been stubbed in.
- // TODO(kmb): Print a warning so people can start fixing their deps?
- depsCollector.missingImplementedInterface(internalName, implemented);
+ if (coreLibrarySupport != null
+ && coreLibrarySupport.isEmulatedCoreClassOrInterface(implemented)) {
+ return true; // need to stub in emulated interface methods such as Collection.stream()
+ } else if (coreLibrarySupport != null
+ && coreLibrarySupport.isRenamedCoreLibrary(implemented)) {
+ // Check default methods of renamed interfaces
+ bytecode = checkNotNull(bootclasspath.readIfKnown(implemented), implemented);
+ } else {
+ continue;
+ }
} else {
- // Class in classpath and bootclasspath is a bad idea but in any event, assume the
- // bootclasspath will take precedence like in a classloader.
- // We can skip code attributes as we just need to find default methods to stub.
- DefaultMethodFinder finder = new DefaultMethodFinder();
- bytecode.accept(finder, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG);
- if (finder.foundDefaultMethods()) {
- return true;
+ bytecode = classpath.readIfKnown(implemented);
+ if (bytecode == null) {
+ // Interface isn't on the classpath, which indicates incomplete classpaths. Record missing
+ // dependency so we can check it later. If we don't check then we may get runtime
+ // failures or wrong behavior from default methods that should've been stubbed in.
+ // TODO(kmb): Print a warning so people can start fixing their deps?
+ depsCollector.missingImplementedInterface(internalName, implemented);
+ continue;
}
}
+
+ // Class in classpath and bootclasspath is a bad idea but in any event, assume the
+ // bootclasspath will take precedence like in a classloader.
+ // We can skip code attributes as we just need to find default methods to stub.
+ DefaultMethodFinder finder = new DefaultMethodFinder();
+ bytecode.accept(finder, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG);
+ if (finder.foundDefaultMethods()) {
+ return true;
+ }
}
return false;
}
@@ -365,30 +429,22 @@ public class DefaultMethodClassFixer extends ClassVisitor {
&& !instanceMethods.contains(name + ":" + desc);
}
- private void stubMissingDefaultAndBridgeMethods(String implemented) {
- if (bootclasspath.isKnown(implemented)) {
- // Default methods on the bootclasspath will be available at runtime, so just ignore them.
- return;
- }
- ClassReader bytecode =
- checkNotNull(
- classpath.readIfKnown(implemented),
- "Couldn't find interface %s implemented by %s",
- implemented,
- internalName);
- bytecode.accept(new DefaultMethodStubber(), ClassReader.SKIP_DEBUG);
- }
-
/**
* Visitor for interfaces that produces delegates in the class visited by the outer {@link
* DefaultMethodClassFixer} for every default method encountered.
*/
private class DefaultMethodStubber extends ClassVisitor {
+ private final boolean isBootclasspathInterface;
+ private final boolean mayNeedStubsForSuperclass;
+
private String stubbedInterfaceName;
- public DefaultMethodStubber() {
+ public DefaultMethodStubber(
+ boolean isBootclasspathInterface, boolean mayNeedStubsForSuperclass) {
super(Opcodes.ASM6);
+ this.isBootclasspathInterface = isBootclasspathInterface;
+ this.mayNeedStubsForSuperclass = mayNeedStubsForSuperclass;
}
@Override
@@ -413,8 +469,11 @@ public class DefaultMethodClassFixer extends ClassVisitor {
// definitions conflict, but see stubMissingDefaultMethods() for how we deal with default
// methods redefined in interfaces extending another.
recordIfInstanceMethod(access, name, desc);
- depsCollector.assumeCompanionClass(
- internalName, InterfaceDesugaring.getCompanionClassName(stubbedInterfaceName));
+ if (!isBootclasspathInterface) {
+ // Don't record these dependencies, as we can't check them
+ depsCollector.assumeCompanionClass(
+ internalName, InterfaceDesugaring.getCompanionClassName(stubbedInterfaceName));
+ }
// Add this method to the class we're desugaring and stub in a body to call the default
// implementation in the interface's companion class. ijar omits these methods when setting
@@ -424,6 +483,21 @@ public class DefaultMethodClassFixer extends ClassVisitor {
MethodVisitor stubMethod =
DefaultMethodClassFixer.this.visitMethod(access, name, desc, (String) null, exceptions);
+ String receiverName = stubbedInterfaceName;
+ String owner = InterfaceDesugaring.getCompanionClassName(stubbedInterfaceName);
+ if (mayNeedStubsForSuperclass) {
+ // Reflect what CoreLibraryInvocationRewriter would do if it encountered a super-call to a
+ // moved implementation of an emulated method. Equivalent to emitting the invokespecial
+ // super call here and relying on CoreLibraryInvocationRewriter for the rest
+ Class<?> emulatedImplementation =
+ coreLibrarySupport.getCoreInterfaceRewritingTarget(
+ Opcodes.INVOKESPECIAL, superName, name, desc, /*itf=*/ false);
+ if (emulatedImplementation != null && !emulatedImplementation.isInterface()) {
+ receiverName = emulatedImplementation.getName().replace('.', '/');
+ owner = checkNotNull(coreLibrarySupport.getMoveTarget(receiverName, name));
+ }
+ }
+
int slot = 0;
stubMethod.visitVarInsn(Opcodes.ALOAD, slot++); // load the receiver
Type neededType = Type.getMethodType(desc);
@@ -433,30 +507,92 @@ public class DefaultMethodClassFixer extends ClassVisitor {
}
stubMethod.visitMethodInsn(
Opcodes.INVOKESTATIC,
- InterfaceDesugaring.getCompanionClassName(stubbedInterfaceName),
+ owner,
name,
- InterfaceDesugaring.companionDefaultMethodDescriptor(stubbedInterfaceName, desc),
- /*itf*/ false);
+ InterfaceDesugaring.companionDefaultMethodDescriptor(receiverName, desc),
+ /*itf=*/ false);
stubMethod.visitInsn(neededType.getReturnType().getOpcode(Opcodes.IRETURN));
stubMethod.visitMaxs(0, 0); // rely on class writer to compute these
stubMethod.visitEnd();
- return null;
+ return null; // don't visit the visited interface's default method
} else if (shouldStubAsBridgeDefaultMethod(access, name, desc)) {
recordIfInstanceMethod(access, name, desc);
- // For bridges we just copy their bodies instead of going through the companion class.
- // Meanwhile, we also need to desugar the copied method bodies, so that any calls to
- // interface methods are correctly handled.
- return new InterfaceDesugaring.InterfaceInvocationRewriter(
- DefaultMethodClassFixer.this.visitMethod(access, name, desc, (String) null, exceptions),
- stubbedInterfaceName,
- bootclasspath,
- depsCollector,
- internalName);
+ MethodVisitor stubMethod =
+ DefaultMethodClassFixer.this.visitMethod(access, name, desc, (String) null, exceptions);
+ // If we're visiting a bootclasspath interface then we most likely don't have the code.
+ // That means we can't just copy the method bodies as we're trying to do below.
+ if (isBootclasspathInterface) {
+ // Synthesize a "bridge" method that calls the true implementation
+ Method bridged = findBridgedMethod(name, desc);
+ checkState(bridged != null,
+ "TODO: Can't stub core interface bridge method %s.%s %s in %s",
+ stubbedInterfaceName, name, desc, internalName);
+
+ int slot = 0;
+ stubMethod.visitVarInsn(Opcodes.ALOAD, slot++); // load the receiver
+ Type neededType = Type.getType(bridged);
+ for (Type arg : neededType.getArgumentTypes()) {
+ // TODO(b/73586397): insert downcasts if necessary
+ stubMethod.visitVarInsn(arg.getOpcode(Opcodes.ILOAD), slot);
+ slot += arg.getSize();
+ }
+ // Just call the bridged method directly on the visited class using invokevirtual
+ stubMethod.visitMethodInsn(
+ Opcodes.INVOKEVIRTUAL,
+ internalName,
+ name,
+ neededType.getDescriptor(),
+ /*itf=*/ false);
+ stubMethod.visitInsn(neededType.getReturnType().getOpcode(Opcodes.IRETURN));
+
+ stubMethod.visitMaxs(0, 0); // rely on class writer to compute these
+ stubMethod.visitEnd();
+ return null; // don't visit the visited interface's bridge method
+ } else {
+ // For bridges we just copy their bodies instead of going through the companion class.
+ // Meanwhile, we also need to desugar the copied method bodies, so that any calls to
+ // interface methods are correctly handled.
+ return new InterfaceDesugaring.InterfaceInvocationRewriter(
+ stubMethod,
+ stubbedInterfaceName,
+ bootclasspath,
+ targetLoader,
+ depsCollector,
+ internalName);
+ }
} else {
- return null; // we don't care about the actual code in these methods
+ return null; // not a default or bridge method or the class already defines this method.
}
}
+
+ /**
+ * Returns a non-bridge interface method with given name that a method with the given descriptor
+ * can bridge to, if any such method can be found.
+ */
+ @Nullable
+ private Method findBridgedMethod(String name, String desc) {
+ Type[] paramTypes = Type.getArgumentTypes(desc);
+ Class<?> itf = loadFromInternal(stubbedInterfaceName);
+ checkArgument(itf.isInterface(), "Should be an interface: %s", stubbedInterfaceName);
+ Method result = null;
+ for (Method m : itf.getDeclaredMethods()) {
+ if (m.isBridge()) {
+ continue;
+ }
+ if (!m.getName().equals(name)) {
+ continue;
+ }
+ // For now, only support specialized return types (which don't require casts)
+ // TODO(b/73586397): Make this work for other kinds of bridges in core library interfaces
+ if (Arrays.equals(paramTypes, Type.getArgumentTypes(m))) {
+ checkState(result == null,
+ "Found multiple bridge target %s and %s for descriptor %s", result, m, desc);
+ return result = m;
+ }
+ }
+ return result;
+ }
}
/**
@@ -510,8 +646,13 @@ public class DefaultMethodClassFixer extends ClassVisitor {
private class InstanceMethodRecorder extends ClassVisitor {
- public InstanceMethodRecorder() {
+ private final boolean ignoreEmulatedMethods;
+
+ private String className;
+
+ public InstanceMethodRecorder(boolean ignoreEmulatedMethods) {
super(Opcodes.ASM6);
+ this.ignoreEmulatedMethods = ignoreEmulatedMethods;
}
@Override
@@ -523,12 +664,23 @@ public class DefaultMethodClassFixer extends ClassVisitor {
String superName,
String[] interfaces) {
checkArgument(BitFlags.noneSet(access, Opcodes.ACC_INTERFACE));
+ className = name; // updated every time we start visiting another superclass
super.visit(version, access, name, signature, superName, interfaces);
}
@Override
public MethodVisitor visitMethod(
int access, String name, String desc, String signature, String[] exceptions) {
+ if (ignoreEmulatedMethods
+ && BitFlags.noneSet(access, Opcodes.ACC_STATIC) // short-circuit
+ && coreLibrarySupport.getCoreInterfaceRewritingTarget(
+ Opcodes.INVOKEVIRTUAL, className, name, desc, /*itf=*/ false)
+ != null) {
+ // *don't* record emulated core library method implementations in immediate subclasses of
+ // emulated core library clasess so that they can be stubbed (since the inherited
+ // implementation may be missing at runtime).
+ return null;
+ }
recordIfInstanceMethod(access, name, desc);
return null;
}
@@ -594,17 +746,17 @@ public class DefaultMethodClassFixer extends ClassVisitor {
}
}
- /** Comparator for interfaces that compares by whether interfaces extend one another. */
- enum InterfaceComparator implements Comparator<Class<?>> {
+ /** Comparator for classes and interfaces that compares by whether subtyping relationship. */
+ enum SubtypeComparator implements Comparator<Class<?>> {
+ /** Orders subtypes before supertypes and breaks ties lexicographically. */
INSTANCE;
@Override
public int compare(Class<?> o1, Class<?> o2) {
- checkArgument(o1.isInterface());
- checkArgument(o2.isInterface());
if (o1 == o2) {
return 0;
}
+ // order subtypes before supertypes
if (o1.isAssignableFrom(o2)) { // o1 is supertype of o2
return 1; // we want o1 to come after o2
}
diff --git a/java/com/google/devtools/build/android/desugar/Desugar.java b/java/com/google/devtools/build/android/desugar/Desugar.java
index 5d3df4a..c176f9c 100644
--- a/java/com/google/devtools/build/android/desugar/Desugar.java
+++ b/java/com/google/devtools/build/android/desugar/Desugar.java
@@ -17,7 +17,6 @@ import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.devtools.build.android.desugar.LambdaClassMaker.LAMBDA_METAFACTORY_DUMPER_PROPERTY;
-import static java.nio.charset.StandardCharsets.ISO_8859_1;
import com.google.auto.value.AutoValue;
import com.google.common.annotations.VisibleForTesting;
@@ -28,17 +27,24 @@ import com.google.common.io.ByteStreams;
import com.google.common.io.Closer;
import com.google.devtools.build.android.Converters.ExistingPathConverter;
import com.google.devtools.build.android.Converters.PathConverter;
-import com.google.devtools.build.android.desugar.CoreLibraryRewriter.UnprefixingClassWriter;
+import com.google.devtools.build.android.desugar.io.CoreLibraryRewriter;
+import com.google.devtools.build.android.desugar.io.CoreLibraryRewriter.UnprefixingClassWriter;
+import com.google.devtools.build.android.desugar.io.HeaderClassLoader;
+import com.google.devtools.build.android.desugar.io.IndexedInputs;
+import com.google.devtools.build.android.desugar.io.InputFileProvider;
+import com.google.devtools.build.android.desugar.io.OutputFileProvider;
+import com.google.devtools.build.android.desugar.io.ThrowingClassLoader;
import com.google.devtools.common.options.Option;
import com.google.devtools.common.options.OptionDocumentationCategory;
import com.google.devtools.common.options.OptionEffectTag;
-import com.google.devtools.common.options.Options;
import com.google.devtools.common.options.OptionsBase;
-import com.google.errorprone.annotations.MustBeClosed;
+import com.google.devtools.common.options.OptionsParser;
+import com.google.devtools.common.options.ShellQuotedParamsFilePreProcessor;
import java.io.IOError;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
+import java.nio.file.FileSystems;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -195,6 +201,15 @@ class Desugar {
public boolean tolerateMissingDependencies;
@Option(
+ name = "desugar_supported_core_libs",
+ defaultValue = "false",
+ documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+ effectTags = {OptionEffectTag.UNKNOWN},
+ help = "Enable core library desugaring, which requires configuration with related flags."
+ )
+ public boolean desugarCoreLibs;
+
+ @Option(
name = "desugar_interface_method_bodies_if_needed",
defaultValue = "true",
category = "misc",
@@ -248,6 +263,51 @@ class Desugar {
)
public boolean coreLibrary;
+ /** Type prefixes that we'll move to a custom package. */
+ @Option(
+ name = "rewrite_core_library_prefix",
+ defaultValue = "", // ignored
+ allowMultiple = true,
+ documentationCategory = OptionDocumentationCategory.UNDOCUMENTED,
+ effectTags = {OptionEffectTag.UNKNOWN},
+ help = "Assume the given java.* prefixes are desugared."
+ )
+ public List<String> rewriteCoreLibraryPrefixes;
+
+ /** Interfaces whose default and static interface methods we'll emulate. */
+ @Option(
+ name = "emulate_core_library_interface",
+ defaultValue = "", // ignored
+ allowMultiple = true,
+ documentationCategory = OptionDocumentationCategory.UNDOCUMENTED,
+ effectTags = {OptionEffectTag.UNKNOWN},
+ help = "Assume the given java.* interfaces are emulated."
+ )
+ public List<String> emulateCoreLibraryInterfaces;
+
+ /** Members that we will retarget to the given new owner. */
+ @Option(
+ name = "retarget_core_library_member",
+ defaultValue = "", // ignored
+ allowMultiple = true,
+ documentationCategory = OptionDocumentationCategory.UNDOCUMENTED,
+ effectTags = {OptionEffectTag.UNKNOWN},
+ help = "Method invocations to retarget, given as \"class/Name#member->new/class/Name\". "
+ + "The new owner is blindly assumed to exist."
+ )
+ public List<String> retargetCoreLibraryMembers;
+
+ /** Members not to rewrite. */
+ @Option(
+ name = "dont_rewrite_core_library_invocation",
+ defaultValue = "", // ignored
+ allowMultiple = true,
+ documentationCategory = OptionDocumentationCategory.UNDOCUMENTED,
+ effectTags = {OptionEffectTag.UNKNOWN},
+ help = "Method invocations not to rewrite, given as \"class/Name#method\"."
+ )
+ public List<String> dontTouchCoreLibraryMembers;
+
/** Set to work around b/62623509 with JaCoCo versions prior to 0.7.9. */
// TODO(kmb): Remove when Android Studio doesn't need it anymore (see b/37116789)
@Option(
@@ -265,7 +325,7 @@ class Desugar {
private final DesugarOptions options;
private final CoreLibraryRewriter rewriter;
private final LambdaClassMaker lambdas;
- private final GeneratedClassStore store;
+ private final GeneratedClassStore store = new GeneratedClassStore();
private final Set<String> visitedExceptionTypes = new HashSet<>();
/** The counter to record the times of try-with-resources desugaring is invoked. */
private final AtomicInteger numOfTryWithResourcesInvoked = new AtomicInteger();
@@ -282,7 +342,6 @@ class Desugar {
this.options = options;
this.rewriter = new CoreLibraryRewriter(options.coreLibrary ? "__desugar__/" : "");
this.lambdas = new LambdaClassMaker(dumpDirectory);
- this.store = new GeneratedClassStore();
this.outputJava7 = options.minSdkVersion < 24;
this.allowDefaultMethods =
options.desugarInterfaceMethodBodiesIfNeeded || options.minSdkVersion >= 24;
@@ -332,8 +391,8 @@ class Desugar {
Files.isDirectory(inputPath) || !Files.isDirectory(outputPath),
"Input jar file requires an output jar file");
- try (OutputFileProvider outputFileProvider = toOutputFileProvider(outputPath);
- InputFileProvider inputFiles = toInputFileProvider(inputPath)) {
+ try (OutputFileProvider outputFileProvider = OutputFileProvider.create(outputPath);
+ InputFileProvider inputFiles = InputFileProvider.open(inputPath)) {
DependencyCollector depsCollector = createDepsCollector();
IndexedInputs indexedInputFiles = new IndexedInputs(ImmutableList.of(inputFiles));
// Prepend classpath with input file itself so LambdaDesugaring can load classes with
@@ -358,6 +417,17 @@ class Desugar {
ImmutableSet.Builder<String> interfaceLambdaMethodCollector = ImmutableSet.builder();
ClassVsInterface interfaceCache = new ClassVsInterface(classpathReader);
+ CoreLibrarySupport coreLibrarySupport =
+ options.desugarCoreLibs
+ ? new CoreLibrarySupport(
+ rewriter,
+ loader,
+ options.rewriteCoreLibraryPrefixes,
+ options.emulateCoreLibraryInterfaces,
+ options.retargetCoreLibraryMembers,
+ options.dontTouchCoreLibraryMembers)
+ : null;
+
desugarClassesInInput(
inputFiles,
outputFileProvider,
@@ -365,6 +435,7 @@ class Desugar {
classpathReader,
depsCollector,
bootclasspathReader,
+ coreLibrarySupport,
interfaceCache,
interfaceLambdaMethodCollector);
@@ -374,11 +445,14 @@ class Desugar {
classpathReader,
depsCollector,
bootclasspathReader,
+ coreLibrarySupport,
interfaceCache,
interfaceLambdaMethodCollector.build(),
bridgeMethodReader);
- desugarAndWriteGeneratedClasses(outputFileProvider, bootclasspathReader);
+ desugarAndWriteGeneratedClasses(
+ outputFileProvider, loader, bootclasspathReader, coreLibrarySupport);
+
copyThrowableExtensionClass(outputFileProvider);
byte[] depsInfo = depsCollector.toByteArray();
@@ -394,7 +468,7 @@ class Desugar {
}
/**
- * Returns a dependency collector for use with a single input Jar. If
+ * Returns a dependency collector for use with a single input Jar. If
* {@link DesugarOptions#emitDependencyMetadata} is set, this method instantiates the collector
* reflectively to allow compiling and using the desugar tool without this mechanism.
*/
@@ -445,19 +519,20 @@ class Desugar {
@Nullable ClassReaderFactory classpathReader,
DependencyCollector depsCollector,
ClassReaderFactory bootclasspathReader,
+ @Nullable CoreLibrarySupport coreLibrarySupport,
ClassVsInterface interfaceCache,
ImmutableSet.Builder<String> interfaceLambdaMethodCollector)
throws IOException {
- for (String filename : inputFiles) {
- if (OutputFileProvider.DESUGAR_DEPS_FILENAME.equals(filename)) {
+ for (String inputFilename : inputFiles) {
+ if (OutputFileProvider.DESUGAR_DEPS_FILENAME.equals(inputFilename)) {
// TODO(kmb): rule out that this happens or merge input file with what's in depsCollector
continue; // skip as we're writing a new file like this at the end or don't want it
}
- try (InputStream content = inputFiles.getInputStream(filename)) {
+ try (InputStream content = inputFiles.getInputStream(inputFilename)) {
// We can write classes uncompressed since they need to be converted to .dex format
// for Android anyways. Resources are written as they were in the input jar to avoid
// any danger of accidentally uncompressed resources ending up in an .apk.
- if (filename.endsWith(".class")) {
+ if (inputFilename.endsWith(".class")) {
ClassReader reader = rewriter.reader(content);
UnprefixingClassWriter writer = rewriter.writer(ClassWriter.COMPUTE_MAXS);
ClassVisitor visitor =
@@ -466,19 +541,24 @@ class Desugar {
classpathReader,
depsCollector,
bootclasspathReader,
+ coreLibrarySupport,
interfaceCache,
interfaceLambdaMethodCollector,
writer,
reader);
if (writer == visitor) {
// Just copy the input if there are no rewritings
- outputFileProvider.write(filename, reader.b);
+ outputFileProvider.write(inputFilename, reader.b);
} else {
reader.accept(visitor, 0);
+ String filename = writer.getClassName() + ".class";
+ checkState(
+ (options.coreLibrary && coreLibrarySupport != null)
+ || filename.equals(inputFilename));
outputFileProvider.write(filename, writer.toByteArray());
}
} else {
- outputFileProvider.copyFrom(filename, inputFiles);
+ outputFileProvider.copyFrom(inputFilename, inputFiles);
}
}
}
@@ -494,6 +574,7 @@ class Desugar {
@Nullable ClassReaderFactory classpathReader,
DependencyCollector depsCollector,
ClassReaderFactory bootclasspathReader,
+ @Nullable CoreLibrarySupport coreLibrarySupport,
ClassVsInterface interfaceCache,
ImmutableSet<String> interfaceLambdaMethods,
@Nullable ClassReaderFactory bridgeMethodReader)
@@ -525,6 +606,7 @@ class Desugar {
classpathReader,
depsCollector,
bootclasspathReader,
+ coreLibrarySupport,
interfaceCache,
interfaceLambdaMethods,
bridgeMethodReader,
@@ -532,17 +614,26 @@ class Desugar {
writer,
reader);
reader.accept(visitor, 0);
- String filename =
- rewriter.unprefix(lambdaClass.getValue().desiredInternalName()) + ".class";
- outputFileProvider.write(filename, writer.toByteArray());
+ checkState(
+ (options.coreLibrary && coreLibrarySupport != null)
+ || rewriter
+ .unprefix(lambdaClass.getValue().desiredInternalName())
+ .equals(writer.getClassName()));
+ outputFileProvider.write(writer.getClassName() + ".class", writer.toByteArray());
}
}
}
private void desugarAndWriteGeneratedClasses(
- OutputFileProvider outputFileProvider, ClassReaderFactory bootclasspathReader)
+ OutputFileProvider outputFileProvider,
+ ClassLoader loader,
+ ClassReaderFactory bootclasspathReader,
+ @Nullable CoreLibrarySupport coreLibrarySupport)
throws IOException {
// Write out any classes we generated along the way
+ if (coreLibrarySupport != null) {
+ coreLibrarySupport.makeDispatchHelpers(store);
+ }
ImmutableMap<String, ClassNode> generatedClasses = store.drain();
checkState(
generatedClasses.isEmpty() || (allowDefaultMethods && outputJava7),
@@ -552,11 +643,39 @@ class Desugar {
UnprefixingClassWriter writer = rewriter.writer(ClassWriter.COMPUTE_MAXS);
// checkState above implies that we want Java 7 .class files, so send through that visitor.
// Don't need a ClassReaderFactory b/c static interface methods should've been moved.
- ClassVisitor visitor =
- new Java7Compatibility(writer, (ClassReaderFactory) null, bootclasspathReader);
+ ClassVisitor visitor = writer;
+ if (coreLibrarySupport != null) {
+ visitor = new EmulatedInterfaceRewriter(visitor, coreLibrarySupport);
+ visitor = new CorePackageRenamer(visitor, coreLibrarySupport);
+ visitor = new CoreLibraryInvocationRewriter(visitor, coreLibrarySupport);
+ }
+
+ if (!allowTryWithResources) {
+ CloseResourceMethodScanner closeResourceMethodScanner = new CloseResourceMethodScanner();
+ generated.getValue().accept(closeResourceMethodScanner);
+ visitor =
+ new TryWithResourcesRewriter(
+ visitor,
+ loader,
+ visitedExceptionTypes,
+ numOfTryWithResourcesInvoked,
+ closeResourceMethodScanner.hasCloseResourceMethod());
+ }
+ if (!allowCallsToObjectsNonNull) {
+ // Not sure whether there will be implicit null check emitted by javac, so we rerun
+ // the inliner again
+ visitor = new ObjectsRequireNonNullMethodRewriter(visitor, rewriter);
+ }
+ if (!allowCallsToLongCompare) {
+ visitor = new LongCompareMethodRewriter(visitor, rewriter);
+ }
+
+ visitor = new Java7Compatibility(visitor, (ClassReaderFactory) null, bootclasspathReader);
generated.getValue().accept(visitor);
- String filename = rewriter.unprefix(generated.getKey()) + ".class";
- outputFileProvider.write(filename, writer.toByteArray());
+ checkState(
+ (options.coreLibrary && coreLibrarySupport != null)
+ || rewriter.unprefix(generated.getKey()).equals(writer.getClassName()));
+ outputFileProvider.write(writer.getClassName() + ".class", writer.toByteArray());
}
}
@@ -569,6 +688,7 @@ class Desugar {
@Nullable ClassReaderFactory classpathReader,
DependencyCollector depsCollector,
ClassReaderFactory bootclasspathReader,
+ @Nullable CoreLibrarySupport coreLibrarySupport,
ClassVsInterface interfaceCache,
ImmutableSet<String> interfaceLambdaMethods,
@Nullable ClassReaderFactory bridgeMethodReader,
@@ -576,6 +696,13 @@ class Desugar {
UnprefixingClassWriter writer,
ClassReader input) {
ClassVisitor visitor = checkNotNull(writer);
+
+ if (coreLibrarySupport != null) {
+ visitor = new EmulatedInterfaceRewriter(visitor, coreLibrarySupport);
+ visitor = new CorePackageRenamer(visitor, coreLibrarySupport);
+ visitor = new CoreLibraryInvocationRewriter(visitor, coreLibrarySupport);
+ }
+
if (!allowTryWithResources) {
CloseResourceMethodScanner closeResourceMethodScanner = new CloseResourceMethodScanner();
input.accept(closeResourceMethodScanner, ClassReader.SKIP_DEBUG);
@@ -590,10 +717,10 @@ class Desugar {
if (!allowCallsToObjectsNonNull) {
// Not sure whether there will be implicit null check emitted by javac, so we rerun
// the inliner again
- visitor = new ObjectsRequireNonNullMethodRewriter(visitor);
+ visitor = new ObjectsRequireNonNullMethodRewriter(visitor, rewriter);
}
if (!allowCallsToLongCompare) {
- visitor = new LongCompareMethodRewriter(visitor);
+ visitor = new LongCompareMethodRewriter(visitor, rewriter);
}
if (outputJava7) {
// null ClassReaderFactory b/c we don't expect to need it for lambda classes
@@ -601,17 +728,25 @@ class Desugar {
if (options.desugarInterfaceMethodBodiesIfNeeded) {
visitor =
new DefaultMethodClassFixer(
- visitor, classpathReader, depsCollector, bootclasspathReader, loader);
+ visitor,
+ classpathReader,
+ depsCollector,
+ coreLibrarySupport,
+ bootclasspathReader,
+ loader);
visitor =
new InterfaceDesugaring(
visitor,
interfaceCache,
depsCollector,
+ coreLibrarySupport,
bootclasspathReader,
+ loader,
store,
options.legacyJacocoFix);
}
}
+
visitor =
new LambdaClassFixer(
visitor,
@@ -639,11 +774,19 @@ class Desugar {
@Nullable ClassReaderFactory classpathReader,
DependencyCollector depsCollector,
ClassReaderFactory bootclasspathReader,
+ @Nullable CoreLibrarySupport coreLibrarySupport,
ClassVsInterface interfaceCache,
ImmutableSet.Builder<String> interfaceLambdaMethodCollector,
UnprefixingClassWriter writer,
ClassReader input) {
ClassVisitor visitor = checkNotNull(writer);
+
+ if (coreLibrarySupport != null) {
+ visitor = new EmulatedInterfaceRewriter(visitor, coreLibrarySupport);
+ visitor = new CorePackageRenamer(visitor, coreLibrarySupport);
+ visitor = new CoreLibraryInvocationRewriter(visitor, coreLibrarySupport);
+ }
+
if (!allowTryWithResources) {
CloseResourceMethodScanner closeResourceMethodScanner = new CloseResourceMethodScanner();
input.accept(closeResourceMethodScanner, ClassReader.SKIP_DEBUG);
@@ -656,10 +799,10 @@ class Desugar {
closeResourceMethodScanner.hasCloseResourceMethod());
}
if (!allowCallsToObjectsNonNull) {
- visitor = new ObjectsRequireNonNullMethodRewriter(visitor);
+ visitor = new ObjectsRequireNonNullMethodRewriter(visitor, rewriter);
}
if (!allowCallsToLongCompare) {
- visitor = new LongCompareMethodRewriter(visitor);
+ visitor = new LongCompareMethodRewriter(visitor, rewriter);
}
if (!options.onlyDesugarJavac9ForLint) {
if (outputJava7) {
@@ -667,17 +810,25 @@ class Desugar {
if (options.desugarInterfaceMethodBodiesIfNeeded) {
visitor =
new DefaultMethodClassFixer(
- visitor, classpathReader, depsCollector, bootclasspathReader, loader);
+ visitor,
+ classpathReader,
+ depsCollector,
+ coreLibrarySupport,
+ bootclasspathReader,
+ loader);
visitor =
new InterfaceDesugaring(
visitor,
interfaceCache,
depsCollector,
+ coreLibrarySupport,
bootclasspathReader,
+ loader,
store,
options.legacyJacocoFix);
}
}
+
// LambdaDesugaring is relatively expensive, so check first whether we need it. Additionally,
// we need to collect lambda methods referenced by invokedynamic instructions up-front anyway.
// TODO(kmb): Scan constant pool instead of visiting the class to find bootstrap methods etc.
@@ -730,7 +881,7 @@ class Desugar {
} catch (ReflectiveOperationException e) {
// We do not want to crash Desugar, if we cannot load or access these classes or fields.
// We aim to provide better diagnostics. If we cannot, just let it go.
- e.printStackTrace();
+ e.printStackTrace(System.err); // To silence error-prone's complaint.
}
}
@@ -760,13 +911,12 @@ class Desugar {
return dumpDirectory;
}
- private static DesugarOptions parseCommandLineOptions(String[] args) throws IOException {
- if (args.length == 1 && args[0].startsWith("@")) {
- args = Files.readAllLines(Paths.get(args[0].substring(1)), ISO_8859_1).toArray(new String[0]);
- }
- DesugarOptions options =
- Options.parseAndExitUponError(DesugarOptions.class, /*allowResidue=*/ false, args)
- .getOptions();
+ private static DesugarOptions parseCommandLineOptions(String[] args) {
+ OptionsParser parser = OptionsParser.newOptionsParser(DesugarOptions.class);
+ parser.setAllowResidue(false);
+ parser.enableParamsFileSupport(new ShellQuotedParamsFilePreProcessor(FileSystems.getDefault()));
+ parser.parseAndExitUponError(args);
+ DesugarOptions options = parser.getOptions(DesugarOptions.class);
checkArgument(!options.inputJars.isEmpty(), "--input is required");
checkArgument(
@@ -780,6 +930,10 @@ class Desugar {
for (Path path : options.bootclasspath) {
checkArgument(!Files.isDirectory(path), "Bootclasspath entry must be a jar file: %s", path);
}
+ checkArgument(!options.desugarCoreLibs
+ || !options.rewriteCoreLibraryPrefixes.isEmpty()
+ || !options.emulateCoreLibraryInterfaces.isEmpty(),
+ "--desugar_supported_core_libs requires specifying renamed and/or emulated core libraries");
return options;
}
@@ -793,19 +947,6 @@ class Desugar {
return ioPairListbuilder.build();
}
- @VisibleForTesting
- static class ThrowingClassLoader extends ClassLoader {
- @Override
- protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
- if (name.startsWith("java.")) {
- // Use system class loader for java. classes, since ClassLoader.defineClass gets
- // grumpy when those don't come from the standard place.
- return super.loadClass(name, resolve);
- }
- throw new ClassNotFoundException();
- }
- }
-
private static void deleteTreeOnExit(final Path directory) {
Thread shutdownHook =
new Thread() {
@@ -844,26 +985,6 @@ class Desugar {
}
}
- /** Transform a Path to an {@link OutputFileProvider} */
- @MustBeClosed
- private static OutputFileProvider toOutputFileProvider(Path path) throws IOException {
- if (Files.isDirectory(path)) {
- return new DirectoryOutputFileProvider(path);
- } else {
- return new ZipOutputFileProvider(path);
- }
- }
-
- /** Transform a Path to an InputFileProvider that needs to be closed by the caller. */
- @MustBeClosed
- private static InputFileProvider toInputFileProvider(Path path) throws IOException {
- if (Files.isDirectory(path)) {
- return new DirectoryInputFileProvider(path);
- } else {
- return new ZipInputFileProvider(path);
- }
- }
-
/**
* Transform a list of Path to a list of InputFileProvider and register them with the given
* closer.
@@ -874,7 +995,7 @@ class Desugar {
Closer closer, List<Path> paths) throws IOException {
ImmutableList.Builder<InputFileProvider> builder = new ImmutableList.Builder<>();
for (Path path : paths) {
- builder.add(closer.register(toInputFileProvider(path)));
+ builder.add(closer.register(InputFileProvider.open(path)));
}
return builder.build();
}
diff --git a/java/com/google/devtools/build/android/desugar/EmulatedInterfaceRewriter.java b/java/com/google/devtools/build/android/desugar/EmulatedInterfaceRewriter.java
new file mode 100644
index 0000000..355dd97
--- /dev/null
+++ b/java/com/google/devtools/build/android/desugar/EmulatedInterfaceRewriter.java
@@ -0,0 +1,92 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.desugar;
+
+import com.google.devtools.build.android.desugar.io.BitFlags;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.Opcodes;
+
+/**
+ * Visitor that renames emulated interfaces and marks classes that extend emulated interfaces to
+ * also implement the renamed interfaces. {@link DefaultMethodClassFixer} makes sure the requisite
+ * methods are present in all classes implementing the renamed interface. Doing this helps with
+ * dynamic dispatch on emulated interfaces.
+ */
+public class EmulatedInterfaceRewriter extends ClassVisitor {
+
+ private static final String[] EMPTY_ARRAY = new String[0];
+
+ private final CoreLibrarySupport support;
+
+ public EmulatedInterfaceRewriter(ClassVisitor dest, CoreLibrarySupport support) {
+ super(Opcodes.ASM6, dest);
+ this.support = support;
+ }
+
+ @Override
+ public void visit(
+ int version,
+ int access,
+ String name,
+ String signature,
+ String superName,
+ String[] interfaces) {
+ boolean emulated = support.isEmulatedCoreClassOrInterface(name);
+ {
+ // 1. see if we should implement any additional interfaces.
+ // Use LinkedHashSet to dedupe but maintain deterministic order
+ LinkedHashSet<String> newInterfaces = new LinkedHashSet<>();
+ if (interfaces != null && interfaces.length > 0) {
+ // Make classes implementing emulated interfaces also implement the renamed interfaces we
+ // create below. This includes making the renamed interfaces extends each other as needed.
+ Collections.addAll(newInterfaces, interfaces);
+ for (String itf : interfaces) {
+ if (support.isEmulatedCoreClassOrInterface(itf)) {
+ newInterfaces.add(support.renameCoreLibrary(itf));
+ }
+ }
+ }
+ if (!emulated) {
+ // For an immediate subclass of an emulated class, also fill in any interfaces implemented
+ // by superclasses, similar to the additional default method stubbing performed in
+ // DefaultMethodClassFixer in this situation.
+ Class<?> superclass = support.getEmulatedCoreClassOrInterface(superName);
+ while (superclass != null) {
+ for (Class<?> implemented : superclass.getInterfaces()) {
+ String itf = implemented.getName().replace('.', '/');
+ if (support.isEmulatedCoreClassOrInterface(itf)) {
+ newInterfaces.add(support.renameCoreLibrary(itf));
+ }
+ }
+ superclass = superclass.getSuperclass();
+ }
+ }
+ // Update implemented interfaces and signature if we did anything above
+ if (interfaces == null
+ ? !newInterfaces.isEmpty()
+ : interfaces.length != newInterfaces.size()) {
+ interfaces = newInterfaces.toArray(EMPTY_ARRAY);
+ signature = null; // additional interfaces invalidate any signature
+ }
+ }
+
+ // 2. see if we need to rename this interface itself
+ if (BitFlags.isInterface(access) && emulated) {
+ name = support.renameCoreLibrary(name);
+ }
+ super.visit(version, access, name, signature, superName, interfaces);
+ }
+}
diff --git a/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java b/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java
index b8b3ead..e9e3199 100644
--- a/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java
+++ b/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java
@@ -17,6 +17,9 @@ import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
+import com.google.devtools.build.android.desugar.io.BitFlags;
+import com.google.devtools.build.android.desugar.io.FieldInfo;
+import java.lang.reflect.Method;
import javax.annotation.Nullable;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassVisitor;
@@ -46,7 +49,9 @@ class InterfaceDesugaring extends ClassVisitor {
private final ClassVsInterface interfaceCache;
private final DependencyCollector depsCollector;
+ private final CoreLibrarySupport coreLibrarySupport;
private final ClassReaderFactory bootclasspath;
+ private final ClassLoader targetLoader;
private final GeneratedClassStore store;
private final boolean legacyJaCoCo;
@@ -61,13 +66,17 @@ class InterfaceDesugaring extends ClassVisitor {
ClassVisitor dest,
ClassVsInterface interfaceCache,
DependencyCollector depsCollector,
+ @Nullable CoreLibrarySupport coreLibrarySupport,
ClassReaderFactory bootclasspath,
+ ClassLoader targetLoader,
GeneratedClassStore store,
boolean legacyJaCoCo) {
super(Opcodes.ASM6, dest);
this.interfaceCache = interfaceCache;
this.depsCollector = depsCollector;
+ this.coreLibrarySupport = coreLibrarySupport;
this.bootclasspath = bootclasspath;
+ this.targetLoader = targetLoader;
this.store = store;
this.legacyJaCoCo = legacyJaCoCo;
}
@@ -210,6 +219,10 @@ class InterfaceDesugaring extends ClassVisitor {
internalName,
desc);
++numberOfDefaultMethods;
+ if (coreLibrarySupport != null) {
+ coreLibrarySupport.registerIfEmulatedCoreInterface(
+ access, internalName, name, desc, exceptions);
+ }
abstractDest =
super.visitMethod(access | Opcodes.ACC_ABSTRACT, name, desc, signature, exceptions);
}
@@ -234,7 +247,12 @@ class InterfaceDesugaring extends ClassVisitor {
}
return result != null
? new InterfaceInvocationRewriter(
- result, isInterface() ? internalName : null, bootclasspath, depsCollector, codeOwner)
+ result,
+ isInterface() ? internalName : null,
+ bootclasspath,
+ targetLoader,
+ depsCollector,
+ codeOwner)
: null;
}
@@ -265,8 +283,7 @@ class InterfaceDesugaring extends ClassVisitor {
return "<clinit>".equals(methodName);
}
- private static String normalizeInterfaceMethodName(
- String name, boolean isLambda, boolean isStatic) {
+ static String normalizeInterfaceMethodName(String name, boolean isLambda, boolean isStatic) {
if (isLambda) {
// Rename lambda method to reflect the new owner. Not doing so confuses LambdaDesugaring
// if it's run over this class again. LambdaDesugaring has already renamed the method from
@@ -355,6 +372,7 @@ class InterfaceDesugaring extends ClassVisitor {
@Nullable private final String interfaceName;
private final ClassReaderFactory bootclasspath;
+ private final ClassLoader targetLoader;
private final DependencyCollector depsCollector;
/** Internal name that'll be used to record any dependencies on interface methods. */
private final String declaringClass;
@@ -363,11 +381,13 @@ class InterfaceDesugaring extends ClassVisitor {
MethodVisitor dest,
@Nullable String knownInterfaceName,
ClassReaderFactory bootclasspath,
+ ClassLoader targetLoader,
DependencyCollector depsCollector,
String declaringClass) {
super(Opcodes.ASM6, dest);
this.interfaceName = knownInterfaceName;
this.bootclasspath = bootclasspath;
+ this.targetLoader = targetLoader;
this.depsCollector = depsCollector;
this.declaringClass = declaringClass;
}
@@ -410,7 +430,16 @@ class InterfaceDesugaring extends ClassVisitor {
checkArgument(!owner.endsWith(DependencyCollector.INTERFACE_COMPANION_SUFFIX),
"shouldn't consider %s an interface", owner);
if (opcode == Opcodes.INVOKESPECIAL) {
- // Turn Interface.super.m() into Interface$$CC.m(receiver)
+ // Turn Interface.super.m() into DefiningInterface$$CC.m(receiver). Note that owner
+ // always refers to the current type's immediate super-interface, but the default method
+ // may be inherited by that interface, so we have to figure out where the method is
+ // defined and invoke it in the corresponding companion class (b/73355452). Note that
+ // we're always dealing with interfaces here, and all interface methods are public,
+ // so using Class.getMethods should suffice to find inherited methods. Also note this
+ // can only be a default method invocation, no abstract method invocation.
+ owner =
+ findDefaultMethod(owner, name, desc)
+ .getDeclaringClass().getName().replace('.', '/');
opcode = Opcodes.INVOKESTATIC;
desc = companionDefaultMethodDescriptor(owner, desc);
}
@@ -422,6 +451,23 @@ class InterfaceDesugaring extends ClassVisitor {
}
super.visitMethodInsn(opcode, owner, name, desc, itf);
}
+
+ private Method findDefaultMethod(String owner, String name, String desc) {
+ try {
+ Class<?> clazz = targetLoader.loadClass(owner.replace('/', '.'));
+ // otherwise getting public methods with getMethods() below isn't enough
+ checkArgument(clazz.isInterface(), "Not an interface: %s", owner);
+ for (Method m : clazz.getMethods()) {
+ if (m.getName().equals(name) && Type.getMethodDescriptor(m).equals(desc)) {
+ checkState(m.isDefault(), "Found non-default method: %s", m);
+ return m;
+ }
+ }
+ } catch (ClassNotFoundException e) {
+ throw new IllegalStateException("Couldn't load " + owner, e);
+ }
+ throw new IllegalArgumentException("Method not found: " + owner + "." + name + desc);
+ }
}
/**
diff --git a/java/com/google/devtools/build/android/desugar/Java7Compatibility.java b/java/com/google/devtools/build/android/desugar/Java7Compatibility.java
index 37a45dd..2090d5c 100644
--- a/java/com/google/devtools/build/android/desugar/Java7Compatibility.java
+++ b/java/com/google/devtools/build/android/desugar/Java7Compatibility.java
@@ -17,6 +17,7 @@ import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
+import com.google.devtools.build.android.desugar.io.BitFlags;
import javax.annotation.Nullable;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.Attribute;
diff --git a/java/com/google/devtools/build/android/desugar/LambdaClassFixer.java b/java/com/google/devtools/build/android/desugar/LambdaClassFixer.java
index fb05bcb..6b0a921 100644
--- a/java/com/google/devtools/build/android/desugar/LambdaClassFixer.java
+++ b/java/com/google/devtools/build/android/desugar/LambdaClassFixer.java
@@ -19,6 +19,7 @@ import static com.google.common.base.Preconditions.checkState;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.android.desugar.io.BitFlags;
import java.util.HashSet;
import java.util.LinkedHashSet;
import org.objectweb.asm.AnnotationVisitor;
diff --git a/java/com/google/devtools/build/android/desugar/LambdaClassMaker.java b/java/com/google/devtools/build/android/desugar/LambdaClassMaker.java
index 72f039a..94c6bbc 100644
--- a/java/com/google/devtools/build/android/desugar/LambdaClassMaker.java
+++ b/java/com/google/devtools/build/android/desugar/LambdaClassMaker.java
@@ -81,12 +81,12 @@ class LambdaClassMaker {
if (!Files.exists(rootPathPrefix.getParent())) {
return ImmutableList.of();
}
- try (Stream<Path> paths =
- Files.list(rootPathPrefix.getParent())
- .filter(
- path -> path.toString().startsWith(rootPathPrefixStr)
- && !existingPaths.contains(path))) {
- return paths.collect(ImmutableList.toImmutableList());
+ try (Stream<Path> paths = Files.list(rootPathPrefix.getParent())) {
+ return paths
+ .filter(
+ path ->
+ path.toString().startsWith(rootPathPrefixStr) && !existingPaths.contains(path))
+ .collect(ImmutableList.toImmutableList());
}
}
}
diff --git a/java/com/google/devtools/build/android/desugar/LambdaDesugaring.java b/java/com/google/devtools/build/android/desugar/LambdaDesugaring.java
index 5f41347..f9b5316 100644
--- a/java/com/google/devtools/build/android/desugar/LambdaDesugaring.java
+++ b/java/com/google/devtools/build/android/desugar/LambdaDesugaring.java
@@ -21,6 +21,7 @@ import static org.objectweb.asm.Opcodes.ASM6;
import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.android.desugar.io.BitFlags;
import java.io.IOException;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles.Lookup;
diff --git a/java/com/google/devtools/build/android/desugar/LongCompareMethodRewriter.java b/java/com/google/devtools/build/android/desugar/LongCompareMethodRewriter.java
index f66d862..7f2f355 100644
--- a/java/com/google/devtools/build/android/desugar/LongCompareMethodRewriter.java
+++ b/java/com/google/devtools/build/android/desugar/LongCompareMethodRewriter.java
@@ -17,6 +17,7 @@ import static org.objectweb.asm.Opcodes.ASM6;
import static org.objectweb.asm.Opcodes.INVOKESTATIC;
import static org.objectweb.asm.Opcodes.LCMP;
+import com.google.devtools.build.android.desugar.io.CoreLibraryRewriter;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
@@ -26,8 +27,11 @@ import org.objectweb.asm.MethodVisitor;
*/
public class LongCompareMethodRewriter extends ClassVisitor {
- public LongCompareMethodRewriter(ClassVisitor cv) {
+ private final CoreLibraryRewriter rewriter;
+
+ public LongCompareMethodRewriter(ClassVisitor cv, CoreLibraryRewriter rewriter) {
super(ASM6, cv);
+ this.rewriter = rewriter;
}
@Override
@@ -37,7 +41,7 @@ public class LongCompareMethodRewriter extends ClassVisitor {
return visitor == null ? visitor : new LongCompareMethodVisitor(visitor);
}
- private static class LongCompareMethodVisitor extends MethodVisitor {
+ private class LongCompareMethodVisitor extends MethodVisitor {
public LongCompareMethodVisitor(MethodVisitor visitor) {
super(ASM6, visitor);
@@ -45,14 +49,14 @@ public class LongCompareMethodRewriter extends ClassVisitor {
@Override
public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
- if (opcode != INVOKESTATIC
- || !owner.equals("java/lang/Long")
- || !name.equals("compare")
- || !desc.equals("(JJ)I")) {
+ if (opcode == INVOKESTATIC
+ && rewriter.unprefix(owner).equals("java/lang/Long")
+ && name.equals("compare")
+ && desc.equals("(JJ)I")) {
+ super.visitInsn(LCMP);
+ } else {
super.visitMethodInsn(opcode, owner, name, desc, itf);
- return;
}
- super.visitInsn(LCMP);
}
}
}
diff --git a/java/com/google/devtools/build/android/desugar/ObjectsRequireNonNullMethodRewriter.java b/java/com/google/devtools/build/android/desugar/ObjectsRequireNonNullMethodRewriter.java
index 86465d6..931459a 100644
--- a/java/com/google/devtools/build/android/desugar/ObjectsRequireNonNullMethodRewriter.java
+++ b/java/com/google/devtools/build/android/desugar/ObjectsRequireNonNullMethodRewriter.java
@@ -19,6 +19,7 @@ import static org.objectweb.asm.Opcodes.INVOKESTATIC;
import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL;
import static org.objectweb.asm.Opcodes.POP;
+import com.google.devtools.build.android.desugar.io.CoreLibraryRewriter;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
@@ -29,8 +30,11 @@ import org.objectweb.asm.MethodVisitor;
*/
public class ObjectsRequireNonNullMethodRewriter extends ClassVisitor {
- public ObjectsRequireNonNullMethodRewriter(ClassVisitor cv) {
+ private final CoreLibraryRewriter rewriter;
+
+ public ObjectsRequireNonNullMethodRewriter(ClassVisitor cv, CoreLibraryRewriter rewriter) {
super(ASM6, cv);
+ this.rewriter = rewriter;
}
@Override
@@ -40,7 +44,7 @@ public class ObjectsRequireNonNullMethodRewriter extends ClassVisitor {
return visitor == null ? visitor : new ObjectsMethodInlinerMethodVisitor(visitor);
}
- private static class ObjectsMethodInlinerMethodVisitor extends MethodVisitor {
+ private class ObjectsMethodInlinerMethodVisitor extends MethodVisitor {
public ObjectsMethodInlinerMethodVisitor(MethodVisitor mv) {
super(ASM6, mv);
@@ -48,20 +52,19 @@ public class ObjectsRequireNonNullMethodRewriter extends ClassVisitor {
@Override
public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
- if (opcode != INVOKESTATIC
- || !owner.equals("java/util/Objects")
- || !name.equals("requireNonNull")
- || !desc.equals("(Ljava/lang/Object;)Ljava/lang/Object;")) {
+ if (opcode == INVOKESTATIC
+ && rewriter.unprefix(owner).equals("java/util/Objects")
+ && name.equals("requireNonNull")
+ && desc.equals("(Ljava/lang/Object;)Ljava/lang/Object;")) {
+ // a call to Objects.requireNonNull(Object o)
+ // duplicate the first argument 'o', as this method returns 'o'.
+ super.visitInsn(DUP);
+ super.visitMethodInsn(
+ INVOKEVIRTUAL, "java/lang/Object", "getClass", "()Ljava/lang/Class;", false);
+ super.visitInsn(POP);
+ } else {
super.visitMethodInsn(opcode, owner, name, desc, itf);
- return;
}
-
- // a call to Objects.requireNonNull(Object o)
- // duplicate the first argument 'o', as this method returns 'o'.
- super.visitInsn(DUP);
- super.visitMethodInsn(
- INVOKEVIRTUAL, "java/lang/Object", "getClass", "()Ljava/lang/Class;", false);
- super.visitInsn(POP);
}
}
}
diff --git a/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriter.java b/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriter.java
index 8e6d6d5..98eef45 100644
--- a/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriter.java
+++ b/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriter.java
@@ -29,6 +29,7 @@ import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.devtools.build.android.desugar.BytecodeTypeInference.InferredType;
+import com.google.devtools.build.android.desugar.io.BitFlags;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Optional;
@@ -165,14 +166,12 @@ public class TryWithResourcesRewriter extends ClassVisitor {
new CloseResourceMethodSpecializer(cv, resourceInternalName, isInterface));
}
} else {
+ // It is possible that all calls to $closeResources(...) are in dead code regions, and the
+ // calls are eliminated, which leaving the method $closeResources() unused. (b/78030676).
+ // In this case, we just discard the method body.
checkState(
- closeResourceMethod == null,
- "The field resourceTypeInternalNames is empty. "
- + "But the class has the $closeResource method.");
- checkState(
- !hasCloseResourceMethod,
- "The class %s has close resource method, but resourceTypeInternalNames is empty.",
- internalName);
+ !hasCloseResourceMethod || closeResourceMethod != null,
+ "There should be $closeResources(...) in the class file.");
}
super.visitEnd();
}
@@ -311,18 +310,20 @@ public class TryWithResourcesRewriter extends ClassVisitor {
InferredType resourceType = typeInference.getTypeOfOperandFromTop(0);
Optional<String> resourceClassInternalName = resourceType.getInternalName();
- checkState(
- resourceClassInternalName.isPresent(),
- "The resource class %s is not a reference type in %s.%s",
- resourceType,
- internalName,
- methodSignature);
- checkState(
- isAssignableFrom(
- "java.lang.AutoCloseable", resourceClassInternalName.get().replace('/', '.')),
- "The resource type should be a subclass of java.lang.AutoCloseable: %s",
- resourceClassInternalName);
-
+ {
+ // Check the resource type.
+ checkState(
+ resourceClassInternalName.isPresent(),
+ "The resource class %s is not a reference type in %s.%s",
+ resourceType,
+ internalName,
+ methodSignature);
+ String resourceClassName = resourceClassInternalName.get().replace('/', '.');
+ checkState(
+ hasCloseMethod(resourceClassName),
+ "The resource class %s should have a close() method.",
+ resourceClassName);
+ }
resourceTypeInternalNames.add(resourceClassInternalName.get());
super.visitMethodInsn(
opcode,
@@ -356,6 +357,26 @@ public class TryWithResourcesRewriter extends ClassVisitor {
return isAssignableFrom("java.lang.Throwable", owner.replace('/', '.'));
}
+ private boolean hasCloseMethod(String resourceClassName) {
+ try {
+ Class<?> klass = classLoader.loadClass(resourceClassName);
+ klass.getMethod("close");
+ return true;
+ } catch (ClassNotFoundException e) {
+ throw new AssertionError(
+ "Failed to load class "
+ + resourceClassName
+ + " when desugaring method "
+ + internalName
+ + "."
+ + methodSignature,
+ e);
+ } catch (NoSuchMethodException e) {
+ // There is no close() method in the class, so return false.
+ return false;
+ }
+ }
+
private boolean isAssignableFrom(String baseClassName, String subClassName) {
try {
Class<?> baseClass = classLoader.loadClass(baseClassName);
diff --git a/java/com/google/devtools/build/android/desugar/BitFlags.java b/java/com/google/devtools/build/android/desugar/io/BitFlags.java
index 8be2288..af6f481 100644
--- a/java/com/google/devtools/build/android/desugar/BitFlags.java
+++ b/java/com/google/devtools/build/android/desugar/io/BitFlags.java
@@ -11,12 +11,12 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.devtools.build.android.desugar;
+package com.google.devtools.build.android.desugar.io;
import org.objectweb.asm.Opcodes;
/** Convenience method for working with {@code int} bitwise flags. */
-class BitFlags {
+public class BitFlags {
/**
* Returns {@code true} iff <b>all</b> bits in {@code bitmask} are set in {@code flags}. Trivially
diff --git a/java/com/google/devtools/build/android/desugar/CoreLibraryRewriter.java b/java/com/google/devtools/build/android/desugar/io/CoreLibraryRewriter.java
index 7f1591b..f3c546c 100644
--- a/java/com/google/devtools/build/android/desugar/CoreLibraryRewriter.java
+++ b/java/com/google/devtools/build/android/desugar/io/CoreLibraryRewriter.java
@@ -11,10 +11,11 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.devtools.build.android.desugar;
+package com.google.devtools.build.android.desugar.io;
import java.io.IOException;
import java.io.InputStream;
+import javax.annotation.Nullable;
import org.objectweb.asm.Attribute;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
@@ -24,7 +25,7 @@ import org.objectweb.asm.commons.ClassRemapper;
import org.objectweb.asm.commons.Remapper;
/** Utility class to prefix or unprefix class names of core library classes */
-class CoreLibraryRewriter {
+public class CoreLibraryRewriter {
private final String prefix;
public CoreLibraryRewriter(String prefix) {
@@ -85,6 +86,10 @@ class CoreLibraryRewriter {
return false;
}
+ public String getPrefix() {
+ return prefix;
+ }
+
/** Removes prefix from class names */
public String unprefix(String typeName) {
if (prefix.isEmpty() || !typeName.startsWith(prefix)) {
@@ -152,6 +157,8 @@ class CoreLibraryRewriter {
public class UnprefixingClassWriter extends ClassVisitor {
private final ClassWriter writer;
+ private String finalClassName;
+
UnprefixingClassWriter(int flags) {
super(Opcodes.ASM6);
this.writer = new ClassWriter(flags);
@@ -159,7 +166,7 @@ class CoreLibraryRewriter {
if (!prefix.isEmpty()) {
this.cv =
new ClassRemapper(
- this.cv,
+ this.writer,
new Remapper() {
@Override
public String map(String typeName) {
@@ -169,8 +176,26 @@ class CoreLibraryRewriter {
}
}
- byte[] toByteArray() {
+ /** Returns the (unprefixed) name of the class once written. */
+ @Nullable
+ public String getClassName() {
+ return finalClassName;
+ }
+
+ public byte[] toByteArray() {
return writer.toByteArray();
}
+
+ @Override
+ public void visit(
+ int version,
+ int access,
+ String name,
+ String signature,
+ String superName,
+ String[] interfaces) {
+ finalClassName = unprefix(name);
+ super.visit(version, access, name, signature, superName, interfaces);
+ }
}
}
diff --git a/java/com/google/devtools/build/android/desugar/DirectoryInputFileProvider.java b/java/com/google/devtools/build/android/desugar/io/DirectoryInputFileProvider.java
index 1c5abc9..c607b42 100644
--- a/java/com/google/devtools/build/android/desugar/DirectoryInputFileProvider.java
+++ b/java/com/google/devtools/build/android/desugar/io/DirectoryInputFileProvider.java
@@ -11,7 +11,7 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.devtools.build.android.desugar;
+package com.google.devtools.build.android.desugar.io;
import java.io.File;
import java.io.FileInputStream;
diff --git a/java/com/google/devtools/build/android/desugar/DirectoryOutputFileProvider.java b/java/com/google/devtools/build/android/desugar/io/DirectoryOutputFileProvider.java
index 782a81e..f8e87cb 100644
--- a/java/com/google/devtools/build/android/desugar/DirectoryOutputFileProvider.java
+++ b/java/com/google/devtools/build/android/desugar/io/DirectoryOutputFileProvider.java
@@ -11,7 +11,7 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.devtools.build.android.desugar;
+package com.google.devtools.build.android.desugar.io;
import com.google.common.io.ByteStreams;
import java.io.IOException;
@@ -21,7 +21,7 @@ import java.nio.file.Files;
import java.nio.file.Path;
/** Output provider is a directory. */
-public class DirectoryOutputFileProvider implements OutputFileProvider {
+class DirectoryOutputFileProvider implements OutputFileProvider {
private final Path root;
diff --git a/java/com/google/devtools/build/android/desugar/FieldInfo.java b/java/com/google/devtools/build/android/desugar/io/FieldInfo.java
index c281039..0b4f634 100644
--- a/java/com/google/devtools/build/android/desugar/FieldInfo.java
+++ b/java/com/google/devtools/build/android/desugar/io/FieldInfo.java
@@ -11,7 +11,7 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.devtools.build.android.desugar;
+package com.google.devtools.build.android.desugar.io;
import com.google.auto.value.AutoValue;
@@ -19,7 +19,7 @@ import com.google.auto.value.AutoValue;
@AutoValue
public abstract class FieldInfo {
- static FieldInfo create(String owner, String name, String desc) {
+ public static FieldInfo create(String owner, String name, String desc) {
return new AutoValue_FieldInfo(owner, name, desc);
}
diff --git a/java/com/google/devtools/build/android/desugar/HeaderClassLoader.java b/java/com/google/devtools/build/android/desugar/io/HeaderClassLoader.java
index 0a757bf..f70dc0e 100644
--- a/java/com/google/devtools/build/android/desugar/HeaderClassLoader.java
+++ b/java/com/google/devtools/build/android/desugar/io/HeaderClassLoader.java
@@ -11,7 +11,7 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.devtools.build.android.desugar;
+package com.google.devtools.build.android.desugar.io;
import com.google.common.collect.ImmutableList;
import java.io.IOError;
@@ -33,7 +33,7 @@ import org.objectweb.asm.Opcodes;
*
* @see java.net.URLClassLoader
*/
-class HeaderClassLoader extends ClassLoader {
+public class HeaderClassLoader extends ClassLoader {
private final IndexedInputs indexedInputs;
private final CoreLibraryRewriter rewriter;
@@ -58,7 +58,8 @@ class HeaderClassLoader extends ClassLoader {
// Have ASM compute maxs so we don't need to figure out how many formal parameters there are
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
ImmutableList<FieldInfo> interfaceFieldNames = getFieldsIfReaderIsInterface(reader);
- reader.accept(new CodeStubber(writer, interfaceFieldNames), 0);
+ // TODO(kmb): Consider SKIP_CODE and stubbing everything so class loader doesn't verify code
+ reader.accept(new CodeStubber(writer, interfaceFieldNames), ClassReader.SKIP_DEBUG);
bytecode = writer.toByteArray();
} catch (IOException e) {
throw new IOError(e);
diff --git a/java/com/google/devtools/build/android/desugar/IndexedInputs.java b/java/com/google/devtools/build/android/desugar/io/IndexedInputs.java
index 33c6132..8ce4b62 100644
--- a/java/com/google/devtools/build/android/desugar/IndexedInputs.java
+++ b/java/com/google/devtools/build/android/desugar/io/IndexedInputs.java
@@ -11,7 +11,7 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.devtools.build.android.desugar;
+package com.google.devtools.build.android.desugar.io;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
@@ -28,7 +28,7 @@ import javax.annotation.Nullable;
* scanning all inputs over and over for each class to load. An indexed inputs can have a parent
* that is firstly used when a file name is searched.
*/
-class IndexedInputs {
+public class IndexedInputs {
private final ImmutableMap<String, InputFileProvider> inputFiles;
diff --git a/java/com/google/devtools/build/android/desugar/InputFileProvider.java b/java/com/google/devtools/build/android/desugar/io/InputFileProvider.java
index c2b6353..c41d018 100644
--- a/java/com/google/devtools/build/android/desugar/InputFileProvider.java
+++ b/java/com/google/devtools/build/android/desugar/io/InputFileProvider.java
@@ -11,15 +11,18 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.devtools.build.android.desugar;
+package com.google.devtools.build.android.desugar.io;
+import com.google.errorprone.annotations.MustBeClosed;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
import java.util.zip.ZipEntry;
/** Input file provider allows to iterate on relative path filename of a directory or a jar file. */
-interface InputFileProvider extends Closeable, Iterable<String> {
+public interface InputFileProvider extends Closeable, Iterable<String> {
/**
* Return a ZipEntry for {@code filename}. If the provider is a {@link ZipInputFileProvider}, the
@@ -33,4 +36,14 @@ interface InputFileProvider extends Closeable, Iterable<String> {
* responsibility of the caller to close this stream.
*/
InputStream getInputStream(String filename) throws IOException;
+
+ /** Transform a Path to an InputFileProvider that needs to be closed by the caller. */
+ @MustBeClosed
+ public static InputFileProvider open(Path path) throws IOException {
+ if (Files.isDirectory(path)) {
+ return new DirectoryInputFileProvider(path);
+ } else {
+ return new ZipInputFileProvider(path);
+ }
+ }
}
diff --git a/java/com/google/devtools/build/android/desugar/OutputFileProvider.java b/java/com/google/devtools/build/android/desugar/io/OutputFileProvider.java
index 7a590ef..e693786 100644
--- a/java/com/google/devtools/build/android/desugar/OutputFileProvider.java
+++ b/java/com/google/devtools/build/android/desugar/io/OutputFileProvider.java
@@ -11,12 +11,15 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.devtools.build.android.desugar;
+package com.google.devtools.build.android.desugar.io;
+import com.google.errorprone.annotations.MustBeClosed;
import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
/** Output file provider allows to write files in directory or jar files. */
-interface OutputFileProvider extends AutoCloseable {
+public interface OutputFileProvider extends AutoCloseable {
/** Filename to use to write out dependency metadata for later consistency checking. */
public static final String DESUGAR_DEPS_FILENAME = "META-INF/desugar_deps";
@@ -29,4 +32,14 @@ interface OutputFileProvider extends AutoCloseable {
/** Write {@code content} in {@code filename} to this output */
void write(String filename, byte[] content) throws IOException;
+
+ /** Transform a Path to an {@link OutputFileProvider} */
+ @MustBeClosed
+ public static OutputFileProvider create(Path path) throws IOException {
+ if (Files.isDirectory(path)) {
+ return new DirectoryOutputFileProvider(path);
+ } else {
+ return new ZipOutputFileProvider(path);
+ }
+ }
}
diff --git a/java/com/google/devtools/build/android/desugar/io/ThrowingClassLoader.java b/java/com/google/devtools/build/android/desugar/io/ThrowingClassLoader.java
new file mode 100644
index 0000000..16f83f2
--- /dev/null
+++ b/java/com/google/devtools/build/android/desugar/io/ThrowingClassLoader.java
@@ -0,0 +1,27 @@
+// Copyright 2016 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.desugar.io;
+
+/** Class loader that throws whenever it can, for use the parent of a class loader hierarchy. */
+public class ThrowingClassLoader extends ClassLoader {
+ @Override
+ protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
+ if (name.startsWith("java.")) {
+ // Use system class loader for java. classes, since ClassLoader.defineClass gets
+ // grumpy when those don't come from the standard place.
+ return super.loadClass(name, resolve);
+ }
+ throw new ClassNotFoundException();
+ }
+} \ No newline at end of file
diff --git a/java/com/google/devtools/build/android/desugar/ZipInputFileProvider.java b/java/com/google/devtools/build/android/desugar/io/ZipInputFileProvider.java
index 307c8b8..9bd7758 100644
--- a/java/com/google/devtools/build/android/desugar/ZipInputFileProvider.java
+++ b/java/com/google/devtools/build/android/desugar/io/ZipInputFileProvider.java
@@ -11,7 +11,7 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.devtools.build.android.desugar;
+package com.google.devtools.build.android.desugar.io;
import com.google.common.base.Functions;
import com.google.common.collect.Iterators;
diff --git a/java/com/google/devtools/build/android/desugar/ZipOutputFileProvider.java b/java/com/google/devtools/build/android/desugar/io/ZipOutputFileProvider.java
index 8d6501d..36cb26d 100644
--- a/java/com/google/devtools/build/android/desugar/ZipOutputFileProvider.java
+++ b/java/com/google/devtools/build/android/desugar/io/ZipOutputFileProvider.java
@@ -11,7 +11,7 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.devtools.build.android.desugar;
+package com.google.devtools.build.android.desugar.io;
import static com.google.common.base.Preconditions.checkArgument;
@@ -26,7 +26,7 @@ import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
/** Output provider is a zip file. */
-public class ZipOutputFileProvider implements OutputFileProvider {
+class ZipOutputFileProvider implements OutputFileProvider {
private final ZipOutputStream out;
diff --git a/java/com/google/devtools/build/android/desugar/scan/KeepReference.java b/java/com/google/devtools/build/android/desugar/scan/KeepReference.java
new file mode 100644
index 0000000..bae3f38
--- /dev/null
+++ b/java/com/google/devtools/build/android/desugar/scan/KeepReference.java
@@ -0,0 +1,51 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.desugar.scan;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.auto.value.AutoValue;
+import com.google.errorprone.annotations.Immutable;
+
+@AutoValue
+@Immutable
+abstract class KeepReference {
+ public static KeepReference classReference(String internalName) {
+ checkArgument(!internalName.isEmpty());
+ return new AutoValue_KeepReference(internalName, "", "");
+ }
+
+ public static KeepReference memberReference(String internalName, String name, String desc) {
+ checkArgument(!internalName.isEmpty());
+ checkArgument(!name.isEmpty());
+ checkArgument(!desc.isEmpty());
+ return new AutoValue_KeepReference(internalName, name, desc);
+ }
+
+ public final boolean isMemberReference() {
+ return !name().isEmpty();
+ }
+
+ public final boolean isMethodReference() {
+ return desc().startsWith("(");
+ }
+
+ public final boolean isFieldReference() {
+ return isMemberReference() && !isMethodReference();
+ }
+
+ public abstract String internalName();
+ public abstract String name();
+ public abstract String desc();
+}
diff --git a/java/com/google/devtools/build/android/desugar/scan/KeepScanner.java b/java/com/google/devtools/build/android/desugar/scan/KeepScanner.java
new file mode 100644
index 0000000..4924f7c
--- /dev/null
+++ b/java/com/google/devtools/build/android/desugar/scan/KeepScanner.java
@@ -0,0 +1,303 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.desugar.scan;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+import static java.nio.file.StandardOpenOption.CREATE;
+import static java.util.Comparator.comparing;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.io.ByteStreams;
+import com.google.common.io.Closer;
+import com.google.devtools.build.android.Converters.ExistingPathConverter;
+import com.google.devtools.build.android.Converters.PathConverter;
+import com.google.devtools.build.android.desugar.io.CoreLibraryRewriter;
+import com.google.devtools.build.android.desugar.io.HeaderClassLoader;
+import com.google.devtools.build.android.desugar.io.IndexedInputs;
+import com.google.devtools.build.android.desugar.io.InputFileProvider;
+import com.google.devtools.build.android.desugar.io.ThrowingClassLoader;
+import com.google.devtools.common.options.Option;
+import com.google.devtools.common.options.OptionDocumentationCategory;
+import com.google.devtools.common.options.OptionEffectTag;
+import com.google.devtools.common.options.OptionsBase;
+import com.google.devtools.common.options.OptionsParser;
+import com.google.devtools.common.options.ShellQuotedParamsFilePreProcessor;
+import java.io.IOError;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintStream;
+import java.lang.reflect.Method;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.Type;
+
+class KeepScanner {
+
+ public static class KeepScannerOptions extends OptionsBase {
+ @Option(
+ name = "input",
+ defaultValue = "null",
+ documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+ effectTags = OptionEffectTag.UNKNOWN,
+ converter = ExistingPathConverter.class,
+ abbrev = 'i',
+ help = "Input Jar with classes to scan."
+ )
+ public Path inputJars;
+
+ @Option(
+ name = "classpath_entry",
+ allowMultiple = true,
+ defaultValue = "",
+ documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+ effectTags = {OptionEffectTag.UNKNOWN},
+ converter = ExistingPathConverter.class,
+ help =
+ "Ordered classpath (Jar or directory) to resolve symbols in the --input Jar, like "
+ + "javac's -cp flag."
+ )
+ public List<Path> classpath;
+
+ @Option(
+ name = "bootclasspath_entry",
+ allowMultiple = true,
+ defaultValue = "",
+ documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+ effectTags = {OptionEffectTag.UNKNOWN},
+ converter = ExistingPathConverter.class,
+ help =
+ "Bootclasspath that was used to compile the --input Jar with, like javac's "
+ + "-bootclasspath flag (required)."
+ )
+ public List<Path> bootclasspath;
+
+ @Option(
+ name = "keep_file",
+ defaultValue = "null",
+ documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+ effectTags = OptionEffectTag.UNKNOWN,
+ converter = PathConverter.class,
+ help = "Where to write keep rules to."
+ )
+ public Path keepDest;
+
+ @Option(
+ name = "prefix",
+ defaultValue = "j$/",
+ documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+ effectTags = OptionEffectTag.UNKNOWN,
+ help = "type to scan for."
+ )
+ public String prefix;
+ }
+
+ public static void main(String... args) throws Exception {
+ OptionsParser parser = OptionsParser.newOptionsParser(KeepScannerOptions.class);
+ parser.setAllowResidue(false);
+ parser.enableParamsFileSupport(new ShellQuotedParamsFilePreProcessor(FileSystems.getDefault()));
+ parser.parseAndExitUponError(args);
+ KeepScannerOptions options = parser.getOptions(KeepScannerOptions.class);
+
+ Map<String, ImmutableSet<KeepReference>> seeds;
+ try (Closer closer = Closer.create()) {
+ // TODO(kmb): Try to share more of this code with Desugar binary
+ IndexedInputs classpath =
+ new IndexedInputs(toRegisteredInputFileProvider(closer, options.classpath));
+ IndexedInputs bootclasspath =
+ new IndexedInputs(toRegisteredInputFileProvider(closer, options.bootclasspath));
+
+ // Construct classloader from classpath. Since we're assuming the prefix we're looking for
+ // isn't part of the input itself we shouldn't need to include the input in the classloader.
+ CoreLibraryRewriter noopRewriter = new CoreLibraryRewriter("");
+ ClassLoader classloader =
+ new HeaderClassLoader(classpath, noopRewriter,
+ new HeaderClassLoader(bootclasspath, noopRewriter,
+ new ThrowingClassLoader()));
+ seeds = scan(checkNotNull(options.inputJars), options.prefix, classloader);
+ }
+
+ try (PrintStream out =
+ new PrintStream(
+ Files.newOutputStream(options.keepDest, CREATE), /*autoFlush=*/ false, "UTF-8")) {
+ writeKeepDirectives(out, seeds);
+ }
+ }
+
+ /**
+ * Writes a -keep rule for each class listing any members to keep. We sort classes and members
+ * so the output is deterministic.
+ */
+ private static void writeKeepDirectives(
+ PrintStream out, Map<String, ImmutableSet<KeepReference>> seeds) {
+ seeds
+ .entrySet()
+ .stream()
+ .sorted(comparing(Map.Entry::getKey))
+ .forEachOrdered(
+ type -> {
+ out.printf("-keep class %s {%n", type.getKey().replace('/', '.'));
+ type.getValue()
+ .stream()
+ .filter(KeepReference::isMemberReference)
+ .sorted(comparing(KeepReference::name).thenComparing(KeepReference::desc))
+ .map(ref -> toKeepDescriptor(ref))
+ .distinct() // drop duplicates due to method descriptors with different returns
+ .forEachOrdered(line -> out.append(" ").append(line).append(";").println());
+ out.printf("}%n");
+ });
+ }
+
+ /** Scans for and returns references with owners matching the given prefix grouped by owner. */
+ private static Map<String, ImmutableSet<KeepReference>> scan(
+ Path jarFile, String prefix, ClassLoader classpath) throws IOException {
+ // We read the Jar sequentially since ZipFile uses locks anyway but then allow scanning each
+ // class in parallel.
+ try (ZipFile zip = new ZipFile(jarFile.toFile())) {
+ return zip.stream()
+ .filter(entry -> entry.getName().endsWith(".class"))
+ .map(entry -> readFully(zip, entry))
+ .parallel()
+ .flatMap(
+ content -> PrefixReferenceScanner.scan(new ClassReader(content), prefix).stream())
+ .distinct() // so we don't process the same reference multiple times next
+ .map(ref -> nearestDeclaration(ref, classpath))
+ .collect(
+ Collectors.groupingByConcurrent(
+ KeepReference::internalName, ImmutableSet.toImmutableSet()));
+ }
+ }
+
+ private static byte[] readFully(ZipFile zip, ZipEntry entry) {
+ byte[] result = new byte[(int) entry.getSize()];
+ try (InputStream content = zip.getInputStream(entry)) {
+ ByteStreams.readFully(content, result);
+ return result;
+ } catch (IOException e) {
+ throw new IOError(e);
+ }
+ }
+
+ /**
+ * Find the nearest definition of the given reference in the class hierarchy and return the
+ * modified reference. This is needed b/c bytecode sometimes refers to a method or field using
+ * an owner type that inherits the method or field instead of defining the member itself.
+ * In that case we need to find and keep the inherited definition.
+ */
+ private static KeepReference nearestDeclaration(KeepReference ref, ClassLoader classpath) {
+ if (!ref.isMemberReference() || "<init>".equals(ref.name())) {
+ return ref; // class and constructor references don't need any further work
+ }
+
+ Class<?> clazz;
+ try {
+ clazz = classpath.loadClass(ref.internalName().replace('/', '.'));
+ } catch (ClassNotFoundException e) {
+ throw (NoClassDefFoundError) new NoClassDefFoundError("Couldn't load " + ref).initCause(e);
+ }
+
+ Class<?> owner = findDeclaringClass(clazz, ref);
+ if (owner == clazz) {
+ return ref;
+ }
+ String parent = checkNotNull(owner, "Can't resolve: %s", ref).getName().replace('.', '/');
+ return KeepReference.memberReference(parent, ref.name(), ref.desc());
+ }
+
+ private static Class<?> findDeclaringClass(Class<?> clazz, KeepReference ref) {
+ if (ref.isFieldReference()) {
+ try {
+ return clazz.getField(ref.name()).getDeclaringClass();
+ } catch (NoSuchFieldException e) {
+ // field must be non-public, so search class hierarchy
+ do {
+ try {
+ return clazz.getDeclaredField(ref.name()).getDeclaringClass();
+ } catch (NoSuchFieldException ignored) {
+ // fall through for clarity
+ }
+ clazz = clazz.getSuperclass();
+ } while (clazz != null);
+ }
+ } else {
+ checkState(ref.isMethodReference());
+ Type descriptor = Type.getMethodType(ref.desc());
+ for (Method m : clazz.getMethods()) {
+ if (m.getName().equals(ref.name()) && Type.getType(m).equals(descriptor)) {
+ return m.getDeclaringClass();
+ }
+ }
+ do {
+ // Method must be non-public, so search class hierarchy
+ for (Method m : clazz.getDeclaredMethods()) {
+ if (m.getName().equals(ref.name()) && Type.getType(m).equals(descriptor)) {
+ return m.getDeclaringClass();
+ }
+ }
+ clazz = clazz.getSuperclass();
+ } while (clazz != null);
+ }
+ return null;
+ }
+
+ private static CharSequence toKeepDescriptor(KeepReference member) {
+ StringBuilder result = new StringBuilder();
+ if (member.isMethodReference()) {
+ if (!"<init>".equals(member.name())) {
+ result.append("*** ");
+ }
+ result.append(member.name()).append("(");
+ // Ignore return type as it's unique in the source language
+ boolean first = true;
+ for (Type param : Type.getMethodType(member.desc()).getArgumentTypes()) {
+ if (first) {
+ first = false;
+ } else {
+ result.append(", ");
+ }
+ result.append(param.getClassName());
+ }
+ result.append(")");
+ } else {
+ checkArgument(member.isFieldReference());
+ result.append("*** ").append(member.name()); // field names are unique so ignore descriptor
+ }
+ return result;
+ }
+
+ /**
+ * Transform a list of Path to a list of InputFileProvider and register them with the given
+ * closer.
+ */
+ @SuppressWarnings("MustBeClosedChecker")
+ private static ImmutableList<InputFileProvider> toRegisteredInputFileProvider(
+ Closer closer, List<Path> paths) throws IOException {
+ ImmutableList.Builder<InputFileProvider> builder = new ImmutableList.Builder<>();
+ for (Path path : paths) {
+ builder.add(closer.register(InputFileProvider.open(path)));
+ }
+ return builder.build();
+ }
+
+ private KeepScanner() {}
+}
diff --git a/java/com/google/devtools/build/android/desugar/scan/PrefixReferenceScanner.java b/java/com/google/devtools/build/android/desugar/scan/PrefixReferenceScanner.java
new file mode 100644
index 0000000..b899ccc
--- /dev/null
+++ b/java/com/google/devtools/build/android/desugar/scan/PrefixReferenceScanner.java
@@ -0,0 +1,405 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.desugar.scan;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.common.collect.ImmutableSet;
+import javax.annotation.Nullable;
+import org.objectweb.asm.AnnotationVisitor;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.FieldVisitor;
+import org.objectweb.asm.Handle;
+import org.objectweb.asm.Label;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.Type;
+import org.objectweb.asm.TypePath;
+
+/** {@link ClassVisitor} that records references to classes starting with a given prefix. */
+class PrefixReferenceScanner extends ClassVisitor {
+
+ /**
+ * Returns references with the given prefix in the given class.
+ *
+ * @param prefix an internal name prefix, typically a package such as {@code com/google/}
+ */
+ public static ImmutableSet<KeepReference> scan(ClassReader reader, String prefix) {
+ PrefixReferenceScanner scanner = new PrefixReferenceScanner(prefix);
+ // Frames irrelevant for Android so skip them. Don't skip debug info in case the class we're
+ // visiting has local variable tables (typically it doesn't anyways).
+ reader.accept(scanner, ClassReader.SKIP_FRAMES);
+ return scanner.roots.build();
+ }
+
+ private final ImmutableSet.Builder<KeepReference> roots = ImmutableSet.builder();
+ private final PrefixReferenceMethodVisitor mv = new PrefixReferenceMethodVisitor();
+ private final PrefixReferenceFieldVisitor fv = new PrefixReferenceFieldVisitor();
+ private final PrefixReferenceAnnotationVisitor av = new PrefixReferenceAnnotationVisitor();
+
+ private final String prefix;
+
+ public PrefixReferenceScanner(String prefix) {
+ super(Opcodes.ASM6);
+ this.prefix = prefix;
+ }
+
+ @Override
+ public void visit(
+ int version,
+ int access,
+ String name,
+ String signature,
+ String superName,
+ String[] interfaces) {
+ checkArgument(!name.startsWith(prefix));
+ if (superName != null) {
+ classReference(superName);
+ }
+ classReferences(interfaces);
+ super.visit(version, access, name, signature, superName, interfaces);
+ }
+
+ @Override
+ public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
+ typeReference(desc);
+ return av;
+ }
+
+ @Override
+ public void visitOuterClass(String owner, String name, String desc) {
+ classReference(owner);
+ if (desc != null) {
+ typeReference(Type.getMethodType(desc));
+ }
+ }
+
+ @Override
+ public AnnotationVisitor visitTypeAnnotation(
+ int typeRef, TypePath typePath, String desc, boolean visible) {
+ typeReference(desc);
+ return av;
+ }
+
+ @Override
+ public void visitInnerClass(String name, String outerName, String innerName, int access) {
+ classReference(name);
+ if (outerName != null) {
+ classReference(outerName);
+ }
+ }
+
+ @Override
+ public FieldVisitor visitField(
+ int access, String name, String desc, String signature, Object value) {
+ typeReference(desc);
+ return fv;
+ }
+
+ @Override
+ public MethodVisitor visitMethod(
+ int access, String name, String desc, String signature, String[] exceptions) {
+ typeReference(Type.getMethodType(desc));
+ classReferences(exceptions);
+ return mv;
+ }
+
+ private void classReferences(@Nullable String[] internalNames) {
+ if (internalNames != null) {
+ for (String itf : internalNames) {
+ classReference(itf);
+ }
+ }
+ }
+
+ // The following methods are package-private so they don't incur bridge methods when called from
+ // inner classes below.
+
+ void classReference(String internalName) {
+ checkArgument(internalName.charAt(0) != '[' && internalName.charAt(0) != '(', internalName);
+ checkArgument(!internalName.endsWith(";"), internalName);
+ if (internalName.startsWith(prefix)) {
+ roots.add(KeepReference.classReference(internalName));
+ }
+ }
+
+ void objectReference(String internalName) {
+ // don't call this for method types, convert to Type instead
+ checkArgument(internalName.charAt(0) != '(', internalName);
+ if (internalName.charAt(0) == '[') {
+ typeReference(internalName);
+ } else {
+ classReference(internalName);
+ }
+ }
+
+ void typeReference(String typeDesc) {
+ // don't call this for method types, convert to Type instead
+ checkArgument(typeDesc.charAt(0) != '(', typeDesc);
+
+ int lpos = typeDesc.lastIndexOf('[') + 1;
+ if (typeDesc.charAt(lpos) == 'L') {
+ checkArgument(typeDesc.endsWith(";"), typeDesc);
+ classReference(typeDesc.substring(lpos, typeDesc.length() - 1));
+ } else {
+ // else primitive or primitive array
+ checkArgument(typeDesc.length() == lpos + 1, typeDesc);
+ switch (typeDesc.charAt(lpos)) {
+ case 'B':
+ case 'C':
+ case 'S':
+ case 'I':
+ case 'J':
+ case 'D':
+ case 'F':
+ case 'Z':
+ break;
+ default:
+ throw new AssertionError("Unexpected type descriptor: " + typeDesc);
+ }
+ }
+ }
+
+ void typeReference(Type type) {
+ switch (type.getSort()) {
+ case Type.ARRAY:
+ typeReference(type.getElementType());
+ break;
+ case Type.OBJECT:
+ classReference(type.getInternalName());
+ break;
+
+ case Type.METHOD:
+ for (Type param : type.getArgumentTypes()) {
+ typeReference(param);
+ }
+ typeReference(type.getReturnType());
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ void fieldReference(String owner, String name, String desc) {
+ objectReference(owner);
+ typeReference(desc);
+ if (owner.startsWith(prefix)) {
+ roots.add(KeepReference.memberReference(owner, name, desc));
+ }
+ }
+
+ void methodReference(String owner, String name, String desc) {
+ checkArgument(desc.charAt(0) == '(', desc);
+ objectReference(owner);
+ typeReference(Type.getMethodType(desc));
+ if (owner.startsWith(prefix)) {
+ roots.add(KeepReference.memberReference(owner, name, desc));
+ }
+ }
+
+ void handleReference(Handle handle) {
+ switch (handle.getTag()) {
+ case Opcodes.H_GETFIELD:
+ case Opcodes.H_GETSTATIC:
+ case Opcodes.H_PUTFIELD:
+ case Opcodes.H_PUTSTATIC:
+ fieldReference(handle.getOwner(), handle.getName(), handle.getDesc());
+ break;
+
+ default:
+ methodReference(handle.getOwner(), handle.getName(), handle.getDesc());
+ break;
+ }
+ }
+
+ private class PrefixReferenceMethodVisitor extends MethodVisitor {
+
+ public PrefixReferenceMethodVisitor() {
+ super(Opcodes.ASM6);
+ }
+
+ @Override
+ public AnnotationVisitor visitAnnotationDefault() {
+ return av;
+ }
+
+ @Override
+ public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
+ typeReference(desc);
+ return av;
+ }
+
+ @Override
+ public AnnotationVisitor visitTypeAnnotation(
+ int typeRef, TypePath typePath, String desc, boolean visible) {
+ typeReference(desc);
+ return av;
+ }
+
+ @Override
+ public AnnotationVisitor visitParameterAnnotation(int parameter, String desc, boolean visible) {
+ typeReference(desc);
+ return av;
+ }
+
+ @Override
+ public void visitTypeInsn(int opcode, String type) {
+ objectReference(type);
+ }
+
+ @Override
+ public void visitFieldInsn(int opcode, String owner, String name, String desc) {
+ fieldReference(owner, name, desc);
+ }
+
+ @Override
+ @SuppressWarnings("deprecation") // Implementing deprecated method to be sure
+ public void visitMethodInsn(int opcode, String owner, String name, String desc) {
+ visitMethodInsn(opcode, owner, name, desc, opcode == Opcodes.INVOKEINTERFACE);
+ }
+
+ @Override
+ public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
+ methodReference(owner, name, desc);
+ }
+
+ @Override
+ public void visitInvokeDynamicInsn(String name, String desc, Handle bsm, Object... bsmArgs) {
+ typeReference(Type.getMethodType(desc));
+ handleReference(bsm);
+ for (Object bsmArg : bsmArgs) {
+ visitConstant(bsmArg);
+ }
+ }
+
+ @Override
+ public void visitLdcInsn(Object cst) {
+ visitConstant(cst);
+ }
+
+ private void visitConstant(Object cst) {
+ if (cst instanceof Type) {
+ typeReference((Type) cst);
+ } else if (cst instanceof Handle) {
+ handleReference((Handle) cst);
+ } else {
+ // Check for other expected types as javadoc recommends
+ checkArgument(
+ cst instanceof String
+ || cst instanceof Integer
+ || cst instanceof Long
+ || cst instanceof Float
+ || cst instanceof Double,
+ "Unexpected constant: ", cst);
+ }
+ }
+
+ @Override
+ public void visitMultiANewArrayInsn(String desc, int dims) {
+ typeReference(desc);
+ }
+
+ @Override
+ public AnnotationVisitor visitInsnAnnotation(
+ int typeRef, TypePath typePath, String desc, boolean visible) {
+ typeReference(desc);
+ return av;
+ }
+
+ @Override
+ public void visitTryCatchBlock(Label start, Label end, Label handler, String type) {
+ if (type != null) {
+ classReference(type);
+ }
+ }
+
+ @Override
+ public AnnotationVisitor visitTryCatchAnnotation(
+ int typeRef, TypePath typePath, String desc, boolean visible) {
+ typeReference(desc);
+ return av;
+ }
+
+ @Override
+ public void visitLocalVariable(
+ String name, String desc, String signature, Label start, Label end, int index) {
+ typeReference(desc);
+ }
+
+ @Override
+ public AnnotationVisitor visitLocalVariableAnnotation(
+ int typeRef,
+ TypePath typePath,
+ Label[] start,
+ Label[] end,
+ int[] index,
+ String desc,
+ boolean visible) {
+ typeReference(desc);
+ return av;
+ }
+ }
+
+ private class PrefixReferenceFieldVisitor extends FieldVisitor {
+
+ public PrefixReferenceFieldVisitor() {
+ super(Opcodes.ASM6);
+ }
+
+ @Override
+ public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
+ typeReference(desc);
+ return av;
+ }
+
+ @Override
+ public AnnotationVisitor visitTypeAnnotation(
+ int typeRef, TypePath typePath, String desc, boolean visible) {
+ typeReference(desc);
+ return av;
+ }
+ }
+
+ private class PrefixReferenceAnnotationVisitor extends AnnotationVisitor {
+
+ public PrefixReferenceAnnotationVisitor() {
+ super(Opcodes.ASM6);
+ }
+
+ @Override
+ public void visit(String name, Object value) {
+ if (value instanceof Type) {
+ typeReference((Type) value);
+ }
+ }
+
+ @Override
+ public void visitEnum(String name, String desc, String value) {
+ fieldReference(desc.substring(1, desc.length() - 1), value, desc);
+ }
+
+ @Override
+ public AnnotationVisitor visitAnnotation(String name, String desc) {
+ typeReference(desc);
+ return av;
+ }
+
+ @Override
+ public AnnotationVisitor visitArray(String name) {
+ return av;
+ }
+ }
+}
diff --git a/java/com/google/devtools/common/options/Converters.java b/java/com/google/devtools/common/options/Converters.java
index 35f2da4..fb3bbfa 100644
--- a/java/com/google/devtools/common/options/Converters.java
+++ b/java/com/google/devtools/common/options/Converters.java
@@ -26,10 +26,7 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
-/**
- * Some convenient converters used by blaze. Note: These are specific to
- * blaze.
- */
+/** Some convenient converters used by blaze. Note: These are specific to blaze. */
public final class Converters {
/** Standard converter for booleans. Accepts common shorthands/synonyms. */
@@ -180,9 +177,7 @@ public final class Converters {
}
}
- /**
- * Standard converter for the {@link java.time.Duration} type.
- */
+ /** Standard converter for the {@link java.time.Duration} type. */
public static class DurationConverter implements Converter<Duration> {
private final Pattern durationRegex = Pattern.compile("^([0-9]+)(d|h|m|s|ms)$");
@@ -198,7 +193,7 @@ public final class Converters {
}
long duration = Long.parseLong(m.group(1));
String unit = m.group(2);
- switch(unit) {
+ switch (unit) {
case "d":
return Duration.ofDays(duration);
case "h":
@@ -210,8 +205,8 @@ public final class Converters {
case "ms":
return Duration.ofMillis(duration);
default:
- throw new IllegalStateException("This must not happen. Did you update the regex without "
- + "the switch case?");
+ throw new IllegalStateException(
+ "This must not happen. Did you update the regex without the switch case?");
}
}
@@ -240,14 +235,8 @@ public final class Converters {
.build();
/**
- * Join a list of words as in English. Examples:
- * "nothing"
- * "one"
- * "one or two"
- * "one and two"
- * "one, two or three".
- * "one, two and three".
- * The toString method of each element is used.
+ * Join a list of words as in English. Examples: "nothing" "one" "one or two" "one and two" "one,
+ * two or three". "one, two and three". The toString method of each element is used.
*/
static String joinEnglishList(Iterable<?> choices) {
StringBuilder buf = new StringBuilder();
@@ -261,14 +250,12 @@ public final class Converters {
return buf.length() == 0 ? "nothing" : buf.toString();
}
- public static class SeparatedOptionListConverter
- implements Converter<List<String>> {
+ public static class SeparatedOptionListConverter implements Converter<List<String>> {
private final String separatorDescription;
private final Splitter splitter;
- protected SeparatedOptionListConverter(char separator,
- String separatorDescription) {
+ protected SeparatedOptionListConverter(char separator, String separatorDescription) {
this.separatorDescription = separatorDescription;
this.splitter = Splitter.on(separator);
}
@@ -284,8 +271,7 @@ public final class Converters {
}
}
- public static class CommaSeparatedOptionListConverter
- extends SeparatedOptionListConverter {
+ public static class CommaSeparatedOptionListConverter extends SeparatedOptionListConverter {
public CommaSeparatedOptionListConverter() {
super(',', "comma");
}
@@ -299,10 +285,10 @@ public final class Converters {
public static class LogLevelConverter implements Converter<Level> {
- public static final Level[] LEVELS = new Level[] {
- Level.OFF, Level.SEVERE, Level.WARNING, Level.INFO, Level.FINE,
- Level.FINER, Level.FINEST
- };
+ public static final Level[] LEVELS =
+ new Level[] {
+ Level.OFF, Level.SEVERE, Level.WARNING, Level.INFO, Level.FINE, Level.FINER, Level.FINEST
+ };
@Override
public Level convert(String input) throws OptionsParsingException {
@@ -318,12 +304,9 @@ public final class Converters {
public String getTypeDescription() {
return "0 <= an integer <= " + (LEVELS.length - 1);
}
-
}
- /**
- * Checks whether a string is part of a set of strings.
- */
+ /** Checks whether a string is part of a set of strings. */
public static class StringSetConverter implements Converter<String> {
// TODO(bazel-team): if this class never actually contains duplicates, we could s/List/Set/
@@ -349,9 +332,7 @@ public final class Converters {
}
}
- /**
- * Checks whether a string is a valid regex pattern and compiles it.
- */
+ /** Checks whether a string is a valid regex pattern and compiles it. */
public static class RegexPatternConverter implements Converter<Pattern> {
@Override
@@ -369,9 +350,7 @@ public final class Converters {
}
}
- /**
- * Limits the length of a string argument.
- */
+ /** Limits the length of a string argument. */
public static class LengthLimitingConverter implements Converter<String> {
private final int maxSize;
@@ -393,9 +372,7 @@ public final class Converters {
}
}
- /**
- * Checks whether an integer is in the given range.
- */
+ /** Checks whether an integer is in the given range. */
public static class RangeConverter implements Converter<Integer> {
final int minValue;
final int maxValue;
@@ -432,25 +409,27 @@ public final class Converters {
return "an integer, >= " + minValue;
} else {
return "an integer in "
- + (minValue < 0 ? "(" + minValue + ")" : minValue) + "-" + maxValue + " range";
+ + (minValue < 0 ? "(" + minValue + ")" : minValue)
+ + "-"
+ + maxValue
+ + " range";
}
}
}
/**
- * A converter for variable assignments from the parameter list of a blaze
- * command invocation. Assignments are expected to have the form "name=value",
- * where names and values are defined to be as permissive as possible.
+ * A converter for variable assignments from the parameter list of a blaze command invocation.
+ * Assignments are expected to have the form "name=value", where names and values are defined to
+ * be as permissive as possible.
*/
public static class AssignmentConverter implements Converter<Map.Entry<String, String>> {
@Override
- public Map.Entry<String, String> convert(String input)
- throws OptionsParsingException {
+ public Map.Entry<String, String> convert(String input) throws OptionsParsingException {
int pos = input.indexOf("=");
if (pos <= 0) {
- throw new OptionsParsingException("Variable definitions must be in the form of a "
- + "'name=value' assignment");
+ throw new OptionsParsingException(
+ "Variable definitions must be in the form of a 'name=value' assignment");
}
String name = input.substring(0, pos);
String value = input.substring(pos + 1);
@@ -461,24 +440,22 @@ public final class Converters {
public String getTypeDescription() {
return "a 'name=value' assignment";
}
-
}
/**
- * A converter for variable assignments from the parameter list of a blaze
- * command invocation. Assignments are expected to have the form "name[=value]",
- * where names and values are defined to be as permissive as possible and value
- * part can be optional (in which case it is considered to be null).
+ * A converter for variable assignments from the parameter list of a blaze command invocation.
+ * Assignments are expected to have the form "name[=value]", where names and values are defined to
+ * be as permissive as possible and value part can be optional (in which case it is considered to
+ * be null).
*/
public static class OptionalAssignmentConverter implements Converter<Map.Entry<String, String>> {
@Override
- public Map.Entry<String, String> convert(String input)
- throws OptionsParsingException {
- int pos = input.indexOf("=");
+ public Map.Entry<String, String> convert(String input) throws OptionsParsingException {
+ int pos = input.indexOf('=');
if (pos == 0 || input.length() == 0) {
- throw new OptionsParsingException("Variable definitions must be in the form of a "
- + "'name=value' or 'name' assignment");
+ throw new OptionsParsingException(
+ "Variable definitions must be in the form of a 'name=value' or 'name' assignment");
} else if (pos < 0) {
return Maps.immutableEntry(input, null);
}
@@ -491,7 +468,40 @@ public final class Converters {
public String getTypeDescription() {
return "a 'name=value' assignment with an optional value part";
}
+ }
+
+ /**
+ * A converter for named integers of the form "[name=]value". When no name is specified, an empty
+ * string is used for the key.
+ */
+ public static class NamedIntegersConverter implements Converter<Map.Entry<String, Integer>> {
+ @Override
+ public Map.Entry<String, Integer> convert(String input) throws OptionsParsingException {
+ int pos = input.indexOf('=');
+ if (pos == 0 || input.length() == 0) {
+ throw new OptionsParsingException(
+ "Specify either 'value' or 'name=value', where 'value' is an integer");
+ } else if (pos < 0) {
+ try {
+ return Maps.immutableEntry("", Integer.parseInt(input));
+ } catch (NumberFormatException e) {
+ throw new OptionsParsingException("'" + input + "' is not an int");
+ }
+ }
+ String name = input.substring(0, pos);
+ String value = input.substring(pos + 1);
+ try {
+ return Maps.immutableEntry(name, Integer.parseInt(value));
+ } catch (NumberFormatException e) {
+ throw new OptionsParsingException("'" + value + "' is not an int");
+ }
+ }
+
+ @Override
+ public String getTypeDescription() {
+ return "an integer or a named integer, 'name=value'";
+ }
}
public static class HelpVerbosityConverter extends EnumConverter<OptionsParser.HelpVerbosity> {
@@ -508,5 +518,4 @@ public final class Converters {
super(0, 100);
}
}
-
}
diff --git a/java/com/google/devtools/common/options/InvocationPolicyEnforcer.java b/java/com/google/devtools/common/options/InvocationPolicyEnforcer.java
index a53ff5b..3b42a29 100644
--- a/java/com/google/devtools/common/options/InvocationPolicyEnforcer.java
+++ b/java/com/google/devtools/common/options/InvocationPolicyEnforcer.java
@@ -248,6 +248,15 @@ public final class InvocationPolicyEnforcer {
OptionPriority nextPriority =
OptionPriority.lowestOptionPriorityAtCategory(PriorityCategory.INVOCATION_POLICY);
for (FlagPolicy policy : invocationPolicy.getFlagPoliciesList()) {
+ // Explicitly disallow --config in invocation policy.
+ if (policy.getFlagName().equals("config")) {
+ throw new OptionsParsingException(
+ "Invocation policy is applied after --config expansion, changing config values now "
+ + "would have no effect and is disallowed to prevent confusion. Please remove the "
+ + "following policy : "
+ + policy);
+ }
+
// These policies are high-level, before expansion, and so are not the implicitDependents or
// expansions of any other flag, other than in an obtuse sense from --invocation_policy.
OptionPriority currentPriority = nextPriority;
@@ -306,9 +315,7 @@ public final class InvocationPolicyEnforcer {
* <p>None of the flagPolicies returned should be on expansion flags.
*/
private static List<FlagPolicyWithContext> expandPolicy(
- FlagPolicyWithContext originalPolicy,
- OptionsParser parser,
- Level loglevel)
+ FlagPolicyWithContext originalPolicy, OptionsParser parser, Level loglevel)
throws OptionsParsingException {
List<FlagPolicyWithContext> expandedPolicies = new ArrayList<>();
@@ -701,11 +708,15 @@ public final class InvocationPolicyEnforcer {
}
// Check that if the default value of the flag is disallowed by the policy, that the policy
- // does not also set use_default. Otherwise the default value would will still be set if the
+ // does not also set use_default. Otherwise the default value would still be set if the
// user uses a disallowed value. This doesn't apply to repeatable flags since the default
- // value for repeatable flags is always the empty list.
- if (!optionDescription.getOptionDefinition().allowsMultiple()) {
-
+ // value for repeatable flags is always the empty list. It also doesn't apply to flags that
+ // are null by default, since these flags' default value is not parsed by the converter, so
+ // there is no guarantee that there exists an accepted user-input value that would also set
+ // the value to NULL. In these cases, we assume that "unset" is a distinct value that is
+ // always allowed.
+ if (!optionDescription.getOptionDefinition().allowsMultiple()
+ && !optionDescription.getOptionDefinition().isSpecialNullDefault()) {
boolean defaultValueAllowed =
isFlagValueAllowed(
convertedPolicyValues, optionDescription.getOptionDefinition().getDefaultValue());
@@ -749,8 +760,12 @@ public final class InvocationPolicyEnforcer {
throws OptionsParsingException {
OptionDefinition optionDefinition = optionDescription.getOptionDefinition();
- if (!isFlagValueAllowed(
- convertedPolicyValues, optionDescription.getOptionDefinition().getDefaultValue())) {
+ if (optionDefinition.isSpecialNullDefault()) {
+ // Do nothing, the unset value by definition cannot be set. In option filtering operations,
+ // the value is being filtered, but the value that is `no value` passes any filter.
+ // Otherwise, there is no way to "usedefault" on one of these options that has no value by
+ // default.
+ } else if (!isFlagValueAllowed(convertedPolicyValues, optionDefinition.getDefaultValue())) {
if (newValue != null) {
// Use the default value from the policy, since the original default is not allowed
logger.log(
diff --git a/java/com/google/devtools/common/options/Option.java b/java/com/google/devtools/common/options/Option.java
index 45f320a..f26a136 100644
--- a/java/com/google/devtools/common/options/Option.java
+++ b/java/com/google/devtools/common/options/Option.java
@@ -186,22 +186,4 @@ public @interface Option {
* that the old name is deprecated and the new name should be used.
*/
String oldName() default "";
-
- /**
- * Indicates that this option is a wrapper for other options, and will be unwrapped when parsed.
- * For example, if foo is a wrapper option, then "--foo=--bar=baz" will be parsed as the flag
- * "--bar=baz" (rather than --foo taking the value "--bar=baz"). A wrapper option should have the
- * type {@link Void} (if it is something other than Void, the parser will not assign a value to
- * it). The {@link Option#implicitRequirements()}, {@link Option#expansion()}, {@link
- * Option#converter()} attributes will not be processed. Wrapper options are implicitly repeatable
- * (i.e., as though {@link Option#allowMultiple()} is true regardless of its value in the
- * annotation).
- *
- * <p>Wrapper options are provided only for transitioning flags which appear as values to other
- * flags, to top-level flags. Wrapper options should not be used in Invocation Policy, as
- * expansion flags to other flags, or as implicit requirements to other flags. Use the inner flags
- * instead.
- */
- @Deprecated
- boolean wrapperOption() default false;
}
diff --git a/java/com/google/devtools/common/options/OptionDefinition.java b/java/com/google/devtools/common/options/OptionDefinition.java
index 84a9d2d..e89234b 100644
--- a/java/com/google/devtools/common/options/OptionDefinition.java
+++ b/java/com/google/devtools/common/options/OptionDefinition.java
@@ -137,7 +137,7 @@ public class OptionDefinition implements Comparable<OptionDefinition> {
}
/** {@link Option#expansionFunction()} ()} */
- Class<? extends ExpansionFunction> getExpansionFunction() {
+ public Class<? extends ExpansionFunction> getExpansionFunction() {
return optionAnnotation.expansionFunction();
}
@@ -156,11 +156,6 @@ public class OptionDefinition implements Comparable<OptionDefinition> {
return optionAnnotation.oldName();
}
- /** {@link Option#wrapperOption()} ()} ()} */
- public boolean isWrapperOption() {
- return optionAnnotation.wrapperOption();
- }
-
/** Returns whether an option --foo has a negative equivalent --nofoo. */
public boolean hasNegativeOption() {
return getType().equals(boolean.class) || getType().equals(TriState.class);
diff --git a/java/com/google/devtools/common/options/OptionFilterDescriptions.java b/java/com/google/devtools/common/options/OptionFilterDescriptions.java
index 2a7999d..4b4373a 100644
--- a/java/com/google/devtools/common/options/OptionFilterDescriptions.java
+++ b/java/com/google/devtools/common/options/OptionFilterDescriptions.java
@@ -170,6 +170,9 @@ public class OptionFilterDescriptions {
"This option is deprecated. It might be that the feature it affects is deprecated, "
+ "or that another method of supplying the information is preferred.")
.put(
+ OptionMetadataTag.TRIGGERED_BY_ALL_INCOMPATIBLE_CHANGES,
+ "This option is triggered by the expansion option --all_incompatible_changes.")
+ .put(
OptionMetadataTag.HIDDEN, // Here for completeness, these options are UNDOCUMENTED.
"This option should not be used by a user, and should not be logged.")
.put(
diff --git a/java/com/google/devtools/common/options/OptionInstanceOrigin.java b/java/com/google/devtools/common/options/OptionInstanceOrigin.java
index 584e75b..b0782f8 100644
--- a/java/com/google/devtools/common/options/OptionInstanceOrigin.java
+++ b/java/com/google/devtools/common/options/OptionInstanceOrigin.java
@@ -22,14 +22,14 @@ import javax.annotation.Nullable;
public class OptionInstanceOrigin {
private final OptionPriority priority;
@Nullable private final String source;
- @Nullable private final OptionDefinition implicitDependent;
- @Nullable private final OptionDefinition expandedFrom;
+ @Nullable private final ParsedOptionDescription implicitDependent;
+ @Nullable private final ParsedOptionDescription expandedFrom;
public OptionInstanceOrigin(
OptionPriority priority,
String source,
- OptionDefinition implicitDependent,
- OptionDefinition expandedFrom) {
+ ParsedOptionDescription implicitDependent,
+ ParsedOptionDescription expandedFrom) {
this.priority = priority;
this.source = source;
this.implicitDependent = implicitDependent;
@@ -46,12 +46,12 @@ public class OptionInstanceOrigin {
}
@Nullable
- public OptionDefinition getImplicitDependent() {
+ public ParsedOptionDescription getImplicitDependent() {
return implicitDependent;
}
@Nullable
- public OptionDefinition getExpandedFrom() {
+ public ParsedOptionDescription getExpandedFrom() {
return expandedFrom;
}
}
diff --git a/java/com/google/devtools/common/options/OptionMetadataTag.java b/java/com/google/devtools/common/options/OptionMetadataTag.java
index c511fa6..563aa3e 100644
--- a/java/com/google/devtools/common/options/OptionMetadataTag.java
+++ b/java/com/google/devtools/common/options/OptionMetadataTag.java
@@ -55,7 +55,17 @@ public enum OptionMetadataTag {
*
* <p>These should be in category {@code OptionDocumentationCategory.UNDOCUMENTED}.
*/
- INTERNAL(4);
+ INTERNAL(4),
+
+ /**
+ * Options that are triggered by --all_incompatible_changes.
+ *
+ * <p>These must also be labelled {@link OptionMetadataTag#INCOMPATIBLE_CHANGE} and have the
+ * prefix --incompatible_. Note that the option name prefix is also a triggering case for the
+ * --all_incompatible_changes expansion, and so all options that start with the "incompatible_"
+ * prefix must have this tag.
+ */
+ TRIGGERED_BY_ALL_INCOMPATIBLE_CHANGES(5);
private final int value;
diff --git a/java/com/google/devtools/common/options/OptionPriority.java b/java/com/google/devtools/common/options/OptionPriority.java
index ec5d0d8..53f0d75 100644
--- a/java/com/google/devtools/common/options/OptionPriority.java
+++ b/java/com/google/devtools/common/options/OptionPriority.java
@@ -13,6 +13,7 @@
// limitations under the License.
package com.google.devtools.common.options;
+import com.google.common.collect.ImmutableList;
import java.util.Objects;
/**
@@ -25,43 +26,63 @@ import java.util.Objects;
*/
public class OptionPriority implements Comparable<OptionPriority> {
private final PriorityCategory priorityCategory;
- private final int index;
- private final boolean locked;
+ /**
+ * Each option that is passed explicitly has 0 ancestors, so it only has its command line index
+ * (or rc index, etc., depending on the category), but expanded options have the command line
+ * index of its parent and then its position within the options that were expanded at that point.
+ * Since options can expand to expanding options, and --config can expand to expansion options as
+ * well, this can technically go arbitrarily deep, but in practice this is very short, of length <
+ * 5, most commonly of length 1.
+ */
+ private final ImmutableList<Integer> priorityIndices;
+
+ private boolean alreadyExpanded = false;
- private OptionPriority(PriorityCategory priorityCategory, int index, boolean locked) {
+ private OptionPriority(
+ PriorityCategory priorityCategory, ImmutableList<Integer> priorityIndices) {
this.priorityCategory = priorityCategory;
- this.index = index;
- this.locked = locked;
+ this.priorityIndices = priorityIndices;
}
/** Get the first OptionPriority for that category. */
static OptionPriority lowestOptionPriorityAtCategory(PriorityCategory category) {
- return new OptionPriority(category, 0, false);
+ return new OptionPriority(category, ImmutableList.of(0));
}
/**
* Get the priority for the option following this one. In normal, incremental option parsing, the
- * returned priority would compareTo as after the current one. Does not increment locked
+ * returned priority would compareTo as after the current one. Does not increment ancestor
* priorities.
*/
static OptionPriority nextOptionPriority(OptionPriority priority) {
- if (priority.locked) {
- return priority;
- }
- return new OptionPriority(priority.priorityCategory, priority.index + 1, false);
+ int lastElementPosition = priority.priorityIndices.size() - 1;
+ return new OptionPriority(
+ priority.priorityCategory,
+ ImmutableList.<Integer>builder()
+ .addAll(priority.priorityIndices.subList(0, lastElementPosition))
+ .add(priority.priorityIndices.get(lastElementPosition) + 1)
+ .build());
}
/**
- * Return a priority for this option that will avoid priority increases by calls to
- * nextOptionPriority.
+ * Some options are expanded to other options, and the children options need to have their order
+ * preserved while maintaining their position between the options that flank the parent option.
*
- * <p>Some options are expanded in-place, and need to be all parsed at the priority of the
- * original option. In this case, parsing one of these after another should not cause the option
- * to be considered as higher priority than the ones before it (this would cause overlap between
- * the expansion of --expansion_flag and a option following it in the same list of options).
+ * @return the priority for the first child of the passed priority. This child's ordering can be
+ * tracked the same way that the parent's was.
*/
- public static OptionPriority getLockedPriority(OptionPriority priority) {
- return new OptionPriority(priority.priorityCategory, priority.index, true);
+ public static OptionPriority getChildPriority(OptionPriority parentPriority)
+ throws OptionsParsingException {
+ if (parentPriority.alreadyExpanded) {
+ throw new OptionsParsingException("Tried to expand option too many times");
+ }
+ // Prevent this option from being re-expanded.
+ parentPriority.alreadyExpanded = true;
+
+ // The child priority has 1 more level of nesting than its parent.
+ return new OptionPriority(
+ parentPriority.priorityCategory,
+ ImmutableList.<Integer>builder().addAll(parentPriority.priorityIndices).add(0).build());
}
public PriorityCategory getPriorityCategory() {
@@ -71,28 +92,36 @@ public class OptionPriority implements Comparable<OptionPriority> {
@Override
public int compareTo(OptionPriority o) {
if (priorityCategory.equals(o.priorityCategory)) {
- return index - o.index;
+ for (int i = 0; i < priorityIndices.size() && i < o.priorityIndices.size(); ++i) {
+ if (!priorityIndices.get(i).equals(o.priorityIndices.get(i))) {
+ return priorityIndices.get(i).compareTo(o.priorityIndices.get(i));
+ }
+ }
+ // The values are up to the shorter one's length are the same, so the shorter one is a direct
+ // ancestor and comes first.
+ return Integer.compare(priorityIndices.size(), o.priorityIndices.size());
}
- return priorityCategory.ordinal() - o.priorityCategory.ordinal();
+ return Integer.compare(priorityCategory.ordinal(), o.priorityCategory.ordinal());
}
@Override
public boolean equals(Object o) {
if (o instanceof OptionPriority) {
OptionPriority other = (OptionPriority) o;
- return other.priorityCategory.equals(priorityCategory) && other.index == index;
+ return priorityCategory.equals(other.priorityCategory)
+ && priorityIndices.equals(other.priorityIndices);
}
return false;
}
@Override
public int hashCode() {
- return Objects.hash(priorityCategory, index);
+ return Objects.hash(priorityCategory, priorityIndices);
}
@Override
public String toString() {
- return String.format("OptionPriority(%s,%s)", priorityCategory, index);
+ return String.format("OptionPriority(%s,%s)", priorityCategory, priorityIndices);
}
/**
diff --git a/java/com/google/devtools/common/options/OptionValueDescription.java b/java/com/google/devtools/common/options/OptionValueDescription.java
index 616b3b5..11ad7fe 100644
--- a/java/com/google/devtools/common/options/OptionValueDescription.java
+++ b/java/com/google/devtools/common/options/OptionValueDescription.java
@@ -22,7 +22,7 @@ import com.google.devtools.common.options.OptionsParser.ConstructionException;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
-import java.util.Map.Entry;
+import java.util.Map;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
@@ -98,8 +98,6 @@ public abstract class OptionValueDescription {
return new RepeatableOptionValueDescription(option);
} else if (option.hasImplicitRequirements()) {
return new OptionWithImplicitRequirementsValueDescription(option);
- } else if (option.isWrapperOption()) {
- return new WrapperOptionValueDescription(option);
} else {
return new SingleOptionValueDescription(option);
}
@@ -187,11 +185,11 @@ public abstract class OptionValueDescription {
// log warnings describing the change.
if (parsedOption.getPriority().compareTo(effectiveOptionInstance.getPriority()) >= 0) {
// Identify the option that might have led to the current and new value of this option.
- OptionDefinition implicitDependent = parsedOption.getImplicitDependent();
- OptionDefinition expandedFrom = parsedOption.getExpandedFrom();
- OptionDefinition optionThatDependsOnEffectiveValue =
+ ParsedOptionDescription implicitDependent = parsedOption.getImplicitDependent();
+ ParsedOptionDescription expandedFrom = parsedOption.getExpandedFrom();
+ ParsedOptionDescription optionThatDependsOnEffectiveValue =
effectiveOptionInstance.getImplicitDependent();
- OptionDefinition optionThatExpandedToEffectiveValue =
+ ParsedOptionDescription optionThatExpandedToEffectiveValue =
effectiveOptionInstance.getExpandedFrom();
Object newValue = parsedOption.getConvertedValue();
@@ -227,7 +225,7 @@ public abstract class OptionValueDescription {
// Create a warning if an expansion option overrides an explicit option:
warnings.add(
String.format(
- "%s was expanded and now overrides a previous explicitly specified %s with %s",
+ "%s was expanded and now overrides the explicit option %s with %s",
expandedFrom,
effectiveOptionInstance.getCommandLineForm(),
parsedOption.getCommandLineForm()));
@@ -278,8 +276,8 @@ public abstract class OptionValueDescription {
.asMap()
.entrySet()
.stream()
- .sorted(Comparator.comparing(Entry::getKey))
- .map(Entry::getValue)
+ .sorted(Comparator.comparing(Map.Entry::getKey))
+ .map(Map.Entry::getValue)
.flatMap(Collection::stream)
.map(ParsedOptionDescription::getSource)
.distinct()
@@ -294,8 +292,8 @@ public abstract class OptionValueDescription {
.asMap()
.entrySet()
.stream()
- .sorted(Comparator.comparing(Entry::getKey))
- .map(Entry::getValue)
+ .sorted(Comparator.comparing(Map.Entry::getKey))
+ .map(Map.Entry::getValue)
.flatMap(Collection::stream)
.collect(Collectors.toList());
}
@@ -322,8 +320,8 @@ public abstract class OptionValueDescription {
.asMap()
.entrySet()
.stream()
- .sorted(Comparator.comparing(Entry::getKey))
- .map(Entry::getValue)
+ .sorted(Comparator.comparing(Map.Entry::getKey))
+ .map(Map.Entry::getValue)
.flatMap(Collection::stream)
// Only provide the options that aren't implied elsewhere.
.filter(optionDesc -> optionDesc.getImplicitDependent() == null)
@@ -430,50 +428,6 @@ public abstract class OptionValueDescription {
optionDefinition, parsedOption.getSource()));
}
}
-
- /** Form for options that contain other options in the value text to which they expand. */
- private static final class WrapperOptionValueDescription extends OptionValueDescription {
-
- WrapperOptionValueDescription(OptionDefinition optionDefinition) {
- super(optionDefinition);
- }
-
- @Override
- public Object getValue() {
- return null;
- }
-
- @Override
- public String getSourceString() {
- return null;
- }
-
- @Override
- ExpansionBundle addOptionInstance(ParsedOptionDescription parsedOption, List<String> warnings)
- throws OptionsParsingException {
- if (!parsedOption.getUnconvertedValue().startsWith("-")) {
- throw new OptionsParsingException(
- String.format(
- "Invalid value format for %s. You may have meant --%s=--%s",
- optionDefinition,
- optionDefinition.getOptionName(),
- parsedOption.getUnconvertedValue()));
- }
- return new ExpansionBundle(
- ImmutableList.of(parsedOption.getUnconvertedValue()),
- (parsedOption.getSource() == null)
- ? String.format("unwrapped from %s", optionDefinition)
- : String.format(
- "unwrapped from %s (source %s)", optionDefinition, parsedOption.getSource()));
- }
-
- @Override
- public ImmutableList<ParsedOptionDescription> getCanonicalInstances() {
- // No wrapper options get listed in the canonical form - the options they are wrapping will
- // be in the right place.
- return ImmutableList.of();
- }
- }
}
diff --git a/java/com/google/devtools/common/options/OptionsBase.java b/java/com/google/devtools/common/options/OptionsBase.java
index 6b9f2f1..9496c65 100644
--- a/java/com/google/devtools/common/options/OptionsBase.java
+++ b/java/com/google/devtools/common/options/OptionsBase.java
@@ -20,7 +20,6 @@ import java.lang.reflect.Field;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
-import java.util.Map.Entry;
/**
* Base class for all options classes. Extend this class, adding public instance fields annotated
@@ -93,7 +92,7 @@ public abstract class OptionsBase {
public final String cacheKey() {
StringBuilder result = new StringBuilder(getClass().getName()).append("{");
- for (Entry<String, Object> entry : asMap().entrySet()) {
+ for (Map.Entry<String, Object> entry : asMap().entrySet()) {
result.append(entry.getKey()).append("=");
Object value = entry.getValue();
diff --git a/java/com/google/devtools/common/options/OptionsParser.java b/java/com/google/devtools/common/options/OptionsParser.java
index fb7161c..6f1d7b6 100644
--- a/java/com/google/devtools/common/options/OptionsParser.java
+++ b/java/com/google/devtools/common/options/OptionsParser.java
@@ -33,7 +33,6 @@ import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
-import java.util.Map.Entry;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
@@ -299,7 +298,7 @@ public class OptionsParser implements OptionsProvider {
getOptionsSortedByCategory();
ImmutableMap<OptionDocumentationCategory, String> optionCategoryDescriptions =
OptionFilterDescriptions.getOptionCategoriesEnumDescription(productName);
- for (Entry<OptionDocumentationCategory, List<OptionDefinition>> e :
+ for (Map.Entry<OptionDocumentationCategory, List<OptionDefinition>> e :
optionsByCategory.entrySet()) {
String categoryDescription = optionCategoryDescriptions.get(e.getKey());
List<OptionDefinition> categorizedOptionList = e.getValue();
@@ -463,7 +462,7 @@ public class OptionsParser implements OptionsProvider {
ImmutableMap<OptionDocumentationCategory, String> optionCategoryDescriptions =
OptionFilterDescriptions.getOptionCategoriesEnumDescription(productName);
- for (Entry<OptionDocumentationCategory, List<OptionDefinition>> e :
+ for (Map.Entry<OptionDocumentationCategory, List<OptionDefinition>> e :
optionsByCategory.entrySet()) {
desc.append("<dl>");
String categoryDescription = optionCategoryDescriptions.get(e.getKey());
@@ -619,13 +618,26 @@ public class OptionsParser implements OptionsProvider {
}
}
- public void parseOptionsFixedAtSpecificPriority(
- OptionPriority priority, String source, List<String> args) throws OptionsParsingException {
- Preconditions.checkNotNull(priority, "Priority not specified for arglist " + args);
+ /**
+ * Parses the args at the priority of the provided option. This is useful for after-the-fact
+ * expansion.
+ *
+ * @param optionToExpand the option that is being "expanded" after the fact. The provided args
+ * will have the same priority as this option.
+ * @param source a description of where the expansion arguments came from.
+ * @param args the arguments to parse as the expansion. Order matters, as the value of a flag may
+ * be in the following argument.
+ */
+ public void parseArgsAsExpansionOfOption(
+ ParsedOptionDescription optionToExpand, String source, List<String> args)
+ throws OptionsParsingException {
+ Preconditions.checkNotNull(
+ optionToExpand, "Option for expansion not specified for arglist " + args);
Preconditions.checkArgument(
- priority.getPriorityCategory() != OptionPriority.PriorityCategory.DEFAULT,
+ optionToExpand.getPriority().getPriorityCategory()
+ != OptionPriority.PriorityCategory.DEFAULT,
"Priority cannot be default, which was specified for arglist " + args);
- residue.addAll(impl.parseOptionsFixedAtSpecificPriority(priority, o -> source, args));
+ residue.addAll(impl.parseArgsAsExpansionOfOption(optionToExpand, o -> source, args));
if (!allowResidue && !residue.isEmpty()) {
String errorMsg = "Unrecognized arguments: " + Joiner.on(' ').join(residue);
throw new OptionsParsingException(errorMsg);
diff --git a/java/com/google/devtools/common/options/OptionsParserImpl.java b/java/com/google/devtools/common/options/OptionsParserImpl.java
index 496927b..2c15430 100644
--- a/java/com/google/devtools/common/options/OptionsParserImpl.java
+++ b/java/com/google/devtools/common/options/OptionsParserImpl.java
@@ -142,9 +142,10 @@ class OptionsParserImpl {
return optionValues
.keySet()
.stream()
- .sorted()
.map(optionDefinition -> optionValues.get(optionDefinition).getCanonicalInstances())
.flatMap(Collection::stream)
+ // Return the effective (canonical) options in the order they were applied.
+ .sorted(comparing(ParsedOptionDescription::getPriority))
.collect(ImmutableList.toImmutableList());
}
@@ -204,30 +205,33 @@ class OptionsParserImpl {
* OptionInstanceOrigin)}
*/
ImmutableList<ParsedOptionDescription> getExpansionValueDescriptions(
- OptionDefinition expansionFlag, OptionInstanceOrigin originOfExpansionFlag)
+ OptionDefinition expansionFlagDef, OptionInstanceOrigin originOfExpansionFlag)
throws OptionsParsingException {
ImmutableList.Builder<ParsedOptionDescription> builder = ImmutableList.builder();
- OptionInstanceOrigin originOfSubflags;
+
+ // Values needed to correctly track the origin of the expanded options.
+ OptionPriority nextOptionPriority =
+ OptionPriority.getChildPriority(originOfExpansionFlag.getPriority());
+ String source;
+ ParsedOptionDescription implicitDependent = null;
+ ParsedOptionDescription expandedFrom = null;
+
ImmutableList<String> options;
- if (expansionFlag.hasImplicitRequirements()) {
- options = ImmutableList.copyOf(expansionFlag.getImplicitRequirements());
- originOfSubflags =
- new OptionInstanceOrigin(
- originOfExpansionFlag.getPriority(),
- String.format(
- "implicitly required by %s (source: %s)",
- expansionFlag, originOfExpansionFlag.getSource()),
- expansionFlag,
- null);
- } else if (expansionFlag.isExpansionOption()) {
- options = optionsData.getEvaluatedExpansion(expansionFlag);
- originOfSubflags =
- new OptionInstanceOrigin(
- originOfExpansionFlag.getPriority(),
- String.format(
- "expanded by %s (source: %s)", expansionFlag, originOfExpansionFlag.getSource()),
- null,
- expansionFlag);
+ ParsedOptionDescription expansionFlagParsedDummy =
+ ParsedOptionDescription.newDummyInstance(expansionFlagDef, originOfExpansionFlag);
+ if (expansionFlagDef.hasImplicitRequirements()) {
+ options = ImmutableList.copyOf(expansionFlagDef.getImplicitRequirements());
+ source =
+ String.format(
+ "implicitly required by %s (source: %s)",
+ expansionFlagDef, originOfExpansionFlag.getSource());
+ implicitDependent = expansionFlagParsedDummy;
+ } else if (expansionFlagDef.isExpansionOption()) {
+ options = optionsData.getEvaluatedExpansion(expansionFlagDef);
+ source =
+ String.format(
+ "expanded by %s (source: %s)", expansionFlagDef, originOfExpansionFlag.getSource());
+ expandedFrom = expansionFlagParsedDummy;
} else {
return ImmutableList.of();
}
@@ -239,11 +243,12 @@ class OptionsParserImpl {
identifyOptionAndPossibleArgument(
unparsedFlagExpression,
optionsIterator,
- originOfSubflags.getPriority(),
- o -> originOfSubflags.getSource(),
- originOfSubflags.getImplicitDependent(),
- originOfSubflags.getExpandedFrom());
+ nextOptionPriority,
+ o -> source,
+ implicitDependent,
+ expandedFrom);
builder.add(parsedOption);
+ nextOptionPriority = OptionPriority.nextOptionPriority(nextOptionPriority);
}
return builder.build();
}
@@ -284,12 +289,19 @@ class OptionsParserImpl {
}
}
- /** Parses the args at the fixed priority. */
- List<String> parseOptionsFixedAtSpecificPriority(
- OptionPriority priority, Function<OptionDefinition, String> sourceFunction, List<String> args)
+ /** Implements {@link OptionsParser#parseArgsAsExpansionOfOption} */
+ List<String> parseArgsAsExpansionOfOption(
+ ParsedOptionDescription optionToExpand,
+ Function<OptionDefinition, String> sourceFunction,
+ List<String> args)
throws OptionsParsingException {
ResidueAndPriority residueAndPriority =
- parse(OptionPriority.getLockedPriority(priority), sourceFunction, null, null, args);
+ parse(
+ OptionPriority.getChildPriority(optionToExpand.getPriority()),
+ sourceFunction,
+ null,
+ optionToExpand,
+ args);
return residueAndPriority.residue;
}
@@ -304,8 +316,8 @@ class OptionsParserImpl {
private ResidueAndPriority parse(
OptionPriority priority,
Function<OptionDefinition, String> sourceFunction,
- OptionDefinition implicitDependent,
- OptionDefinition expandedFrom,
+ ParsedOptionDescription implicitDependent,
+ ParsedOptionDescription expandedFrom,
List<String> args)
throws OptionsParsingException {
List<String> unparsedArgs = new ArrayList<>();
@@ -369,7 +381,7 @@ class OptionsParserImpl {
priorityCategory);
handleNewParsedOption(
- new ParsedOptionDescription(
+ ParsedOptionDescription.newParsedOptionDescription(
option,
String.format("--%s=%s", option.getOptionName(), unconvertedValue),
unconvertedValue,
@@ -392,16 +404,15 @@ class OptionsParserImpl {
@Nullable String unconvertedValue = parsedOption.getUnconvertedValue();
// There are 3 types of flags that expand to other flag values. Expansion flags are the
- // accepted way to do this, but two legacy features remain: implicit requirements and wrapper
- // options. We rely on the OptionProcessor compile-time check's guarantee that no option sets
- // multiple of these behaviors. (In Bazel, --config is another such flag, but that expansion
+ // accepted way to do this, but implicit requirements also do this. We rely on the
+ // OptionProcessor compile-time check's guarantee that no option sets
+ // both expansion behaviors. (In Bazel, --config is another such flag, but that expansion
// is not controlled within the options parser, so we ignore it here)
// As much as possible, we want the behaviors of these different types of flags to be
// identical, as this minimizes the number of edge cases, but we do not yet track these values
- // in the same way. Wrapper options are replaced by their value and implicit requirements are
- // hidden from the reported lists of parsed options.
- if (parsedOption.getImplicitDependent() == null && !optionDefinition.isWrapperOption()) {
+ // in the same way.
+ if (parsedOption.getImplicitDependent() == null) {
// Log explicit options and expanded options in the order they are parsed (can be sorted
// later). This information is needed to correctly canonicalize flags.
parsedOptions.add(parsedOption);
@@ -410,19 +421,13 @@ class OptionsParserImpl {
if (expansionBundle != null) {
ResidueAndPriority residueAndPriority =
parse(
- OptionPriority.getLockedPriority(parsedOption.getPriority()),
+ OptionPriority.getChildPriority(parsedOption.getPriority()),
o -> expansionBundle.sourceOfExpansionArgs,
- optionDefinition.hasImplicitRequirements() ? optionDefinition : null,
- optionDefinition.isExpansionOption() ? optionDefinition : null,
+ optionDefinition.hasImplicitRequirements() ? parsedOption : null,
+ optionDefinition.isExpansionOption() ? parsedOption : null,
expansionBundle.expansionArgs);
if (!residueAndPriority.residue.isEmpty()) {
- if (optionDefinition.isWrapperOption()) {
- throw new OptionsParsingException(
- "Unparsed options remain after unwrapping "
- + unconvertedValue
- + ": "
- + Joiner.on(' ').join(residueAndPriority.residue));
- } else {
+
// Throw an assertion here, because this indicates an error in the definition of this
// option's expansion or requirements, not with the input as provided by the user.
throw new AssertionError(
@@ -430,7 +435,7 @@ class OptionsParserImpl {
+ unconvertedValue
+ ": "
+ Joiner.on(' ').join(residueAndPriority.residue));
- }
+
}
}
}
@@ -440,8 +445,8 @@ class OptionsParserImpl {
Iterator<String> nextArgs,
OptionPriority priority,
Function<OptionDefinition, String> sourceFunction,
- OptionDefinition implicitDependent,
- OptionDefinition expandedFrom)
+ ParsedOptionDescription implicitDependent,
+ ParsedOptionDescription expandedFrom)
throws OptionsParsingException {
// Store the way this option was parsed on the command line.
@@ -506,9 +511,8 @@ class OptionsParserImpl {
// Special-case boolean to supply value based on presence of "no" prefix.
if (optionDefinition.usesBooleanValueSyntax()) {
unconvertedValue = booleanValue ? "1" : "0";
- } else if (optionDefinition.getType().equals(Void.class)
- && !optionDefinition.isWrapperOption()) {
- // This is expected, Void type options have no args (unless they're wrapper options).
+ } else if (optionDefinition.getType().equals(Void.class)) {
+ // This is expected, Void type options have no args.
} else if (nextArgs.hasNext()) {
// "--flag value" form
unconvertedValue = nextArgs.next();
@@ -518,7 +522,7 @@ class OptionsParserImpl {
}
}
- return new ParsedOptionDescription(
+ return ParsedOptionDescription.newParsedOptionDescription(
optionDefinition,
commandLineForm.toString(),
unconvertedValue,
diff --git a/java/com/google/devtools/common/options/OptionsProvider.java b/java/com/google/devtools/common/options/OptionsProvider.java
index d467fe5..ece5d5d 100644
--- a/java/com/google/devtools/common/options/OptionsProvider.java
+++ b/java/com/google/devtools/common/options/OptionsProvider.java
@@ -73,11 +73,13 @@ public interface OptionsProvider extends OptionsClassProvider {
List<OptionValueDescription> asListOfOptionValues();
/**
- * Canonicalizes the list of options that this OptionsParser has parsed. The
- * contract is that if the returned set of options is passed to an options
- * parser with the same options classes, then that will have the same effect
- * as using the original args (which are passed in here), except for cosmetic
- * differences.
+ * Canonicalizes the list of options that this OptionsParser has parsed.
+ *
+ * <p>The contract is that if the returned set of options is passed to an options parser with the
+ * same options classes, then that will have the same effect as using the original args (which are
+ * passed in here), except for cosmetic differences. We do not guarantee that the 'canonical' list
+ * is unique, since some flags may have effects unknown to the parser (--config, for Bazel), so we
+ * do not reorder flags to further simplify the list.
*/
List<String> canonicalize();
}
diff --git a/java/com/google/devtools/common/options/ParsedOptionDescription.java b/java/com/google/devtools/common/options/ParsedOptionDescription.java
index f55f8ad..5088153 100644
--- a/java/com/google/devtools/common/options/ParsedOptionDescription.java
+++ b/java/com/google/devtools/common/options/ParsedOptionDescription.java
@@ -14,6 +14,7 @@
package com.google.devtools.common.options;
+import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import java.util.function.Function;
import javax.annotation.Nullable;
@@ -27,25 +28,49 @@ import javax.annotation.Nullable;
public final class ParsedOptionDescription {
private final OptionDefinition optionDefinition;
- private final String commandLineForm;
+ @Nullable private final String commandLineForm;
@Nullable private final String unconvertedValue;
private final OptionInstanceOrigin origin;
- public ParsedOptionDescription(
+ private ParsedOptionDescription(
OptionDefinition optionDefinition,
- String commandLineForm,
+ @Nullable String commandLineForm,
@Nullable String unconvertedValue,
OptionInstanceOrigin origin) {
- this.optionDefinition = optionDefinition;
+ this.optionDefinition = Preconditions.checkNotNull(optionDefinition);
this.commandLineForm = commandLineForm;
this.unconvertedValue = unconvertedValue;
- this.origin = origin;
+ this.origin = Preconditions.checkNotNull(origin);
+ }
+
+ static ParsedOptionDescription newParsedOptionDescription(
+ OptionDefinition optionDefinition,
+ String commandLineForm,
+ @Nullable String unconvertedValue,
+ OptionInstanceOrigin origin) {
+ // An actual ParsedOptionDescription should always have a form in which it was parsed, but some
+ // options, such as expansion options, legitimately have no value.
+ return new ParsedOptionDescription(
+ optionDefinition,
+ Preconditions.checkNotNull(commandLineForm),
+ unconvertedValue,
+ origin);
+ }
+
+ /**
+ * This factory should be used when there is no actual parsed option, since in those cases we do
+ * not have an original value or form that the option took.
+ */
+ static ParsedOptionDescription newDummyInstance(
+ OptionDefinition optionDefinition, OptionInstanceOrigin origin) {
+ return new ParsedOptionDescription(optionDefinition, null, null, origin);
}
public OptionDefinition getOptionDefinition() {
return optionDefinition;
}
+ @Nullable
public String getCommandLineForm() {
return commandLineForm;
}
@@ -127,11 +152,11 @@ public final class ParsedOptionDescription {
return origin.getSource();
}
- OptionDefinition getImplicitDependent() {
+ ParsedOptionDescription getImplicitDependent() {
return origin.getImplicitDependent();
}
- OptionDefinition getExpandedFrom() {
+ ParsedOptionDescription getExpandedFrom() {
return origin.getExpandedFrom();
}
@@ -152,14 +177,14 @@ public final class ParsedOptionDescription {
@Override
public String toString() {
- StringBuilder result = new StringBuilder();
- result.append(optionDefinition);
- result.append("set to '").append(unconvertedValue).append("' ");
- result.append("with priority ").append(origin.getPriority());
- if (origin.getSource() != null) {
- result.append(" and source '").append(origin.getSource()).append("'");
+ // Check that a dummy value-less option instance does not output all the default information.
+ if (commandLineForm == null) {
+ return optionDefinition.toString();
}
- return result.toString();
+ String source = origin.getSource();
+ return String.format(
+ "option '%s'%s",
+ commandLineForm, source == null ? "" : String.format(" (source %s)", source));
}
}
diff --git a/java/com/google/devtools/common/options/processor/OptionProcessor.java b/java/com/google/devtools/common/options/processor/OptionProcessor.java
index fd7c023..485efcd 100644
--- a/java/com/google/devtools/common/options/processor/OptionProcessor.java
+++ b/java/com/google/devtools/common/options/processor/OptionProcessor.java
@@ -15,7 +15,6 @@ package com.google.devtools.common.options.processor;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ImmutableMap.Builder;
import com.google.devtools.common.options.Converter;
import com.google.devtools.common.options.Converters;
import com.google.devtools.common.options.ExpansionFunction;
@@ -27,7 +26,7 @@ import com.google.devtools.common.options.OptionsBase;
import com.google.devtools.common.options.OptionsParser;
import com.google.devtools.common.options.OptionsParsingException;
import java.util.List;
-import java.util.Map.Entry;
+import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@@ -36,7 +35,6 @@ import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
-import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
@@ -74,7 +72,6 @@ import javax.tools.Diagnostic;
* <p>These properties can be relied upon at runtime without additional checks.
*/
@SupportedAnnotationTypes({"com.google.devtools.common.options.Option"})
-@SupportedSourceVersion(SourceVersion.RELEASE_8)
public final class OptionProcessor extends AbstractProcessor {
private Types typeUtils;
@@ -84,6 +81,11 @@ public final class OptionProcessor extends AbstractProcessor {
private ImmutableMap<Class<?>, PrimitiveType> primitiveTypeMap;
@Override
+ public SourceVersion getSupportedSourceVersion() {
+ return SourceVersion.latestSupported();
+ }
+
+ @Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
typeUtils = processingEnv.getTypeUtils();
@@ -93,18 +95,19 @@ public final class OptionProcessor extends AbstractProcessor {
// Because of the discrepancies between the java.lang and javax.lang type models, we can't
// directly use the get() method for the default converter map. Instead, we'll convert it once,
// to be more usable, and with the boxed type return values of convert() as the keys.
- ImmutableMap.Builder<TypeMirror, Converter<?>> converterMapBuilder = new Builder<>();
+ ImmutableMap.Builder<TypeMirror, Converter<?>> converterMapBuilder =
+ new ImmutableMap.Builder<>();
// Create a link from the primitive Classes to their primitive types. This intentionally
// only contains the types in the DEFAULT_CONVERTERS map.
- ImmutableMap.Builder<Class<?>, PrimitiveType> builder = new Builder<>();
+ ImmutableMap.Builder<Class<?>, PrimitiveType> builder = new ImmutableMap.Builder<>();
builder.put(int.class, typeUtils.getPrimitiveType(TypeKind.INT));
builder.put(double.class, typeUtils.getPrimitiveType(TypeKind.DOUBLE));
builder.put(boolean.class, typeUtils.getPrimitiveType(TypeKind.BOOLEAN));
builder.put(long.class, typeUtils.getPrimitiveType(TypeKind.LONG));
primitiveTypeMap = builder.build();
- for (Entry<Class<?>, Converter<?>> entry : Converters.DEFAULT_CONVERTERS.entrySet()) {
+ for (Map.Entry<Class<?>, Converter<?>> entry : Converters.DEFAULT_CONVERTERS.entrySet()) {
Class<?> converterClass = entry.getKey();
String typeName = converterClass.getCanonicalName();
TypeElement typeElement = elementUtils.getTypeElement(typeName);
@@ -476,10 +479,6 @@ public final class OptionProcessor extends AbstractProcessor {
}
if (isExpansion || hasImplicitRequirements) {
- if (annotation.wrapperOption()) {
- throw new OptionProcessorException(
- optionField, "Wrapper options cannot have expansions or implicit requirements.");
- }
if (annotation.allowMultiple()) {
throw new OptionProcessorException(
optionField,
@@ -488,30 +487,6 @@ public final class OptionProcessor extends AbstractProcessor {
}
}
- /**
- * Some flags wrap other flags. They are objectively useless, as there is no difference between
- * passing --wrapper=--foo and --foo other than the "source" information tracked. This
- * functionality comes from requiring compatibility at some past point in time, but is actively
- * being deprecated. No non-deprecated flag can use this feature.
- */
- private void checkWrapperOptions(VariableElement optionField) throws OptionProcessorException {
- Option annotation = optionField.getAnnotation(Option.class);
- if (annotation.wrapperOption()) {
- if (annotation.deprecationWarning().isEmpty()) {
- throw new OptionProcessorException(
- optionField,
- "Can't have non deprecated wrapper options, this feature is deprecated. "
- + "Please add a deprecationWarning.");
- }
- if (!ImmutableList.copyOf(annotation.metadataTags()).contains(OptionMetadataTag.DEPRECATED)) {
- throw new OptionProcessorException(
- optionField,
- "Can't have non deprecated wrapper options, this feature is deprecated. "
- + "Please add the metadata tag DEPRECATED.");
- }
- }
- }
-
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
for (Element annotatedElement : roundEnv.getElementsAnnotatedWith(Option.class)) {
@@ -528,7 +503,6 @@ public final class OptionProcessor extends AbstractProcessor {
checkConverter(optionField);
checkEffectTagRationality(optionField);
checkMetadataTagAndCategoryRationality(optionField);
- checkWrapperOptions(optionField);
} catch (OptionProcessorException e) {
error(e.getElementInError(), e.getMessage());
}
diff --git a/test/java/com/google/devtools/build/android/desugar/CoreLibrarySupportTest.java b/test/java/com/google/devtools/build/android/desugar/CoreLibrarySupportTest.java
new file mode 100644
index 0000000..9b43207
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/CoreLibrarySupportTest.java
@@ -0,0 +1,455 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.desugar;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.android.desugar.io.CoreLibraryRewriter;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.concurrent.ConcurrentMap;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.objectweb.asm.Opcodes;
+
+@RunWith(JUnit4.class)
+public class CoreLibrarySupportTest {
+
+ @Test
+ public void testIsRenamedCoreLibrary() throws Exception {
+ CoreLibrarySupport support =
+ new CoreLibrarySupport(
+ new CoreLibraryRewriter(""),
+ null,
+ ImmutableList.of("java/time/"),
+ ImmutableList.of(),
+ ImmutableList.of(),
+ ImmutableList.of());
+ assertThat(support.isRenamedCoreLibrary("java/time/X")).isTrue();
+ assertThat(support.isRenamedCoreLibrary("java/time/y/X")).isTrue();
+ assertThat(support.isRenamedCoreLibrary("java/io/X")).isFalse();
+ assertThat(support.isRenamedCoreLibrary("java/io/X$$CC")).isTrue();
+ assertThat(support.isRenamedCoreLibrary("java/io/X$$Lambda$17")).isTrue();
+ assertThat(support.isRenamedCoreLibrary("com/google/X")).isFalse();
+ }
+
+ @Test
+ public void testIsRenamedCoreLibrary_prefixedLoader() throws Exception {
+ CoreLibrarySupport support =
+ new CoreLibrarySupport(
+ new CoreLibraryRewriter("__/"),
+ null,
+ ImmutableList.of("java/time/"),
+ ImmutableList.of(),
+ ImmutableList.of(),
+ ImmutableList.of());
+ assertThat(support.isRenamedCoreLibrary("__/java/time/X")).isTrue();
+ assertThat(support.isRenamedCoreLibrary("__/java/time/y/X")).isTrue();
+ assertThat(support.isRenamedCoreLibrary("__/java/io/X")).isFalse();
+ assertThat(support.isRenamedCoreLibrary("__/java/io/X$$CC")).isTrue();
+ assertThat(support.isRenamedCoreLibrary("__/java/io/X$$Lambda$17")).isTrue();
+ assertThat(support.isRenamedCoreLibrary("com/google/X")).isFalse();
+ }
+
+ @Test
+ public void testRenameCoreLibrary() throws Exception {
+ CoreLibrarySupport support =
+ new CoreLibrarySupport(
+ new CoreLibraryRewriter(""),
+ null,
+ ImmutableList.of(),
+ ImmutableList.of(),
+ ImmutableList.of(),
+ ImmutableList.of());
+ assertThat(support.renameCoreLibrary("java/time/X")).isEqualTo("j$/time/X");
+ assertThat(support.renameCoreLibrary("com/google/X")).isEqualTo("com/google/X");
+ }
+
+ @Test
+ public void testRenameCoreLibrary_prefixedLoader() throws Exception {
+ CoreLibrarySupport support =
+ new CoreLibrarySupport(
+ new CoreLibraryRewriter("__/"),
+ null,
+ ImmutableList.of(),
+ ImmutableList.of(),
+ ImmutableList.of(),
+ ImmutableList.of());
+ assertThat(support.renameCoreLibrary("__/java/time/X")).isEqualTo("j$/time/X");
+ assertThat(support.renameCoreLibrary("com/google/X")).isEqualTo("com/google/X");
+ }
+
+ @Test
+ public void testMoveTarget() throws Exception {
+ CoreLibrarySupport support =
+ new CoreLibrarySupport(
+ new CoreLibraryRewriter("__/"),
+ null,
+ ImmutableList.of("java/util/Helper"),
+ ImmutableList.of(),
+ ImmutableList.of("java/util/Existing#match -> java/util/Helper"),
+ ImmutableList.of());
+ assertThat(support.getMoveTarget("__/java/util/Existing", "match")).isEqualTo("j$/util/Helper");
+ assertThat(support.getMoveTarget("java/util/Existing", "match")).isEqualTo("j$/util/Helper");
+ assertThat(support.getMoveTarget("__/java/util/Existing", "matchesnot")).isNull();
+ assertThat(support.getMoveTarget("__/java/util/ExistingOther", "match")).isNull();
+ }
+
+ @Test
+ public void testIsEmulatedCoreClassOrInterface() throws Exception {
+ CoreLibrarySupport support =
+ new CoreLibrarySupport(
+ new CoreLibraryRewriter(""),
+ Thread.currentThread().getContextClassLoader(),
+ ImmutableList.of("java/util/concurrent/"),
+ ImmutableList.of("java/util/Map"),
+ ImmutableList.of(),
+ ImmutableList.of());
+ assertThat(support.isEmulatedCoreClassOrInterface("java/util/Map")).isTrue();
+ assertThat(support.isEmulatedCoreClassOrInterface("java/util/Map$$Lambda$17")).isFalse();
+ assertThat(support.isEmulatedCoreClassOrInterface("java/util/Map$$CC")).isFalse();
+ assertThat(support.isEmulatedCoreClassOrInterface("java/util/HashMap")).isTrue();
+ assertThat(support.isEmulatedCoreClassOrInterface("java/util/concurrent/ConcurrentMap"))
+ .isFalse(); // false for renamed prefixes
+ assertThat(support.isEmulatedCoreClassOrInterface("com/google/Map")).isFalse();
+ }
+
+ @Test
+ public void testGetCoreInterfaceRewritingTarget_emulatedDefaultMethod() throws Exception {
+ CoreLibrarySupport support =
+ new CoreLibrarySupport(
+ new CoreLibraryRewriter(""),
+ Thread.currentThread().getContextClassLoader(),
+ ImmutableList.of(),
+ ImmutableList.of("java/util/Collection"),
+ ImmutableList.of(),
+ ImmutableList.of());
+ assertThat(
+ support.getCoreInterfaceRewritingTarget(
+ Opcodes.INVOKEINTERFACE,
+ "java/util/Collection",
+ "removeIf",
+ "(Ljava/util/function/Predicate;)Z",
+ true))
+ .isEqualTo(Collection.class);
+ assertThat(
+ support.getCoreInterfaceRewritingTarget(
+ Opcodes.INVOKEVIRTUAL,
+ "java/util/ArrayList",
+ "removeIf",
+ "(Ljava/util/function/Predicate;)Z",
+ false))
+ .isEqualTo(Collection.class);
+ assertThat(
+ support.getCoreInterfaceRewritingTarget(
+ Opcodes.INVOKEINTERFACE,
+ "com/google/HypotheticalListInterface",
+ "removeIf",
+ "(Ljava/util/function/Predicate;)Z",
+ true))
+ .isNull();
+ }
+
+ @Test
+ public void testGetCoreInterfaceRewritingTarget_emulatedImplementationMoved() throws Exception {
+ CoreLibrarySupport support =
+ new CoreLibrarySupport(
+ new CoreLibraryRewriter(""),
+ Thread.currentThread().getContextClassLoader(),
+ ImmutableList.of("java/util/Moved"),
+ ImmutableList.of("java/util/Map"),
+ ImmutableList.of("java/util/LinkedHashMap#forEach->java/util/Moved"),
+ ImmutableList.of());
+ assertThat(
+ support.getCoreInterfaceRewritingTarget(
+ Opcodes.INVOKEINTERFACE,
+ "java/util/Map",
+ "forEach",
+ "(Ljava/util/function/BiConsumer;)V",
+ true))
+ .isEqualTo(Map.class);
+ assertThat(
+ support.getCoreInterfaceRewritingTarget(
+ Opcodes.INVOKESPECIAL,
+ "java/util/Map",
+ "forEach",
+ "(Ljava/util/function/BiConsumer;)V",
+ true))
+ .isEqualTo(Map.class);
+ assertThat(
+ support.getCoreInterfaceRewritingTarget(
+ Opcodes.INVOKEVIRTUAL,
+ "java/util/LinkedHashMap",
+ "forEach",
+ "(Ljava/util/function/BiConsumer;)V",
+ false))
+ .isEqualTo(Map.class);
+ assertThat(
+ support.getCoreInterfaceRewritingTarget(
+ Opcodes.INVOKESPECIAL,
+ "java/util/LinkedHashMap",
+ "forEach",
+ "(Ljava/util/function/BiConsumer;)V",
+ false))
+ .isEqualTo(LinkedHashMap.class);
+ assertThat(
+ support.getCoreInterfaceRewritingTarget(
+ Opcodes.INVOKESPECIAL,
+ "java/util/HashMap",
+ "forEach",
+ "(Ljava/util/function/BiConsumer;)V",
+ false))
+ .isEqualTo(Map.class);
+ }
+
+ @Test
+ public void testGetCoreInterfaceRewritingTarget_abstractMethod() throws Exception {
+ CoreLibrarySupport support =
+ new CoreLibrarySupport(
+ new CoreLibraryRewriter(""),
+ Thread.currentThread().getContextClassLoader(),
+ ImmutableList.of(),
+ ImmutableList.of("java/util/Collection"),
+ ImmutableList.of(),
+ ImmutableList.of());
+ assertThat(
+ support.getCoreInterfaceRewritingTarget(
+ Opcodes.INVOKEINTERFACE,
+ "java/util/Collection",
+ "size",
+ "()I",
+ true))
+ .isNull();
+ assertThat(
+ support.getCoreInterfaceRewritingTarget(
+ Opcodes.INVOKEVIRTUAL,
+ "java/util/ArrayList",
+ "size",
+ "()I",
+ false))
+ .isNull();
+ }
+
+ @Test
+ public void testGetCoreInterfaceRewritingTarget_emulatedDefaultOverride() throws Exception {
+ CoreLibrarySupport support =
+ new CoreLibrarySupport(
+ new CoreLibraryRewriter(""),
+ Thread.currentThread().getContextClassLoader(),
+ ImmutableList.of(),
+ ImmutableList.of("java/util/Map"),
+ ImmutableList.of(),
+ ImmutableList.of());
+ assertThat(
+ support.getCoreInterfaceRewritingTarget(
+ Opcodes.INVOKEINTERFACE,
+ "java/util/Map",
+ "putIfAbsent",
+ "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;",
+ true))
+ .isEqualTo(Map.class);
+ assertThat(
+ support.getCoreInterfaceRewritingTarget(
+ Opcodes.INVOKEINTERFACE,
+ "java/util/concurrent/ConcurrentMap",
+ "putIfAbsent",
+ "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;",
+ true))
+ .isNull(); // putIfAbsent is default in Map but abstract in ConcurrentMap
+ assertThat(
+ support.getCoreInterfaceRewritingTarget(
+ Opcodes.INVOKEINTERFACE,
+ "java/util/concurrent/ConcurrentMap",
+ "getOrDefault",
+ "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;",
+ true))
+ .isEqualTo(ConcurrentMap.class); // conversely, getOrDefault is overridden as default method
+ }
+
+ @Test
+ public void testGetCoreInterfaceRewritingTarget_staticInterfaceMethod() throws Exception {
+ CoreLibrarySupport support =
+ new CoreLibrarySupport(
+ new CoreLibraryRewriter(""),
+ Thread.currentThread().getContextClassLoader(),
+ ImmutableList.of(),
+ ImmutableList.of("java/util/Comparator"),
+ ImmutableList.of(),
+ ImmutableList.of());
+ assertThat(
+ support.getCoreInterfaceRewritingTarget(
+ Opcodes.INVOKESTATIC,
+ "java/util/Comparator",
+ "reverseOrder",
+ "()Ljava/util/Comparator;",
+ true))
+ .isEqualTo(Comparator.class);
+ }
+
+ /**
+ * Tests that call sites of renamed core libraries are treated like call sites in regular
+ * {@link InterfaceDesugaring}.
+ */
+ @Test
+ public void testGetCoreInterfaceRewritingTarget_renamed() throws Exception {
+ CoreLibrarySupport support =
+ new CoreLibrarySupport(
+ new CoreLibraryRewriter(""),
+ Thread.currentThread().getContextClassLoader(),
+ ImmutableList.of("java/util/"),
+ ImmutableList.of(),
+ ImmutableList.of(),
+ ImmutableList.of());
+
+ // regular invocations of default methods: ignored
+ assertThat(
+ support.getCoreInterfaceRewritingTarget(
+ Opcodes.INVOKEINTERFACE,
+ "java/util/Collection",
+ "removeIf",
+ "(Ljava/util/function/Predicate;)Z",
+ true))
+ .isNull();
+ assertThat(
+ support.getCoreInterfaceRewritingTarget(
+ Opcodes.INVOKEVIRTUAL,
+ "java/util/ArrayList",
+ "removeIf",
+ "(Ljava/util/function/Predicate;)Z",
+ false))
+ .isNull();
+
+ // abstract methods: ignored
+ assertThat(
+ support.getCoreInterfaceRewritingTarget(
+ Opcodes.INVOKEINTERFACE,
+ "java/util/Collection",
+ "size",
+ "()I",
+ true))
+ .isNull();
+
+ // static interface method
+ assertThat(
+ support.getCoreInterfaceRewritingTarget(
+ Opcodes.INVOKESTATIC,
+ "java/util/Comparator",
+ "reverseOrder",
+ "()Ljava/util/Comparator;",
+ true))
+ .isEqualTo(Comparator.class);
+
+ // invokespecial for default methods: find nearest definition
+ assertThat(
+ support.getCoreInterfaceRewritingTarget(
+ Opcodes.INVOKESPECIAL,
+ "java/util/List",
+ "removeIf",
+ "(Ljava/util/function/Predicate;)Z",
+ true))
+ .isEqualTo(Collection.class);
+ // invokespecial on a class: ignore (even if there's an inherited default method)
+ assertThat(
+ support.getCoreInterfaceRewritingTarget(
+ Opcodes.INVOKESPECIAL,
+ "java/util/ArrayList",
+ "removeIf",
+ "(Ljava/util/function/Predicate;)Z",
+ false))
+ .isNull();
+ }
+
+ @Test
+ public void testGetCoreInterfaceRewritingTarget_ignoreRenamedInvokeInterface() throws Exception {
+ CoreLibrarySupport support =
+ new CoreLibrarySupport(
+ new CoreLibraryRewriter(""),
+ Thread.currentThread().getContextClassLoader(),
+ ImmutableList.of("java/util/concurrent/"), // should return null for these
+ ImmutableList.of("java/util/Map"),
+ ImmutableList.of(),
+ ImmutableList.of());
+ assertThat(
+ support.getCoreInterfaceRewritingTarget(
+ Opcodes.INVOKEINTERFACE,
+ "java/util/Map",
+ "getOrDefault",
+ "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;",
+ true))
+ .isEqualTo(Map.class);
+ assertThat(
+ support.getCoreInterfaceRewritingTarget(
+ Opcodes.INVOKEINTERFACE,
+ "java/util/concurrent/ConcurrentMap",
+ "getOrDefault",
+ "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;",
+ true))
+ .isNull();
+ }
+
+ @Test
+ public void testGetCoreInterfaceRewritingTarget_excludedMethodIgnored() throws Exception {
+ CoreLibrarySupport support =
+ new CoreLibrarySupport(
+ new CoreLibraryRewriter(""),
+ Thread.currentThread().getContextClassLoader(),
+ ImmutableList.of(),
+ ImmutableList.of("java/util/Collection"),
+ ImmutableList.of(),
+ ImmutableList.of("java/util/Collection#removeIf"));
+ assertThat(
+ support.getCoreInterfaceRewritingTarget(
+ Opcodes.INVOKEINTERFACE,
+ "java/util/List",
+ "removeIf",
+ "(Ljava/util/function/Predicate;)Z",
+ true))
+ .isNull();
+ assertThat(
+ support.getCoreInterfaceRewritingTarget(
+ Opcodes.INVOKEVIRTUAL,
+ "java/util/ArrayList",
+ "removeIf",
+ "(Ljava/util/function/Predicate;)Z",
+ false))
+ .isNull();
+ }
+
+ @Test
+ public void testEmulatedMethod_nullExceptions() throws Exception {
+ CoreLibrarySupport.EmulatedMethod m =
+ CoreLibrarySupport.EmulatedMethod.create(1, Number.class, "a", "()V", null);
+ assertThat(m.access()).isEqualTo(1);
+ assertThat(m.owner()).isEqualTo(Number.class);
+ assertThat(m.name()).isEqualTo("a");
+ assertThat(m.descriptor()).isEqualTo("()V");
+ assertThat(m.exceptions()).isEmpty();
+ }
+
+ @Test
+ public void testEmulatedMethod_givenExceptions() throws Exception {
+ CoreLibrarySupport.EmulatedMethod m =
+ CoreLibrarySupport.EmulatedMethod.create(
+ 1, Number.class, "a", "()V", new String[] {"b", "c"});
+ assertThat(m.access()).isEqualTo(1);
+ assertThat(m.owner()).isEqualTo(Number.class);
+ assertThat(m.name()).isEqualTo("a");
+ assertThat(m.descriptor()).isEqualTo("()V");
+ assertThat(m.exceptions()).containsExactly("b", "c").inOrder();
+ }
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/CorePackageRenamerTest.java b/test/java/com/google/devtools/build/android/desugar/CorePackageRenamerTest.java
new file mode 100644
index 0000000..5220ed6
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/CorePackageRenamerTest.java
@@ -0,0 +1,104 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.desugar;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.android.desugar.io.CoreLibraryRewriter;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+
+@RunWith(JUnit4.class)
+public class CorePackageRenamerTest {
+
+ @Test
+ public void testSymbolRewrite() throws Exception {
+ MockClassVisitor out = new MockClassVisitor();
+ CorePackageRenamer renamer =
+ new CorePackageRenamer(
+ out,
+ new CoreLibrarySupport(
+ new CoreLibraryRewriter(""),
+ null,
+ ImmutableList.of("java/time/"),
+ ImmutableList.of(),
+ ImmutableList.of("java/util/A#m->java/time/B"),
+ ImmutableList.of()));
+ MethodVisitor mv = renamer.visitMethod(0, "test", "()V", null, null);
+
+ mv.visitMethodInsn(
+ Opcodes.INVOKESTATIC, "java/time/Instant", "now", "()Ljava/time/Instant;", false);
+ assertThat(out.mv.owner).isEqualTo("j$/time/Instant");
+ assertThat(out.mv.desc).isEqualTo("()Lj$/time/Instant;");
+
+ // Ignore moved methods but not their descriptors
+ mv.visitMethodInsn(
+ Opcodes.INVOKESTATIC, "java/util/A", "m", "()Ljava/time/Instant;", false);
+ assertThat(out.mv.owner).isEqualTo("java/util/A");
+ assertThat(out.mv.desc).isEqualTo("()Lj$/time/Instant;");
+
+ // Ignore arbitrary other methods but not their descriptors
+ mv.visitMethodInsn(
+ Opcodes.INVOKESTATIC, "other/time/Instant", "now", "()Ljava/time/Instant;", false);
+ assertThat(out.mv.owner).isEqualTo("other/time/Instant");
+ assertThat(out.mv.desc).isEqualTo("()Lj$/time/Instant;");
+
+ mv.visitFieldInsn(
+ Opcodes.GETFIELD, "other/time/Instant", "now", "Ljava/time/Instant;");
+ assertThat(out.mv.owner).isEqualTo("other/time/Instant");
+ assertThat(out.mv.desc).isEqualTo("Lj$/time/Instant;");
+ }
+
+ private static class MockClassVisitor extends ClassVisitor {
+
+ final MockMethodVisitor mv = new MockMethodVisitor();
+
+ public MockClassVisitor() {
+ super(Opcodes.ASM6);
+ }
+
+ @Override
+ public MethodVisitor visitMethod(
+ int access, String name, String desc, String signature, String[] exceptions) {
+ return mv;
+ }
+ }
+
+ private static class MockMethodVisitor extends MethodVisitor {
+
+ String owner;
+ String desc;
+
+ public MockMethodVisitor() {
+ super(Opcodes.ASM6);
+ }
+
+ @Override
+ public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
+ this.owner = owner;
+ this.desc = desc;
+ }
+
+ @Override
+ public void visitFieldInsn(int opcode, String owner, String name, String desc) {
+ this.owner = owner;
+ this.desc = desc;
+ }
+ }
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixerTest.java b/test/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixerTest.java
index c74febb..406a36f 100644
--- a/test/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixerTest.java
+++ b/test/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixerTest.java
@@ -15,11 +15,14 @@ package com.google.devtools.build.android.desugar;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.truth.Truth.assertThat;
-import static com.google.devtools.build.android.desugar.DefaultMethodClassFixer.InterfaceComparator.INSTANCE;
+import static com.google.devtools.build.android.desugar.DefaultMethodClassFixer.SubtypeComparator.INSTANCE;
import com.google.common.collect.ImmutableList;
import com.google.common.io.Closer;
-import com.google.devtools.build.android.desugar.Desugar.ThrowingClassLoader;
+import com.google.devtools.build.android.desugar.io.CoreLibraryRewriter;
+import com.google.devtools.build.android.desugar.io.HeaderClassLoader;
+import com.google.devtools.build.android.desugar.io.IndexedInputs;
+import com.google.devtools.build.android.desugar.io.ThrowingClassLoader;
import java.io.File;
import java.io.IOException;
import java.io.Serializable;
@@ -102,6 +105,7 @@ public class DefaultMethodClassFixerTest {
writer,
classpathReader,
DependencyCollector.NoWriteCollectors.FAIL_ON_MISSING,
+ /*coreLibrarySupport=*/ null,
bootclassPath,
classLoader);
reader.accept(fixer, 0);
diff --git a/test/java/com/google/devtools/build/android/desugar/DesugarJava8FunctionalTest.java b/test/java/com/google/devtools/build/android/desugar/DesugarJava8FunctionalTest.java
index 20e6028..75d4f43 100644
--- a/test/java/com/google/devtools/build/android/desugar/DesugarJava8FunctionalTest.java
+++ b/test/java/com/google/devtools/build/android/desugar/DesugarJava8FunctionalTest.java
@@ -32,6 +32,7 @@ import com.google.devtools.build.android.desugar.testdata.java8.GenericDefaultIn
import com.google.devtools.build.android.desugar.testdata.java8.InterfaceMethod;
import com.google.devtools.build.android.desugar.testdata.java8.InterfaceWithDefaultMethod;
import com.google.devtools.build.android.desugar.testdata.java8.InterfaceWithDuplicateMethods.ClassWithDuplicateMethods;
+import com.google.devtools.build.android.desugar.testdata.java8.InterfaceWithInheritedMethods;
import com.google.devtools.build.android.desugar.testdata.java8.Java7InterfaceWithBridges;
import com.google.devtools.build.android.desugar.testdata.java8.Named;
import com.google.devtools.build.android.desugar.testdata.java8.TwoInheritedDefaultMethods;
@@ -415,4 +416,11 @@ public class DesugarJava8FunctionalTest extends DesugarFunctionalTest {
assertThat(new DefaultMethodTransitivelyFromSeparateJava8Target().dflt()).isEqualTo("dflt");
assertThat(new DefaultMethodFromSeparateJava8TargetOverridden().dflt()).isEqualTo("override");
}
+
+ /** Regression test for b/73355452 */
+ @Test
+ public void testSuperCallToInheritedDefaultMethod() {
+ assertThat(new InterfaceWithInheritedMethods.Impl().name()).isEqualTo("Base");
+ assertThat(new InterfaceWithInheritedMethods.Impl().suffix()).isEqualTo("!");
+ }
}
diff --git a/test/java/com/google/devtools/build/android/desugar/DesugarTryWithResourcesFunctionalTest.java b/test/java/com/google/devtools/build/android/desugar/DesugarTryWithResourcesFunctionalTest.java
index 38df9e3..2d567d3 100644
--- a/test/java/com/google/devtools/build/android/desugar/DesugarTryWithResourcesFunctionalTest.java
+++ b/test/java/com/google/devtools/build/android/desugar/DesugarTryWithResourcesFunctionalTest.java
@@ -96,4 +96,34 @@ public class DesugarTryWithResourcesFunctionalTest {
}
}
}
+
+ @Test
+ public void testInheritanceTryWithResources() {
+
+ try {
+ ClassUsingTryWithResources.inheritanceTryWithResources();
+ fail("Expected RuntimeException");
+ } catch (Exception expected) {
+ assertThat(expected.getClass()).isEqualTo(RuntimeException.class);
+
+ String expectedStrategyName = getTwrStrategyClassNameSpecifiedInSystemProperty();
+ assertThat(getStrategyClassName()).isEqualTo(expectedStrategyName);
+ if (isMimicStrategy()) {
+ assertThat(expected.getSuppressed()).isEmpty();
+ assertThat(ThrowableExtension.getSuppressed(expected)).hasLength(1);
+ assertThat(ThrowableExtension.getSuppressed(expected)[0].getClass())
+ .isEqualTo(IOException.class);
+ } else if (isReuseStrategy()) {
+ assertThat(expected.getSuppressed()).hasLength(1);
+ assertThat(expected.getSuppressed()[0].getClass()).isEqualTo(IOException.class);
+ assertThat(ThrowableExtension.getSuppressed(expected)[0].getClass())
+ .isEqualTo(IOException.class);
+ } else if (isNullStrategy()) {
+ assertThat(expected.getSuppressed()).isEmpty();
+ assertThat(ThrowableExtension.getSuppressed(expected)).isEmpty();
+ } else {
+ fail("unexpected desugaring strategy " + getStrategyClassName());
+ }
+ }
+ }
}
diff --git a/test/java/com/google/devtools/build/android/desugar/Java7CompatibilityTest.java b/test/java/com/google/devtools/build/android/desugar/Java7CompatibilityTest.java
index 2eab943..99e51c1 100644
--- a/test/java/com/google/devtools/build/android/desugar/Java7CompatibilityTest.java
+++ b/test/java/com/google/devtools/build/android/desugar/Java7CompatibilityTest.java
@@ -16,6 +16,7 @@ package com.google.devtools.build.android.desugar;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
+import com.google.devtools.build.android.desugar.io.BitFlags;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
diff --git a/test/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriterTest.java b/test/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriterTest.java
index 37afae7..dc0da22 100644
--- a/test/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriterTest.java
+++ b/test/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriterTest.java
@@ -25,6 +25,7 @@ import static org.objectweb.asm.Opcodes.ASM5;
import static org.objectweb.asm.Opcodes.INVOKESTATIC;
import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL;
+import com.google.devtools.build.android.desugar.io.BitFlags;
import com.google.devtools.build.android.desugar.runtime.ThrowableExtension;
import com.google.devtools.build.android.desugar.testdata.ClassUsingTryWithResources;
import java.io.IOException;
diff --git a/test/java/com/google/devtools/build/android/desugar/b72690624_testdata.jar b/test/java/com/google/devtools/build/android/desugar/b72690624_testdata.jar
new file mode 100644
index 0000000..6cca3a0
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/b72690624_testdata.jar
Binary files differ
diff --git a/test/java/com/google/devtools/build/android/desugar/dependencies/MetadataCollectorTest.java b/test/java/com/google/devtools/build/android/desugar/dependencies/MetadataCollectorTest.java
index e99abc4..64dd871 100644
--- a/test/java/com/google/devtools/build/android/desugar/dependencies/MetadataCollectorTest.java
+++ b/test/java/com/google/devtools/build/android/desugar/dependencies/MetadataCollectorTest.java
@@ -16,11 +16,11 @@ package com.google.devtools.build.android.desugar.dependencies;
import static com.google.common.truth.Truth.assertThat;
import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.android.desugar.proto.DesugarDeps;
import com.google.devtools.build.android.desugar.proto.DesugarDeps.Dependency;
import com.google.devtools.build.android.desugar.proto.DesugarDeps.DesugarDepsInfo;
import com.google.devtools.build.android.desugar.proto.DesugarDeps.InterfaceDetails;
import com.google.devtools.build.android.desugar.proto.DesugarDeps.InterfaceWithCompanion;
-import com.google.devtools.build.android.desugar.proto.DesugarDeps.Type;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@@ -103,8 +103,8 @@ public class MetadataCollectorTest {
.build());
}
- private static Type wrapType(String name) {
- return Type.newBuilder().setBinaryName(name).build();
+ private static DesugarDeps.Type wrapType(String name) {
+ return DesugarDeps.Type.newBuilder().setBinaryName(name).build();
}
private DesugarDepsInfo extractProto(MetadataCollector collector) throws Exception {
diff --git a/test/java/com/google/devtools/build/android/desugar/desugar_unused_synthetic_close_resource_method_golden.txt b/test/java/com/google/devtools/build/android/desugar/desugar_unused_synthetic_close_resource_method_golden.txt
new file mode 100644
index 0000000..4be60b6
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/desugar_unused_synthetic_close_resource_method_golden.txt
@@ -0,0 +1,8 @@
+Compiled from "UnusedSyntheticCloseResourceMethod.java"
+public class com.google.devtools.build.android.desugar.testdata.UnusedSyntheticCloseResourceMethod {
+ public com.google.devtools.build.android.desugar.testdata.UnusedSyntheticCloseResourceMethod();
+ Code:
+ 0: aload_0
+ 1: invokespecial #9 // Method java/lang/Object."<init>":()V
+ 4: return
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/FieldInfoTest.java b/test/java/com/google/devtools/build/android/desugar/io/FieldInfoTest.java
index afb2bea..0579822 100644
--- a/test/java/com/google/devtools/build/android/desugar/FieldInfoTest.java
+++ b/test/java/com/google/devtools/build/android/desugar/io/FieldInfoTest.java
@@ -11,7 +11,7 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.devtools.build.android.desugar;
+package com.google.devtools.build.android.desugar.io;
import static com.google.common.truth.Truth.assertThat;
diff --git a/test/java/com/google/devtools/build/android/desugar/IndexedInputsTest.java b/test/java/com/google/devtools/build/android/desugar/io/IndexedInputsTest.java
index bac3fc9..81a4b31 100644
--- a/test/java/com/google/devtools/build/android/desugar/IndexedInputsTest.java
+++ b/test/java/com/google/devtools/build/android/desugar/io/IndexedInputsTest.java
@@ -11,7 +11,7 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.devtools.build.android.desugar;
+package com.google.devtools.build.android.desugar.io;
import static com.google.common.truth.Truth.assertThat;
diff --git a/test/java/com/google/devtools/build/android/desugar/runtime/ThrowableExtensionTestUtility.java b/test/java/com/google/devtools/build/android/desugar/runtime/ThrowableExtensionTestUtility.java
index b65b8bd..489bd7a 100644
--- a/test/java/com/google/devtools/build/android/desugar/runtime/ThrowableExtensionTestUtility.java
+++ b/test/java/com/google/devtools/build/android/desugar/runtime/ThrowableExtensionTestUtility.java
@@ -27,7 +27,7 @@ public class ThrowableExtensionTestUtility {
private static final String SYSTEM_PROPERTY_EXPECTED_STRATEGY = "expected.strategy";
public static String getTwrStrategyClassNameSpecifiedInSystemProperty() {
- String className = System.getProperty(SYSTEM_PROPERTY_EXPECTED_STRATEGY);
+ String className = unquote(System.getProperty(SYSTEM_PROPERTY_EXPECTED_STRATEGY));
assertThat(className).isNotEmpty();
return className;
}
@@ -61,4 +61,13 @@ public class ThrowableExtensionTestUtility {
public static boolean isReuseStrategy() {
return isStrategyOfClass(THROWABLE_EXTENSION_CLASS_NAME + "$ReuseDesugaringStrategy");
}
+
+ private static String unquote(String s) {
+ if (s.startsWith("'") || s.startsWith("\"")) {
+ assertThat(s).endsWith(s.substring(0, 1));
+ return s.substring(1, s.length() - 1);
+ } else {
+ return s;
+ }
+ }
}
diff --git a/test/java/com/google/devtools/build/android/desugar/scan/test_keep_scanner.sh b/test/java/com/google/devtools/build/android/desugar/scan/test_keep_scanner.sh
new file mode 100755
index 0000000..d42859f
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/scan/test_keep_scanner.sh
@@ -0,0 +1,27 @@
+#!/bin/bash
+#
+# Copyright 2018 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+set -eux
+
+out=$(mktemp)
+"$1" --input "$2" --keep_file "${out}" --prefix java/
+
+if ! diff "$3" "${out}"; then
+ echo "Unexpected output"
+ cat "${out}"
+ rm "${out}"
+ exit 1
+fi
+rm "${out}"
diff --git a/test/java/com/google/devtools/build/android/desugar/scan/testdata/CollectionReferences.java b/test/java/com/google/devtools/build/android/desugar/scan/testdata/CollectionReferences.java
new file mode 100644
index 0000000..830364c
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/scan/testdata/CollectionReferences.java
@@ -0,0 +1,64 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.desugar.scan.testdata;
+
+import java.util.AbstractList;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.LinkedList;
+import java.util.List;
+
+/** Test data for {@code KeepScanner} with references to java.* */
+public class CollectionReferences {
+
+ private final List<Date> dates;
+
+ public CollectionReferences() {
+ dates = new ArrayList<>(7);
+ assert !(dates instanceof LinkedList);
+ }
+
+ @SuppressWarnings("unchecked")
+ public void add(Date date) {
+ List<Date> l = (AbstractList<Date>) Collection.class.cast(dates);
+ l.add(date);
+ }
+
+ public Date first() {
+ try {
+ return dates.get(0);
+ } catch (IndexOutOfBoundsException e) {
+ return null;
+ }
+ }
+
+ public long min() {
+ long result = Long.MAX_VALUE; // compile-time constant, no ref
+ for (Date d : dates) {
+ if (d.getTime() < result) {
+ result = d.getTime();
+ }
+ }
+ return result;
+ }
+
+ public void expire(long before) {
+ dates.removeIf(d -> d.getTime() < before);
+ }
+
+ static {
+ System.out.println("Hello!");
+ }
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/scan/testdata/OverlappingCollectionReferences.java b/test/java/com/google/devtools/build/android/desugar/scan/testdata/OverlappingCollectionReferences.java
new file mode 100644
index 0000000..e743a0d
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/scan/testdata/OverlappingCollectionReferences.java
@@ -0,0 +1,49 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.desugar.scan.testdata;
+
+import java.util.ArrayList;
+import java.util.Date;
+
+/** Supplements {@link CollectionReferences} with additional and overlapping references to java.* */
+public class OverlappingCollectionReferences {
+
+ private final ArrayList<Date> dates;
+
+ public OverlappingCollectionReferences() {
+ dates = new ArrayList<>();
+ }
+
+ public void add(Date date) {
+ dates.add(date);
+ }
+
+ public Date first() {
+ try {
+ return dates.get(0);
+ } catch (IndexOutOfBoundsException e) {
+ return null;
+ }
+ }
+
+ public Date max() {
+ long result = Long.MIN_VALUE; // compile-time constant, no ref
+ for (Date d : dates) {
+ if (d.getTime() > result) {
+ result = d.getTime();
+ }
+ }
+ return new Date(result);
+ }
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/scan/testdata_golden.txt b/test/java/com/google/devtools/build/android/desugar/scan/testdata_golden.txt
new file mode 100644
index 0000000..6082576
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/scan/testdata_golden.txt
@@ -0,0 +1,62 @@
+-keep class java.io.PrintStream {
+ *** println(java.lang.String);
+}
+-keep class java.lang.AssertionError {
+ <init>();
+}
+-keep class java.lang.Class {
+ *** cast(java.lang.Object);
+ *** desiredAssertionStatus();
+}
+-keep class java.lang.IndexOutOfBoundsException {
+}
+-keep class java.lang.Object {
+ <init>();
+}
+-keep class java.lang.String {
+}
+-keep class java.lang.System {
+ *** out;
+}
+-keep class java.lang.invoke.CallSite {
+}
+-keep class java.lang.invoke.LambdaMetafactory {
+ *** metafactory(java.lang.invoke.MethodHandles$Lookup, java.lang.String, java.lang.invoke.MethodType, java.lang.invoke.MethodType, java.lang.invoke.MethodHandle, java.lang.invoke.MethodType);
+}
+-keep class java.lang.invoke.MethodHandle {
+}
+-keep class java.lang.invoke.MethodHandles {
+}
+-keep class java.lang.invoke.MethodHandles$Lookup {
+}
+-keep class java.lang.invoke.MethodType {
+}
+-keep class java.util.AbstractList {
+}
+-keep class java.util.ArrayList {
+ <init>();
+ <init>(int);
+ *** add(java.lang.Object);
+ *** get(int);
+ *** iterator();
+}
+-keep class java.util.Collection {
+ *** removeIf(java.util.function.Predicate);
+}
+-keep class java.util.Date {
+ <init>(long);
+ *** getTime();
+}
+-keep class java.util.Iterator {
+ *** hasNext();
+ *** next();
+}
+-keep class java.util.LinkedList {
+}
+-keep class java.util.List {
+ *** add(java.lang.Object);
+ *** get(int);
+ *** iterator();
+}
+-keep class java.util.function.Predicate {
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/testdata/ClassUsingTryWithResources.java b/test/java/com/google/devtools/build/android/desugar/testdata/ClassUsingTryWithResources.java
index c340c84..e4f7f18 100644
--- a/test/java/com/google/devtools/build/android/desugar/testdata/ClassUsingTryWithResources.java
+++ b/test/java/com/google/devtools/build/android/desugar/testdata/ClassUsingTryWithResources.java
@@ -49,6 +49,9 @@ public class ClassUsingTryWithResources {
}
}
+ /** A resource inheriting the close() method from its parent. */
+ public static class InheritanceResource extends SimpleResource {}
+
/** This method will always throw {@link java.lang.Exception}. */
public static void simpleTryWithResources() throws Exception {
// Throwable.addSuppressed(Throwable) should be called in the following block.
@@ -57,6 +60,17 @@ public class ClassUsingTryWithResources {
}
}
+ /**
+ * This method useds {@link InheritanceResource}, which inherits all methods from {@link
+ * SimpleResource}.
+ */
+ public static void inheritanceTryWithResources() throws Exception {
+ // Throwable.addSuppressed(Throwable) should be called in the following block.
+ try (InheritanceResource resource = new InheritanceResource()) {
+ resource.call(true);
+ }
+ }
+
public static Throwable[] checkSuppressedExceptions(boolean throwException) {
// Throwable.addSuppressed(Throwable) should be called in the following block.
try (SimpleResource resource = new SimpleResource()) {
diff --git a/test/java/com/google/devtools/build/android/desugar/testdata/java8/InterfaceWithInheritedMethods.java b/test/java/com/google/devtools/build/android/desugar/testdata/java8/InterfaceWithInheritedMethods.java
new file mode 100644
index 0000000..8656e26
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/testdata/java8/InterfaceWithInheritedMethods.java
@@ -0,0 +1,44 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.desugar.testdata.java8;
+
+/** Regression test data for b/73355452 that also includes calling static methods. */
+public interface InterfaceWithInheritedMethods {
+ default String name() {
+ return "Base";
+ }
+
+ static String staticSuffix() {
+ return "!";
+ }
+
+ static interface Passthrough extends InterfaceWithInheritedMethods {
+ // inherits name(). Note that desugar doesn't produce a companion class for this interface
+ // since it doesn't define any default or static interface methods itself.
+ }
+
+ static class Impl implements Passthrough {
+ @Override
+ public String name() {
+ // Even though Passthrough itself doesn't define name(), bytecode refers to Passthrough.name.
+ return Passthrough.super.name();
+ }
+
+ public String suffix() {
+ // Note that Passthrough.defaultSuffix doesn't compile and bytecode refers to
+ // InterfaceWithInheritedMethods.staticSuffix, so this shouldn't cause issues like b/73355452
+ return staticSuffix();
+ }
+ }
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/testdata_desugared_for_disabling_twr_with_large_minsdkversion_jar_toc_golden.txt b/test/java/com/google/devtools/build/android/desugar/testdata_desugared_for_disabling_twr_with_large_minsdkversion_jar_toc_golden.txt
index b7c3c25..8e396f5 100644
--- a/test/java/com/google/devtools/build/android/desugar/testdata_desugared_for_disabling_twr_with_large_minsdkversion_jar_toc_golden.txt
+++ b/test/java/com/google/devtools/build/android/desugar/testdata_desugared_for_disabling_twr_with_large_minsdkversion_jar_toc_golden.txt
@@ -12,6 +12,7 @@ com/google/devtools/build/android/desugar/testdata/CaptureLambda.class
com/google/devtools/build/android/desugar/testdata/ClassCallingLongCompare$LongCmpFunc.class
com/google/devtools/build/android/desugar/testdata/ClassCallingLongCompare.class
com/google/devtools/build/android/desugar/testdata/ClassCallingRequireNonNull.class
+com/google/devtools/build/android/desugar/testdata/ClassUsingTryWithResources$InheritanceResource.class
com/google/devtools/build/android/desugar/testdata/ClassUsingTryWithResources$SimpleResource.class
com/google/devtools/build/android/desugar/testdata/ClassUsingTryWithResources.class
com/google/devtools/build/android/desugar/testdata/ConcreteFunction$Parser.class
diff --git a/test/java/com/google/devtools/build/android/desugar/testdata_desugared_for_try_with_resources_jar_toc_golden.txt b/test/java/com/google/devtools/build/android/desugar/testdata_desugared_for_try_with_resources_jar_toc_golden.txt
index d03b121..41e8d1c 100644
--- a/test/java/com/google/devtools/build/android/desugar/testdata_desugared_for_try_with_resources_jar_toc_golden.txt
+++ b/test/java/com/google/devtools/build/android/desugar/testdata_desugared_for_try_with_resources_jar_toc_golden.txt
@@ -12,6 +12,7 @@ com/google/devtools/build/android/desugar/testdata/CaptureLambda.class
com/google/devtools/build/android/desugar/testdata/ClassCallingLongCompare$LongCmpFunc.class
com/google/devtools/build/android/desugar/testdata/ClassCallingLongCompare.class
com/google/devtools/build/android/desugar/testdata/ClassCallingRequireNonNull.class
+com/google/devtools/build/android/desugar/testdata/ClassUsingTryWithResources$InheritanceResource.class
com/google/devtools/build/android/desugar/testdata/ClassUsingTryWithResources$SimpleResource.class
com/google/devtools/build/android/desugar/testdata/ClassUsingTryWithResources.class
com/google/devtools/build/android/desugar/testdata/ConcreteFunction$Parser.class
diff --git a/test/java/com/google/devtools/build/android/desugar/testdata_desugared_jar_toc_golden.txt b/test/java/com/google/devtools/build/android/desugar/testdata_desugared_jar_toc_golden.txt
index 91fc415..b79961e 100644
--- a/test/java/com/google/devtools/build/android/desugar/testdata_desugared_jar_toc_golden.txt
+++ b/test/java/com/google/devtools/build/android/desugar/testdata_desugared_jar_toc_golden.txt
@@ -12,6 +12,7 @@ com/google/devtools/build/android/desugar/testdata/CaptureLambda.class
com/google/devtools/build/android/desugar/testdata/ClassCallingLongCompare$LongCmpFunc.class
com/google/devtools/build/android/desugar/testdata/ClassCallingLongCompare.class
com/google/devtools/build/android/desugar/testdata/ClassCallingRequireNonNull.class
+com/google/devtools/build/android/desugar/testdata/ClassUsingTryWithResources$InheritanceResource.class
com/google/devtools/build/android/desugar/testdata/ClassUsingTryWithResources$SimpleResource.class
com/google/devtools/build/android/desugar/testdata/ClassUsingTryWithResources.class
com/google/devtools/build/android/desugar/testdata/ConcreteFunction$Parser.class
diff --git a/test/java/com/google/devtools/build/android/desugar/testdata_desugared_java8_jar_toc_golden.txt b/test/java/com/google/devtools/build/android/desugar/testdata_desugared_java8_jar_toc_golden.txt
index 907edd0..16439ae 100644
--- a/test/java/com/google/devtools/build/android/desugar/testdata_desugared_java8_jar_toc_golden.txt
+++ b/test/java/com/google/devtools/build/android/desugar/testdata_desugared_java8_jar_toc_golden.txt
@@ -12,6 +12,7 @@ com/google/devtools/build/android/desugar/testdata/CaptureLambda.class
com/google/devtools/build/android/desugar/testdata/ClassCallingLongCompare$LongCmpFunc.class
com/google/devtools/build/android/desugar/testdata/ClassCallingLongCompare.class
com/google/devtools/build/android/desugar/testdata/ClassCallingRequireNonNull.class
+com/google/devtools/build/android/desugar/testdata/ClassUsingTryWithResources$InheritanceResource.class
com/google/devtools/build/android/desugar/testdata/ClassUsingTryWithResources$SimpleResource.class
com/google/devtools/build/android/desugar/testdata/ClassUsingTryWithResources.class
com/google/devtools/build/android/desugar/testdata/ConcreteFunction$Parser.class
@@ -81,6 +82,9 @@ com/google/devtools/build/android/desugar/testdata/java8/InterfaceWithDefaultMet
com/google/devtools/build/android/desugar/testdata/java8/InterfaceWithDefaultMethod.class
com/google/devtools/build/android/desugar/testdata/java8/InterfaceWithDuplicateMethods$ClassWithDuplicateMethods.class
com/google/devtools/build/android/desugar/testdata/java8/InterfaceWithDuplicateMethods.class
+com/google/devtools/build/android/desugar/testdata/java8/InterfaceWithInheritedMethods$Impl.class
+com/google/devtools/build/android/desugar/testdata/java8/InterfaceWithInheritedMethods$Passthrough.class
+com/google/devtools/build/android/desugar/testdata/java8/InterfaceWithInheritedMethods.class
com/google/devtools/build/android/desugar/testdata/java8/Java7InterfaceWithBridges$AbstractClassOne.class
com/google/devtools/build/android/desugar/testdata/java8/Java7InterfaceWithBridges$ClassAddOne.class
com/google/devtools/build/android/desugar/testdata/java8/Java7InterfaceWithBridges$ClassAddTwo.class
diff --git a/test/java/com/google/devtools/build/android/desugar/testdata_desugared_without_lambda_desugared_jar_toc_golden.txt b/test/java/com/google/devtools/build/android/desugar/testdata_desugared_without_lambda_desugared_jar_toc_golden.txt
index 256760b..4fdc023 100644
--- a/test/java/com/google/devtools/build/android/desugar/testdata_desugared_without_lambda_desugared_jar_toc_golden.txt
+++ b/test/java/com/google/devtools/build/android/desugar/testdata_desugared_without_lambda_desugared_jar_toc_golden.txt
@@ -11,6 +11,7 @@ com/google/devtools/build/android/desugar/testdata/CaptureLambda.class
com/google/devtools/build/android/desugar/testdata/ClassCallingLongCompare$LongCmpFunc.class
com/google/devtools/build/android/desugar/testdata/ClassCallingLongCompare.class
com/google/devtools/build/android/desugar/testdata/ClassCallingRequireNonNull.class
+com/google/devtools/build/android/desugar/testdata/ClassUsingTryWithResources$InheritanceResource.class
com/google/devtools/build/android/desugar/testdata/ClassUsingTryWithResources$SimpleResource.class
com/google/devtools/build/android/desugar/testdata/ClassUsingTryWithResources.class
com/google/devtools/build/android/desugar/testdata/ConcreteFunction$Parser.class
diff --git a/test/java/com/google/devtools/build/android/desugar/unused_closed_resource.jar b/test/java/com/google/devtools/build/android/desugar/unused_closed_resource.jar
new file mode 100644
index 0000000..6a451eb
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/unused_closed_resource.jar
Binary files differ