summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIvan Gavrilovic <gavra@google.com>2017-09-25 20:18:36 +0000
committerandroid-build-merger <android-build-merger@google.com>2017-09-25 20:18:36 +0000
commit34477e8629b7244fbf9da92845834380606c8e1a (patch)
treed7601367dff9c70fb41ff4f76a5064173374b0c4
parent550655cfbe384de9956137538b2b59c2e3208797 (diff)
parent3a238ab3ddcd05bfd6735ba02317476cfc46829e (diff)
downloaddesugar-34477e8629b7244fbf9da92845834380606c8e1a.tar.gz
Merge remote-tracking branch 'aosp/upstream-master' am: 14905bc683
am: 3a238ab3dd Change-Id: I42c5bc3246a1d22ac12ba53425aada744a1544d4
-rw-r--r--java/com/google/devtools/build/android/desugar/Desugar.java19
-rw-r--r--java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java51
-rw-r--r--java/com/google/devtools/build/android/desugar/Java7Compatibility.java1
-rw-r--r--java/com/google/devtools/build/android/desugar/TryWithResourcesRewriter.java57
-rw-r--r--java/com/google/devtools/build/android/desugar/runtime/ThrowableExtension.java48
-rw-r--r--java/com/google/devtools/common/options/Converters.java28
-rw-r--r--java/com/google/devtools/common/options/DuplicateOptionDeclarationException.java2
-rw-r--r--java/com/google/devtools/common/options/InvocationPolicyEnforcer.java293
-rw-r--r--java/com/google/devtools/common/options/IsolatedOptionsData.java294
-rw-r--r--java/com/google/devtools/common/options/LegacyParamsFilePreProcessor.java147
-rw-r--r--java/com/google/devtools/common/options/Option.java9
-rw-r--r--java/com/google/devtools/common/options/OptionDefinition.java69
-rw-r--r--java/com/google/devtools/common/options/OptionDocumentationCategory.java7
-rw-r--r--java/com/google/devtools/common/options/OptionInstanceOrigin.java57
-rw-r--r--java/com/google/devtools/common/options/OptionValueDescription.java372
-rw-r--r--java/com/google/devtools/common/options/OptionsData.java11
-rw-r--r--java/com/google/devtools/common/options/OptionsParser.java341
-rw-r--r--java/com/google/devtools/common/options/OptionsParserImpl.java549
-rw-r--r--java/com/google/devtools/common/options/OptionsProvider.java27
-rw-r--r--java/com/google/devtools/common/options/OptionsUsage.java8
-rw-r--r--java/com/google/devtools/common/options/ParamsFilePreProcessor.java127
-rw-r--r--java/com/google/devtools/common/options/ParsedOptionDescription.java120
-rw-r--r--java/com/google/devtools/common/options/ShellQuotedParamsFilePreProcessor.java138
-rw-r--r--java/com/google/devtools/common/options/UnquotedParamsFilePreProcessor.java40
-rw-r--r--java/com/google/devtools/common/options/processor/OptionProcessor.java375
-rw-r--r--java/com/google/devtools/common/options/processor/OptionProcessorException.java35
-rw-r--r--java/com/google/devtools/common/options/processor/ProcessorUtils.java98
-rw-r--r--test/java/com/google/devtools/build/android/desugar/Bug62456849TestDataGenerator.java104
-rw-r--r--test/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixerTest.java234
-rw-r--r--test/java/com/google/devtools/build/android/desugar/DesugarCoreLibraryFunctionalTest.java34
-rw-r--r--test/java/com/google/devtools/build/android/desugar/DesugarDefaultMethodsFunctionalTest.java29
-rw-r--r--test/java/com/google/devtools/build/android/desugar/DesugarFunctionalTest.java339
-rw-r--r--test/java/com/google/devtools/build/android/desugar/DesugarJava8FunctionalTest.java397
-rw-r--r--test/java/com/google/devtools/build/android/desugar/DesugarJava8LikeAndroidStudioFunctionalTest.java31
-rw-r--r--test/java/com/google/devtools/build/android/desugar/DesugarLongCompareTest.java135
-rw-r--r--test/java/com/google/devtools/build/android/desugar/DesugarMainClassTest.java82
-rw-r--r--test/java/com/google/devtools/build/android/desugar/DesugarObjectsRequireNonNullTest.java153
-rw-r--r--test/java/com/google/devtools/build/android/desugar/DesugarTryWithResourcesFunctionalTest.java99
-rw-r--r--test/java/com/google/devtools/build/android/desugar/FieldInfoTest.java33
-rw-r--r--test/java/com/google/devtools/build/android/desugar/IndexedInputsTest.java136
-rw-r--r--test/java/com/google/devtools/build/android/desugar/Java7CompatibilityTest.java122
-rw-r--r--test/java/com/google/devtools/build/android/desugar/MethodInfoTest.java33
-rw-r--r--test/java/com/google/devtools/build/android/desugar/StackMapBugTest.java45
-rw-r--r--test/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriterTest.java414
-rw-r--r--test/java/com/google/devtools/build/android/desugar/capture_lambda_disassembled_golden.txt21
-rw-r--r--test/java/com/google/devtools/build/android/desugar/class_with_inherited_abstract_method_disassembled_golden.txt4
-rw-r--r--test/java/com/google/devtools/build/android/desugar/class_with_lambdas_in_implemented_interface_disassembled_golden.txt8
-rwxr-xr-xtest/java/com/google/devtools/build/android/desugar/diff.sh16
-rw-r--r--test/java/com/google/devtools/build/android/desugar/interface_with_desugared_method_bodies_disassembled_golden.txt7
-rw-r--r--test/java/com/google/devtools/build/android/desugar/jacoco_0_7_5_default_method.jarbin0 -> 1393 bytes
-rw-r--r--test/java/com/google/devtools/build/android/desugar/jacoco_legacy_default_method_companion_disassembled_golden.txt37
-rw-r--r--test/java/com/google/devtools/build/android/desugar/mocked_android_framework/android/os/Build.java31
-rw-r--r--test/java/com/google/devtools/build/android/desugar/runtime/ConcurrentWeakIdentityHashMapTest.java262
-rw-r--r--test/java/com/google/devtools/build/android/desugar/runtime/ThrowableExtensionTest.java453
-rw-r--r--test/java/com/google/devtools/build/android/desugar/runtime/ThrowableExtensionTestUtility.java64
-rw-r--r--test/java/com/google/devtools/build/android/desugar/simple_instance_method_reference_disassembled_golden.txt21
-rw-r--r--test/java/com/google/devtools/build/android/desugar/stateless_lambda_disassembled_golden.txt24
-rwxr-xr-xtest/java/com/google/devtools/build/android/desugar/static_initializer_of_functional_interface_should_not_execute.sh22
-rw-r--r--test/java/com/google/devtools/build/android/desugar/testdata/CaptureLambda.java33
-rw-r--r--test/java/com/google/devtools/build/android/desugar/testdata/ClassCallingLongCompare.java51
-rw-r--r--test/java/com/google/devtools/build/android/desugar/testdata/ClassCallingRequireNonNull.java53
-rw-r--r--test/java/com/google/devtools/build/android/desugar/testdata/ClassUsingTryWithResources.java88
-rw-r--r--test/java/com/google/devtools/build/android/desugar/testdata/ConcreteFunction.java54
-rw-r--r--test/java/com/google/devtools/build/android/desugar/testdata/ConstructorReference.java56
-rw-r--r--test/java/com/google/devtools/build/android/desugar/testdata/GuavaLambda.java31
-rw-r--r--test/java/com/google/devtools/build/android/desugar/testdata/InnerClassLambda.java51
-rw-r--r--test/java/com/google/devtools/build/android/desugar/testdata/InterfaceWithLambda.java25
-rw-r--r--test/java/com/google/devtools/build/android/desugar/testdata/Lambda.java60
-rw-r--r--test/java/com/google/devtools/build/android/desugar/testdata/LambdaInOverride.java35
-rw-r--r--test/java/com/google/devtools/build/android/desugar/testdata/MethodReference.java88
-rw-r--r--test/java/com/google/devtools/build/android/desugar/testdata/MethodReferenceInSubclass.java37
-rw-r--r--test/java/com/google/devtools/build/android/desugar/testdata/MethodReferenceSuperclass.java39
-rw-r--r--test/java/com/google/devtools/build/android/desugar/testdata/OuterReferenceLambda.java33
-rw-r--r--test/java/com/google/devtools/build/android/desugar/testdata/SpecializedFunction.java22
-rw-r--r--test/java/com/google/devtools/build/android/desugar/testdata/core_library/java/lang/AutoboxedTypes.java36
-rw-r--r--test/java/com/google/devtools/build/android/desugar/testdata/core_library/test/util/TestClassForStackMapFrame.java56
-rw-r--r--test/java/com/google/devtools/build/android/desugar/testdata/java8/AnnotationsOfDefaultMethodsShouldBeKept.java43
-rw-r--r--test/java/com/google/devtools/build/android/desugar/testdata/java8/ConcreteDefaultInterfaceWithLambda.java28
-rw-r--r--test/java/com/google/devtools/build/android/desugar/testdata/java8/ConcreteOverridesDefaultWithLambda.java37
-rw-r--r--test/java/com/google/devtools/build/android/desugar/testdata/java8/DefaultInterfaceMethodWithStaticInitializer.java172
-rw-r--r--test/java/com/google/devtools/build/android/desugar/testdata/java8/DefaultInterfaceWithBridges.java66
-rw-r--r--test/java/com/google/devtools/build/android/desugar/testdata/java8/DefaultInterfaceWithLambda.java33
-rw-r--r--test/java/com/google/devtools/build/android/desugar/testdata/java8/FunctionWithDefaultMethod.java78
-rw-r--r--test/java/com/google/devtools/build/android/desugar/testdata/java8/FunctionalInterfaceWithInitializerAndDefaultMethods.java60
-rw-r--r--test/java/com/google/devtools/build/android/desugar/testdata/java8/GenericDefaultInterfaceWithLambda.java97
-rw-r--r--test/java/com/google/devtools/build/android/desugar/testdata/java8/InterfaceMethod.java49
-rw-r--r--test/java/com/google/devtools/build/android/desugar/testdata/java8/InterfaceWithDefaultMethod.java41
-rw-r--r--test/java/com/google/devtools/build/android/desugar/testdata/java8/InterfaceWithDuplicateMethods.java45
-rw-r--r--test/java/com/google/devtools/build/android/desugar/testdata/java8/Java7InterfaceWithBridges.java56
-rw-r--r--test/java/com/google/devtools/build/android/desugar/testdata/java8/Named.java72
-rw-r--r--test/java/com/google/devtools/build/android/desugar/testdata/java8/TwoInheritedDefaultMethods.java36
-rw-r--r--test/java/com/google/devtools/build/android/desugar/testdata/java8/VisibilityTestClass.java23
-rw-r--r--test/java/com/google/devtools/build/android/desugar/testdata/java8/subpackage/PackagePrivateInterface.java33
-rw-r--r--test/java/com/google/devtools/build/android/desugar/testdata/java8/subpackage/PublicInterface.java22
-rw-r--r--test/java/com/google/devtools/build/android/desugar/testdata/separate/SeparateBaseClass.java32
-rw-r--r--test/java/com/google/devtools/build/android/desugar/testdata/separate/SeparateInterface.java21
-rw-r--r--test/java/com/google/devtools/build/android/desugar/testdata/testresource.txt1
-rw-r--r--test/java/com/google/devtools/build/android/desugar/testdata_desugared_core_library_jar_toc_golden.txt18
-rw-r--r--test/java/com/google/devtools/build/android/desugar/testdata_desugared_for_disabling_twr_with_large_minsdkversion_jar_toc_golden.txt65
-rw-r--r--test/java/com/google/devtools/build/android/desugar/testdata_desugared_for_try_with_resources_jar_toc_golden.txt72
-rwxr-xr-xtest/java/com/google/devtools/build/android/desugar/testdata_desugared_jar_test.sh44
-rw-r--r--test/java/com/google/devtools/build/android/desugar/testdata_desugared_jar_toc_golden.txt72
-rw-r--r--test/java/com/google/devtools/build/android/desugar/testdata_desugared_java8_jar_toc_golden.txt145
-rw-r--r--test/java/com/google/devtools/build/android/desugar/testdata_desugared_without_lambda_desugared_jar_toc_golden.txt36
104 files changed, 7926 insertions, 1132 deletions
diff --git a/java/com/google/devtools/build/android/desugar/Desugar.java b/java/com/google/devtools/build/android/desugar/Desugar.java
index 63ca5e7..31c362e 100644
--- a/java/com/google/devtools/build/android/desugar/Desugar.java
+++ b/java/com/google/devtools/build/android/desugar/Desugar.java
@@ -229,6 +229,19 @@ class Desugar {
help = "Enables rewriting to desugar java.* classes."
)
public boolean coreLibrary;
+
+ /** 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(
+ name = "legacy_jacoco_fix",
+ defaultValue = "false",
+ documentationCategory = OptionDocumentationCategory.UNDOCUMENTED,
+ effectTags = {OptionEffectTag.UNKNOWN},
+ help = "Consider setting this flag if you're using JaCoCo versions prior to 0.7.9 to work "
+ + "around issues with coverage instrumentation in default and static interface methods. "
+ + "This flag may be removed when no longer needed."
+ )
+ public boolean legacyJacocoFix;
}
private final DesugarOptions options;
@@ -511,7 +524,8 @@ class Desugar {
if (options.desugarInterfaceMethodBodiesIfNeeded) {
visitor =
new DefaultMethodClassFixer(visitor, classpathReader, bootclasspathReader, loader);
- visitor = new InterfaceDesugaring(visitor, bootclasspathReader, store);
+ visitor =
+ new InterfaceDesugaring(visitor, bootclasspathReader, store, options.legacyJacocoFix);
}
}
visitor =
@@ -561,7 +575,8 @@ class Desugar {
if (options.desugarInterfaceMethodBodiesIfNeeded) {
visitor =
new DefaultMethodClassFixer(visitor, classpathReader, bootclasspathReader, loader);
- visitor = new InterfaceDesugaring(visitor, bootclasspathReader, store);
+ visitor =
+ new InterfaceDesugaring(visitor, bootclasspathReader, store, options.legacyJacocoFix);
}
}
// LambdaDesugaring is relatively expensive, so check first whether we need it. Additionally,
diff --git a/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java b/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java
index e2351cb..0d37a20 100644
--- a/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java
+++ b/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java
@@ -20,6 +20,7 @@ import static com.google.common.base.Preconditions.checkState;
import javax.annotation.Nullable;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
@@ -46,6 +47,7 @@ class InterfaceDesugaring extends ClassVisitor {
private final ClassReaderFactory bootclasspath;
private final GeneratedClassStore store;
+ private final boolean legacyJaCoCo;
private String internalName;
private int bytecodeVersion;
@@ -54,10 +56,12 @@ class InterfaceDesugaring extends ClassVisitor {
@Nullable private FieldInfo interfaceFieldToAccessInCompanionMethodToTriggerInterfaceClinit;
public InterfaceDesugaring(
- ClassVisitor dest, ClassReaderFactory bootclasspath, GeneratedClassStore store) {
+ ClassVisitor dest, ClassReaderFactory bootclasspath, GeneratedClassStore store,
+ boolean legacyJaCoCo) {
super(Opcodes.ASM6, dest);
this.bootclasspath = bootclasspath;
this.store = store;
+ this.legacyJaCoCo = legacyJaCoCo;
}
@Override
@@ -120,6 +124,24 @@ class InterfaceDesugaring extends ClassVisitor {
}
@Override
+ public FieldVisitor visitField(
+ int access, String name, String desc, String signature, Object value) {
+ if (legacyJaCoCo
+ && isInterface()
+ && BitFlags.isSet(access, Opcodes.ACC_FINAL)
+ && "$jacocoData".equals(name)) {
+ // Move $jacocoData field to companion class and remove final modifier. We'll rewrite field
+ // accesses accordingly. Code generated by older JaCoCo versions tried to assign to this
+ // final field in methods, and interface fields have to be private, so we move the field
+ // to a class, which ends up looking pretty similar to what JaCoCo generates for classes.
+ access &= ~Opcodes.ACC_FINAL;
+ return companion().visitField(access, name, desc, signature, value);
+ } else {
+ return super.visitField(access, name, desc, signature, value);
+ }
+ }
+
+ @Override
public MethodVisitor visitMethod(
int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor result;
@@ -127,6 +149,9 @@ class InterfaceDesugaring extends ClassVisitor {
result =
new InterfaceFieldWriteCollector(
super.visitMethod(access, name, desc, signature, exceptions));
+ if (result != null && legacyJaCoCo) {
+ result = new MoveJacocoFieldAccess(result);
+ }
} else if (isInterface()
&& BitFlags.noneSet(access, Opcodes.ACC_ABSTRACT | Opcodes.ACC_BRIDGE)) {
checkArgument(BitFlags.noneSet(access, Opcodes.ACC_NATIVE), "Forbidden per JLS ch 9.4");
@@ -176,6 +201,9 @@ class InterfaceDesugaring extends ClassVisitor {
result = abstractDest != null ? new MultiplexAnnotations(codeDest, abstractDest) : codeDest;
}
+ if (result != null && legacyJaCoCo) {
+ result = new MoveJacocoFieldAccess(result);
+ }
} else {
result = super.visitMethod(access, name, desc, signature, exceptions);
}
@@ -343,6 +371,27 @@ class InterfaceDesugaring extends ClassVisitor {
}
/**
+ * Method visitor intended for interface method bodies that rewrites jacoco field accesses to
+ * expect the field in the companion class, to work around problematic bytecode emitted by older
+ * JaCoCo versions (b/62623509).
+ */
+ private static class MoveJacocoFieldAccess extends MethodVisitor {
+
+ public MoveJacocoFieldAccess(MethodVisitor mv) {
+ super(Opcodes.ASM5, mv);
+ }
+
+ @Override
+ public void visitFieldInsn(int opcode, String owner, String name, String desc) {
+ if ("$jacocoData".equals(name)) {
+ checkState(!owner.endsWith(COMPANION_SUFFIX), "Expected interface: %s", owner);
+ owner = getCompanionClassName(owner);
+ }
+ super.visitFieldInsn(opcode, owner, name, desc);
+ }
+ }
+
+ /**
* Method visitor that behaves like a passthrough but additionally duplicates all annotations into
* a second given {@link MethodVisitor}.
*/
diff --git a/java/com/google/devtools/build/android/desugar/Java7Compatibility.java b/java/com/google/devtools/build/android/desugar/Java7Compatibility.java
index 51efe29..752227e 100644
--- a/java/com/google/devtools/build/android/desugar/Java7Compatibility.java
+++ b/java/com/google/devtools/build/android/desugar/Java7Compatibility.java
@@ -78,7 +78,6 @@ public class Java7Compatibility extends ClassVisitor {
// initializer instead
return null;
}
- // TODO(b/31547323): Avoid stack trace and report errors more user-friendly
checkArgument(!isInterface
|| BitFlags.isSet(access, Opcodes.ACC_ABSTRACT)
|| "<clinit>".equals(name),
diff --git a/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriter.java b/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriter.java
index 507b7d0..270d32b 100644
--- a/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriter.java
+++ b/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriter.java
@@ -14,6 +14,8 @@
package com.google.devtools.build.android.desugar;
import static com.google.common.base.Preconditions.checkNotNull;
+import static org.objectweb.asm.Opcodes.ACC_STATIC;
+import static org.objectweb.asm.Opcodes.ACC_SYNTHETIC;
import static org.objectweb.asm.Opcodes.ASM6;
import static org.objectweb.asm.Opcodes.INVOKESTATIC;
import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL;
@@ -88,6 +90,10 @@ public class TryWithResourcesRewriter extends ClassVisitor {
.put("(Ljava/io/PrintWriter;)V", "(Ljava/lang/Throwable;Ljava/io/PrintWriter;)V")
.build();
+ static final String CLOSE_RESOURCE_METHOD_NAME = "$closeResource";
+ static final String CLOSE_RESOURCE_METHOD_DESC =
+ "(Ljava/lang/Throwable;Ljava/lang/AutoCloseable;)V";
+
private final ClassLoader classLoader;
private final Set<String> visitedExceptionTypes;
private final AtomicInteger numOfTryWithResourcesInvoked;
@@ -129,22 +135,38 @@ public class TryWithResourcesRewriter extends ClassVisitor {
// collect exception types.
Collections.addAll(visitedExceptionTypes, exceptions);
}
+ if (isSyntheticCloseResourceMethod(access, name, desc)) {
+ return null; // Discard this method.
+ }
+
MethodVisitor visitor = super.cv.visitMethod(access, name, desc, signature, exceptions);
return visitor == null || shouldCurrentClassBeIgnored
? visitor
- : new TryWithResourceVisitor(internalName + "." + name + desc, visitor, classLoader);
+ : new TryWithResourceVisitor(internalName, name + desc, visitor, classLoader);
+ }
+
+ private boolean isSyntheticCloseResourceMethod(int access, String name, String desc) {
+ return BitFlags.isSet(access, ACC_SYNTHETIC | ACC_STATIC)
+ && CLOSE_RESOURCE_METHOD_NAME.equals(name)
+ && CLOSE_RESOURCE_METHOD_DESC.equals(desc);
}
private class TryWithResourceVisitor extends MethodVisitor {
private final ClassLoader classLoader;
/** For debugging purpose. Enrich exception information. */
+ private final String internalName;
+
private final String methodSignature;
public TryWithResourceVisitor(
- String methodSignature, MethodVisitor methodVisitor, ClassLoader classLoader) {
+ String internalName,
+ String methodSignature,
+ MethodVisitor methodVisitor,
+ ClassLoader classLoader) {
super(ASM6, methodVisitor);
this.classLoader = classLoader;
+ this.internalName = internalName;
this.methodSignature = methodSignature;
}
@@ -158,6 +180,17 @@ public class TryWithResourcesRewriter extends ClassVisitor {
@Override
public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
+ if (isCallToSyntheticCloseResource(opcode, owner, name, desc)) {
+ // Rewrite the call to the runtime library.
+ super.visitMethodInsn(
+ opcode,
+ THROWABLE_EXTENSION_INTERNAL_NAME,
+ "closeResource",
+ "(Ljava/lang/Throwable;Ljava/lang/Object;)V",
+ itf);
+ return;
+ }
+
if (!isMethodCallTargeted(opcode, owner, name, desc)) {
super.visitMethodInsn(opcode, owner, name, desc, itf);
return;
@@ -168,6 +201,23 @@ public class TryWithResourcesRewriter extends ClassVisitor {
INVOKESTATIC, THROWABLE_EXTENSION_INTERNAL_NAME, name, METHOD_DESC_MAP.get(desc), false);
}
+ private boolean isCallToSyntheticCloseResource(
+ int opcode, String owner, String name, String desc) {
+ if (opcode != INVOKESTATIC) {
+ return false;
+ }
+ if (!internalName.equals(owner)) {
+ return false;
+ }
+ if (!CLOSE_RESOURCE_METHOD_NAME.equals(name)) {
+ return false;
+ }
+ if (!CLOSE_RESOURCE_METHOD_DESC.equals(desc)) {
+ return false;
+ }
+ return true;
+ }
+
private boolean isMethodCallTargeted(int opcode, String owner, String name, String desc) {
if (opcode != INVOKEVIRTUAL) {
return false;
@@ -184,7 +234,8 @@ public class TryWithResourcesRewriter extends ClassVisitor {
return throwableClass.isAssignableFrom(klass);
} catch (ClassNotFoundException e) {
throw new AssertionError(
- "Failed to load class when desugaring method " + methodSignature, e);
+ "Failed to load class when desugaring method " + internalName + "." + methodSignature,
+ e);
}
}
}
diff --git a/java/com/google/devtools/build/android/desugar/runtime/ThrowableExtension.java b/java/com/google/devtools/build/android/desugar/runtime/ThrowableExtension.java
index 365884b..070363a 100644
--- a/java/com/google/devtools/build/android/desugar/runtime/ThrowableExtension.java
+++ b/java/com/google/devtools/build/android/desugar/runtime/ThrowableExtension.java
@@ -13,12 +13,15 @@
// limitations under the License.
package com.google.devtools.build.android.desugar.runtime;
+import java.io.Closeable;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
import java.util.List;
import java.util.Vector;
import java.util.concurrent.ConcurrentHashMap;
@@ -43,10 +46,14 @@ public final class ThrowableExtension {
public static final String SYSTEM_PROPERTY_TWR_DISABLE_MIMIC =
"com.google.devtools.build.android.desugar.runtime.twr_disable_mimic";
+ // Visible for testing.
+ static final int API_LEVEL;
+
static {
AbstractDesugaringStrategy strategy;
+ Integer apiLevel = null;
try {
- Integer apiLevel = readApiLevelFromBuildVersion();
+ apiLevel = readApiLevelFromBuildVersion();
if (apiLevel != null && apiLevel.intValue() >= 19) {
strategy = new ReuseDesugaringStrategy();
} else if (useMimicStrategy()) {
@@ -66,6 +73,7 @@ public final class ThrowableExtension {
strategy = new NullDesugaringStrategy();
}
STRATEGY = strategy;
+ API_LEVEL = apiLevel == null ? 1 : apiLevel.intValue();
}
public static AbstractDesugaringStrategy getStrategy() {
@@ -92,6 +100,44 @@ public final class ThrowableExtension {
STRATEGY.printStackTrace(receiver, stream);
}
+ public static void closeResource(Throwable throwable, Object resource) throws Throwable {
+ if (resource == null) {
+ return;
+ }
+ try {
+ if (API_LEVEL >= 19) {
+ ((AutoCloseable) resource).close();
+ } else {
+ if (resource instanceof Closeable) {
+ ((Closeable) resource).close();
+ } else {
+ try {
+ Method method = resource.getClass().getMethod("close");
+ method.invoke(resource);
+ } catch (NoSuchMethodException | SecurityException e) {
+ throw new AssertionError(resource.getClass() + " does not have a close() method.", e);
+ } catch (IllegalAccessException
+ | IllegalArgumentException
+ | ExceptionInInitializerError e) {
+ throw new AssertionError("Fail to call close() on " + resource.getClass(), e);
+ } catch (InvocationTargetException e) {
+ // Exception occurs during the invocation to the close method. The cause is the real
+ // exception.
+ Throwable cause = e.getCause();
+ throw cause;
+ }
+ }
+ }
+ } catch (Throwable e) {
+ if (throwable != null) {
+ addSuppressed(throwable, e);
+ throw throwable;
+ } else {
+ throw e;
+ }
+ }
+ }
+
private static boolean useMimicStrategy() {
return !Boolean.getBoolean(SYSTEM_PROPERTY_TWR_DISABLE_MIMIC);
}
diff --git a/java/com/google/devtools/common/options/Converters.java b/java/com/google/devtools/common/options/Converters.java
index cf26e71..35f2da4 100644
--- a/java/com/google/devtools/common/options/Converters.java
+++ b/java/com/google/devtools/common/options/Converters.java
@@ -15,6 +15,7 @@ package com.google.devtools.common.options;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import java.time.Duration;
import java.util.Iterator;
@@ -167,7 +168,7 @@ public final class Converters {
public static class VoidConverter implements Converter<Void> {
@Override
public Void convert(String input) throws OptionsParsingException {
- if (input == null) {
+ if (input == null || input.equals("null")) {
return null; // expected input, return is unused so null is fine.
}
throw new OptionsParsingException("'" + input + "' unexpected");
@@ -220,24 +221,23 @@ public final class Converters {
}
}
+ // 1:1 correspondence with UsesOnlyCoreTypes.CORE_TYPES.
/**
* The converters that are available to the options parser by default. These are used if the
* {@code @Option} annotation does not specify its own {@code converter}, and its type is one of
* the following.
*/
- static final Map<Class<?>, Converter<?>> DEFAULT_CONVERTERS = Maps.newHashMap();
-
- static {
- // 1:1 correspondence with UsesOnlyCoreTypes.CORE_TYPES.
- DEFAULT_CONVERTERS.put(String.class, new Converters.StringConverter());
- DEFAULT_CONVERTERS.put(int.class, new Converters.IntegerConverter());
- DEFAULT_CONVERTERS.put(long.class, new Converters.LongConverter());
- DEFAULT_CONVERTERS.put(double.class, new Converters.DoubleConverter());
- DEFAULT_CONVERTERS.put(boolean.class, new Converters.BooleanConverter());
- DEFAULT_CONVERTERS.put(TriState.class, new Converters.TriStateConverter());
- DEFAULT_CONVERTERS.put(Duration.class, new Converters.DurationConverter());
- DEFAULT_CONVERTERS.put(Void.class, new Converters.VoidConverter());
- }
+ public static final ImmutableMap<Class<?>, Converter<?>> DEFAULT_CONVERTERS =
+ new ImmutableMap.Builder<Class<?>, Converter<?>>()
+ .put(String.class, new Converters.StringConverter())
+ .put(int.class, new Converters.IntegerConverter())
+ .put(long.class, new Converters.LongConverter())
+ .put(double.class, new Converters.DoubleConverter())
+ .put(boolean.class, new Converters.BooleanConverter())
+ .put(TriState.class, new Converters.TriStateConverter())
+ .put(Duration.class, new Converters.DurationConverter())
+ .put(Void.class, new Converters.VoidConverter())
+ .build();
/**
* Join a list of words as in English. Examples:
diff --git a/java/com/google/devtools/common/options/DuplicateOptionDeclarationException.java b/java/com/google/devtools/common/options/DuplicateOptionDeclarationException.java
index ad4c975..0f4dc08 100644
--- a/java/com/google/devtools/common/options/DuplicateOptionDeclarationException.java
+++ b/java/com/google/devtools/common/options/DuplicateOptionDeclarationException.java
@@ -15,7 +15,7 @@
package com.google.devtools.common.options;
/** Indicates that a flag is declared more than once. */
-public class DuplicateOptionDeclarationException extends RuntimeException {
+public class DuplicateOptionDeclarationException extends Exception {
DuplicateOptionDeclarationException(String message) {
super(message);
diff --git a/java/com/google/devtools/common/options/InvocationPolicyEnforcer.java b/java/com/google/devtools/common/options/InvocationPolicyEnforcer.java
index e76688c..742acb6 100644
--- a/java/com/google/devtools/common/options/InvocationPolicyEnforcer.java
+++ b/java/com/google/devtools/common/options/InvocationPolicyEnforcer.java
@@ -14,6 +14,7 @@
package com.google.devtools.common.options;
import com.google.common.base.Joiner;
+import com.google.common.base.Preconditions;
import com.google.common.base.Verify;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList;
@@ -27,7 +28,6 @@ import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.In
import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.SetValue;
import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.UseDefault;
import com.google.devtools.common.options.OptionsParser.OptionDescription;
-import com.google.devtools.common.options.OptionsParser.OptionValueDescription;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@@ -49,10 +49,11 @@ import javax.annotation.Nullable;
*/
public final class InvocationPolicyEnforcer {
- private static final Logger log = Logger.getLogger(InvocationPolicyEnforcer.class.getName());
-
- private static final Function<Object, String> INVOCATION_POLICY_SOURCE = o -> "Invocation policy";
+ private static final Logger logger = Logger.getLogger(InvocationPolicyEnforcer.class.getName());
+ private static final String INVOCATION_POLICY_SOURCE = "Invocation policy";
+ private static final Function<OptionDefinition, String> INVOCATION_POLICY_SOURCE_FUNCTION =
+ o -> INVOCATION_POLICY_SOURCE;
@Nullable private final InvocationPolicy invocationPolicy;
/**
@@ -108,24 +109,26 @@ public final class InvocationPolicyEnforcer {
// This flag doesn't exist. We are deliberately lenient if the flag policy has a flag
// we don't know about. This is for better future proofing so that as new flags are added,
// new policies can use the new flags without worrying about older versions of Bazel.
- log.info(
+ logger.info(
String.format("Flag '%s' specified by invocation policy does not exist", flagName));
continue;
}
- OptionDescription optionDescription = parser.getOptionDescription(flagName);
- // extractOptionDefinition() will return null if the option does not exist, however
+ OptionDescription optionDescription =
+ parser.getOptionDescription(
+ flagName, OptionPriority.INVOCATION_POLICY, INVOCATION_POLICY_SOURCE);
+ // getOptionDescription() will return null if the option does not exist, however
// getOptionValueDescription() above would have thrown an IllegalArgumentException if that
// were the case.
Verify.verifyNotNull(optionDescription);
switch (flagPolicy.getOperationCase()) {
case SET_VALUE:
- applySetValueOperation(parser, flagPolicy, flagName, valueDescription, optionDescription);
+ applySetValueOperation(parser, flagPolicy, valueDescription, optionDescription);
break;
case USE_DEFAULT:
- applyUseDefaultOperation(parser, "UseDefault", flagName);
+ applyUseDefaultOperation(parser, "UseDefault", optionDescription.getOptionDefinition());
break;
case ALLOW_VALUES:
@@ -135,7 +138,6 @@ public final class InvocationPolicyEnforcer {
allowValues.getAllowedValuesList(),
allowValues.hasNewValue() ? allowValues.getNewValue() : null,
allowValues.hasUseDefault(),
- flagName,
valueDescription,
optionDescription);
break;
@@ -147,7 +149,6 @@ public final class InvocationPolicyEnforcer {
disallowValues.getDisallowedValuesList(),
disallowValues.hasNewValue() ? disallowValues.getNewValue() : null,
disallowValues.hasUseDefault(),
- flagName,
valueDescription,
optionDescription);
break;
@@ -156,11 +157,10 @@ public final class InvocationPolicyEnforcer {
throw new PolicyOperationNotSetException(flagName);
default:
- log.warning(
+ logger.warning(
String.format(
"Unknown operation '%s' from invocation policy for flag '%s'",
- flagPolicy.getOperationCase(),
- flagName));
+ flagPolicy.getOperationCase(), flagName));
break;
}
}
@@ -232,16 +232,23 @@ public final class InvocationPolicyEnforcer {
String.format("Disallow_Values on expansion flags like %s is not allowed.", flagName));
}
- private static ImmutableList<OptionValueDescription> getExpansionsFromFlagPolicy(
+ private static ImmutableList<ParsedOptionDescription> getExpansionsFromFlagPolicy(
FlagPolicy expansionPolicy, OptionDescription optionDescription, OptionsParser parser)
throws OptionsParsingException {
if (!optionDescription.isExpansion()) {
return ImmutableList.of();
}
- String expansionFlagName = expansionPolicy.getFlagName();
+ Preconditions.checkArgument(
+ expansionPolicy
+ .getFlagName()
+ .equals(optionDescription.getOptionDefinition().getOptionName()),
+ String.format(
+ "The optionDescription provided (for flag %s) does not match the policy for flag %s.",
+ optionDescription.getOptionDefinition().getOptionName(),
+ expansionPolicy.getFlagName()));
- ImmutableList.Builder<OptionValueDescription> resultsBuilder = ImmutableList.builder();
+ ImmutableList.Builder<ParsedOptionDescription> resultsBuilder = ImmutableList.builder();
switch (expansionPolicy.getOperationCase()) {
case SET_VALUE:
{
@@ -249,16 +256,29 @@ public final class InvocationPolicyEnforcer {
if (setValue.getFlagValueCount() > 0) {
for (String value : setValue.getFlagValueList()) {
resultsBuilder.addAll(
- parser.getExpansionOptionValueDescriptions(expansionFlagName, value));
+ parser.getExpansionOptionValueDescriptions(
+ optionDescription.getOptionDefinition(),
+ value,
+ OptionPriority.INVOCATION_POLICY,
+ INVOCATION_POLICY_SOURCE));
}
} else {
resultsBuilder.addAll(
- parser.getExpansionOptionValueDescriptions(expansionFlagName, null));
+ parser.getExpansionOptionValueDescriptions(
+ optionDescription.getOptionDefinition(),
+ null,
+ OptionPriority.INVOCATION_POLICY,
+ INVOCATION_POLICY_SOURCE));
}
}
break;
case USE_DEFAULT:
- resultsBuilder.addAll(parser.getExpansionOptionValueDescriptions(expansionFlagName, null));
+ resultsBuilder.addAll(
+ parser.getExpansionOptionValueDescriptions(
+ optionDescription.getOptionDefinition(),
+ null,
+ OptionPriority.INVOCATION_POLICY,
+ INVOCATION_POLICY_SOURCE));
break;
case ALLOW_VALUES:
// All expansions originally given to the parser have been expanded by now, so these two
@@ -273,10 +293,11 @@ public final class InvocationPolicyEnforcer {
case OPERATION_NOT_SET:
throw new PolicyOperationNotSetException(expansionPolicy.getFlagName());
default:
- log.warning(
+ logger.warning(
String.format(
"Unknown operation '%s' from invocation policy for flag '%s'",
- expansionPolicy.getOperationCase(), expansionFlagName));
+ expansionPolicy.getOperationCase(),
+ optionDescription.getOptionDefinition().getOptionName()));
break;
}
@@ -297,54 +318,58 @@ public final class InvocationPolicyEnforcer {
List<FlagPolicy> expandedPolicies = new ArrayList<>();
OptionDescription originalOptionDescription =
- parser.getOptionDescription(originalPolicy.getFlagName());
+ parser.getOptionDescription(
+ originalPolicy.getFlagName(),
+ OptionPriority.INVOCATION_POLICY,
+ INVOCATION_POLICY_SOURCE);
if (originalOptionDescription == null) {
// InvocationPolicy ignores policy on non-existing flags by design, for version compatibility.
return expandedPolicies;
}
- ImmutableList<OptionValueDescription> expansions =
+ ImmutableList<ParsedOptionDescription> expansions =
getExpansionsFromFlagPolicy(originalPolicy, originalOptionDescription, parser);
- ImmutableList.Builder<OptionValueDescription> subflagBuilder = ImmutableList.builder();
- ImmutableList<OptionValueDescription> subflags =
+ ImmutableList.Builder<ParsedOptionDescription> subflagBuilder = ImmutableList.builder();
+ ImmutableList<ParsedOptionDescription> subflags =
subflagBuilder
.addAll(originalOptionDescription.getImplicitRequirements())
.addAll(expansions)
.build();
boolean isExpansion = originalOptionDescription.isExpansion();
- if (!subflags.isEmpty() && log.isLoggable(Level.FINE)) {
+ if (!subflags.isEmpty() && logger.isLoggable(Level.FINE)) {
// Log the expansion. Since this is logged regardless of user provided command line, it is
// only really useful for understanding the invocation policy itself. Most of the time,
// invocation policy does not change, so this can be a log level fine.
List<String> subflagNames = new ArrayList<>(subflags.size());
- for (OptionValueDescription subflag : subflags) {
- subflagNames.add("--" + subflag.getName());
+ for (ParsedOptionDescription subflag : subflags) {
+ subflagNames.add("--" + subflag.getOptionDefinition().getOptionName());
}
- log.logp(Level.FINE,
+ logger.logp(
+ Level.FINE,
"InvocationPolicyEnforcer",
"expandPolicy",
String.format(
- "Expanding %s on option %s to its %s: %s.",
- originalPolicy.getOperationCase(),
- originalPolicy.getFlagName(),
- isExpansion ? "expansions" : "implied flags",
- Joiner.on("; ").join(subflagNames)));
+ "Expanding %s on option %s to its %s: %s.",
+ originalPolicy.getOperationCase(),
+ originalPolicy.getFlagName(),
+ isExpansion ? "expansions" : "implied flags",
+ Joiner.on("; ").join(subflagNames)));
}
// Repeated flags are special, and could set multiple times in an expansion, with the user
// expecting both values to be valid. Collect these separately.
- Multimap<String, OptionValueDescription> repeatableSubflagsInSetValues =
+ Multimap<OptionDefinition, ParsedOptionDescription> repeatableSubflagsInSetValues =
ArrayListMultimap.create();
// Create a flag policy for the child that looks like the parent's policy "transferred" to its
// child. Note that this only makes sense for SetValue, when setting an expansion flag, or
// UseDefault, when preventing it from being set.
- for (OptionValueDescription currentSubflag : subflags) {
- if (currentSubflag.getAllowMultiple()
+ for (ParsedOptionDescription currentSubflag : subflags) {
+ if (currentSubflag.getOptionDefinition().allowsMultiple()
&& originalPolicy.getOperationCase().equals(OperationCase.SET_VALUE)) {
- repeatableSubflagsInSetValues.put(currentSubflag.getName(), currentSubflag);
+ repeatableSubflagsInSetValues.put(currentSubflag.getOptionDefinition(), currentSubflag);
} else {
FlagPolicy subflagAsPolicy =
getSingleValueSubflagAsPolicy(currentSubflag, originalPolicy, isExpansion);
@@ -356,15 +381,13 @@ public final class InvocationPolicyEnforcer {
// If there are any repeatable flag SetValues, deal with them together now.
// Note that expansion flags have no value, and so cannot have multiple values either.
// Skipping the recursion above is fine.
- for (String repeatableFlag : repeatableSubflagsInSetValues.keySet()) {
+ for (OptionDefinition repeatableFlag : repeatableSubflagsInSetValues.keySet()) {
int numValues = repeatableSubflagsInSetValues.get(repeatableFlag).size();
ArrayList<String> newValues = new ArrayList<>(numValues);
- for (OptionValueDescription setValue : repeatableSubflagsInSetValues.get(repeatableFlag)) {
- newValues.add(setValue.getOriginalValueString());
+ for (ParsedOptionDescription setValue : repeatableSubflagsInSetValues.get(repeatableFlag)) {
+ newValues.add(setValue.getUnconvertedValue());
}
- expandedPolicies.add(
- getSetValueSubflagAsPolicy(
- repeatableFlag, newValues, /* allowMultiple */ true, originalPolicy));
+ expandedPolicies.add(getSetValueSubflagAsPolicy(repeatableFlag, newValues, originalPolicy));
}
// Don't add the original policy if it was an expansion flag, which have no value, but do add
@@ -381,21 +404,17 @@ public final class InvocationPolicyEnforcer {
* policies that set the flag, and so interact with repeatable flags, flags that can be set
* multiple times, in subtle ways.
*
- * @param subflagName, the flag the SetValue'd expansion flag expands to.
+ * @param subflag, the definition of the flag the SetValue'd expansion flag expands to.
* @param subflagValue, the values that the SetValue'd expansion flag expands to for this flag.
- * @param allowMultiple, whether the flag is multivalued.
* @param originalPolicy, the original policy on the expansion flag.
* @return the flag policy for the subflag given, this will be part of the expanded form of the
- * SetValue policy on the original flag.
+ * SetValue policy on the original flag.
*/
private static FlagPolicy getSetValueSubflagAsPolicy(
- String subflagName,
- List<String> subflagValue,
- boolean allowMultiple,
- FlagPolicy originalPolicy) {
+ OptionDefinition subflag, List<String> subflagValue, FlagPolicy originalPolicy) {
// Some sanity checks.
Verify.verify(originalPolicy.getOperationCase().equals(OperationCase.SET_VALUE));
- if (!allowMultiple) {
+ if (!subflag.allowsMultiple()) {
Verify.verify(subflagValue.size() <= 1);
}
@@ -405,7 +424,7 @@ public final class InvocationPolicyEnforcer {
for (String value : subflagValue) {
setValueExpansion.addFlagValue(value);
}
- if (allowMultiple) {
+ if (subflag.allowsMultiple()) {
setValueExpansion.setAppend(originalPolicy.getSetValue().getOverridable());
} else {
setValueExpansion.setOverridable(originalPolicy.getSetValue().getOverridable());
@@ -413,10 +432,10 @@ public final class InvocationPolicyEnforcer {
// Commands from the original policy, flag name of the expansion
return FlagPolicy.newBuilder()
- .addAllCommands(originalPolicy.getCommandsList())
- .setFlagName(subflagName)
- .setSetValue(setValueExpansion)
- .build();
+ .addAllCommands(originalPolicy.getCommandsList())
+ .setFlagName(subflag.getOptionName())
+ .setSetValue(setValueExpansion)
+ .build();
}
/**
@@ -424,12 +443,12 @@ public final class InvocationPolicyEnforcer {
* corresponding policy.
*/
private static FlagPolicy getSingleValueSubflagAsPolicy(
- OptionValueDescription currentSubflag, FlagPolicy originalPolicy, boolean isExpansion)
+ ParsedOptionDescription currentSubflag, FlagPolicy originalPolicy, boolean isExpansion)
throws OptionsParsingException {
FlagPolicy subflagAsPolicy = null;
switch (originalPolicy.getOperationCase()) {
case SET_VALUE:
- if (currentSubflag.getAllowMultiple()) {
+ if (currentSubflag.getOptionDefinition().allowsMultiple()) {
throw new AssertionError(
"SetValue subflags with allowMultiple should have been dealt with separately and "
+ "accumulated into a single FlagPolicy.");
@@ -437,14 +456,14 @@ public final class InvocationPolicyEnforcer {
// Accept null originalValueStrings, they are expected when the subflag is also an expansion
// flag.
List<String> subflagValue;
- if (currentSubflag.getOriginalValueString() == null) {
+ if (currentSubflag.getUnconvertedValue() == null) {
subflagValue = ImmutableList.of();
} else {
- subflagValue = ImmutableList.of(currentSubflag.getOriginalValueString());
+ subflagValue = ImmutableList.of(currentSubflag.getUnconvertedValue());
}
subflagAsPolicy =
getSetValueSubflagAsPolicy(
- currentSubflag.getName(), subflagValue, /*allowMultiple=*/ false, originalPolicy);
+ currentSubflag.getOptionDefinition(), subflagValue, originalPolicy);
break;
case USE_DEFAULT:
@@ -452,10 +471,8 @@ public final class InvocationPolicyEnforcer {
subflagAsPolicy =
FlagPolicy.newBuilder()
.addAllCommands(originalPolicy.getCommandsList())
- .setFlagName(currentSubflag.getName())
- .setUseDefault(
- UseDefault
- .getDefaultInstance())
+ .setFlagName(currentSubflag.getOptionDefinition().getOptionName())
+ .setUseDefault(UseDefault.getDefaultInstance())
.build();
break;
@@ -489,7 +506,7 @@ public final class InvocationPolicyEnforcer {
private static void logInApplySetValueOperation(String formattingString, Object... objects) {
// Finding the caller here is relatively expensive and shows up in profiling, so provide it
// manually.
- log.logp(
+ logger.logp(
Level.INFO,
"InvocationPolicyEnforcer",
"applySetValueOperation",
@@ -499,19 +516,18 @@ public final class InvocationPolicyEnforcer {
private static void applySetValueOperation(
OptionsParser parser,
FlagPolicy flagPolicy,
- String flagName,
OptionValueDescription valueDescription,
OptionDescription optionDescription)
throws OptionsParsingException {
-
SetValue setValue = flagPolicy.getSetValue();
+ OptionDefinition optionDefinition = optionDescription.getOptionDefinition();
// SetValue.flag_value must have at least 1 value.
if (setValue.getFlagValueCount() == 0) {
throw new OptionsParsingException(
String.format(
"SetValue operation from invocation policy for flag '%s' does not have a value",
- flagName));
+ optionDefinition.getOptionName()));
}
// Flag must allow multiple values if multiple values are specified by the policy.
@@ -521,7 +537,7 @@ public final class InvocationPolicyEnforcer {
String.format(
"SetValue operation from invocation policy sets multiple values for flag '%s' which "
+ "does not allow multiple values",
- flagName));
+ optionDefinition.getOptionName()));
}
if (setValue.getOverridable() && valueDescription != null) {
@@ -531,57 +547,61 @@ public final class InvocationPolicyEnforcer {
"Keeping value '%s' from source '%s' for flag '%s' "
+ "because the invocation policy specifying the value(s) '%s' is overridable",
valueDescription.getValue(),
- valueDescription.getSource(),
- flagName,
+ valueDescription.getSourceString(),
+ optionDefinition.getOptionName(),
setValue.getFlagValueList());
} else {
if (!setValue.getAppend()) {
// Clear the value in case the flag is a repeated flag so that values don't accumulate.
- parser.clearValue(flagName);
+ parser.clearValue(optionDescription.getOptionDefinition());
}
// Set all the flag values from the policy.
for (String flagValue : setValue.getFlagValueList()) {
if (valueDescription == null) {
logInApplySetValueOperation(
- "Setting value for flag '%s' from invocation "
- + "policy to '%s', overriding the default value '%s'",
- flagName, flagValue, optionDescription.getOptionDefinition().getDefaultValue());
+ "Setting value for flag '%s' from invocation policy to '%s', overriding the "
+ + "default value '%s'",
+ optionDefinition.getOptionName(), flagValue, optionDefinition.getDefaultValue());
} else {
logInApplySetValueOperation(
- "Setting value for flag '%s' from invocation "
- + "policy to '%s', overriding value '%s' from '%s'",
- flagName, flagValue, valueDescription.getValue(), valueDescription.getSource());
+ "Setting value for flag '%s' from invocation policy to '%s', overriding "
+ + "value '%s' from '%s'",
+ optionDefinition.getOptionName(),
+ flagValue,
+ valueDescription.getValue(),
+ valueDescription.getSourceString());
}
- setFlagValue(parser, flagName, flagValue);
+ setFlagValue(parser, optionDefinition, flagValue);
}
}
}
private static void applyUseDefaultOperation(
- OptionsParser parser, String policyType, String flagName) throws OptionsParsingException {
- OptionValueDescription clearedValueDescription = parser.clearValue(flagName);
+ OptionsParser parser, String policyType, OptionDefinition option)
+ throws OptionsParsingException {
+ OptionValueDescription clearedValueDescription = parser.clearValue(option);
if (clearedValueDescription != null) {
// Log the removed value.
- String clearedFlagName = clearedValueDescription.getName();
- String originalValue = clearedValueDescription.getValue().toString();
- String source = clearedValueDescription.getSource();
+ String clearedFlagName = clearedValueDescription.getOptionDefinition().getOptionName();
- OptionDescription desc = parser.getOptionDescription(clearedFlagName);
+ OptionDescription desc =
+ parser.getOptionDescription(
+ clearedFlagName, OptionPriority.INVOCATION_POLICY, INVOCATION_POLICY_SOURCE);
Object clearedFlagDefaultValue = null;
if (desc != null) {
clearedFlagDefaultValue = desc.getOptionDefinition().getDefaultValue();
}
- log.info(
+ logger.info(
String.format(
- "Using default value '%s' for flag '%s' as "
- + "specified by %s invocation policy, overriding original value '%s' from '%s'",
+ "Using default value '%s' for flag '%s' as specified by %s invocation policy, "
+ + "overriding original value '%s' from '%s'",
clearedFlagDefaultValue,
clearedFlagName,
policyType,
- originalValue,
- source));
+ clearedValueDescription.getValue(),
+ clearedValueDescription.getSourceString()));
}
}
@@ -629,11 +649,10 @@ public final class InvocationPolicyEnforcer {
List<String> policyValues,
String newValue,
boolean useDefault,
- String flagName,
OptionValueDescription valueDescription,
OptionDescription optionDescription)
throws OptionsParsingException {
-
+ OptionDefinition optionDefinition = optionDescription.getOptionDefinition();
// Convert all the allowed values from strings to real objects using the options'
// converters so that they can be checked for equality using real .equals() instead
// of string comparison. For example, "--foo=0", "--foo=false", "--nofoo", and "-f-"
@@ -641,18 +660,15 @@ public final class InvocationPolicyEnforcer {
// can be arbitrarily complex.
Set<Object> convertedPolicyValues = new HashSet<>();
for (String value : policyValues) {
- Object convertedValue =
- optionDescription.getOptionDefinition().getConverter().convert(value);
+ Object convertedValue = optionDefinition.getConverter().convert(value);
// Some converters return lists, and if the flag is a repeatable flag, the items in the
// list from the converter should be added, and not the list itself. Otherwise the items
// from invocation policy will be compared to lists, which will never work.
// See OptionsParserImpl.ParsedOptionEntry.addValue.
- if (optionDescription.getOptionDefinition().allowsMultiple()
- && convertedValue instanceof List<?>) {
+ if (optionDefinition.allowsMultiple() && convertedValue instanceof List<?>) {
convertedPolicyValues.addAll((List<?>) convertedValue);
} else {
- convertedPolicyValues.add(
- optionDescription.getOptionDefinition().getConverter().convert(value));
+ convertedPolicyValues.add(optionDefinition.getConverter().convert(value));
}
}
@@ -670,7 +686,9 @@ public final class InvocationPolicyEnforcer {
String.format(
"%sValues policy disallows the default value '%s' for flag '%s' but also "
+ "specifies to use the default value",
- policyType, optionDescription.getOptionDefinition().getDefaultValue(), flagName));
+ policyType,
+ optionDefinition.getDefaultValue(),
+ optionDefinition.getOptionName()));
}
}
@@ -680,59 +698,51 @@ public final class InvocationPolicyEnforcer {
// the flag allowing multiple values, however, flags that allow multiple values cannot have
// default values, and their value is always the empty list if they haven't been specified,
// which is why new_default_value is not a repeated field.
- checkDefaultValue(
- parser,
- policyValues,
- newValue,
- flagName,
- optionDescription,
- convertedPolicyValues);
+ checkDefaultValue(parser, optionDescription, policyValues, newValue, convertedPolicyValues);
} else {
checkUserValue(
parser,
+ optionDescription,
+ valueDescription,
policyValues,
newValue,
useDefault,
- flagName,
- valueDescription,
- optionDescription,
convertedPolicyValues);
}
}
void checkDefaultValue(
OptionsParser parser,
+ OptionDescription optionDescription,
List<String> policyValues,
String newValue,
- String flagName,
- OptionDescription optionDescription,
Set<Object> convertedPolicyValues)
throws OptionsParsingException {
+ OptionDefinition optionDefinition = optionDescription.getOptionDefinition();
if (!isFlagValueAllowed(
convertedPolicyValues, optionDescription.getOptionDefinition().getDefaultValue())) {
if (newValue != null) {
// Use the default value from the policy.
- log.info(
+ logger.info(
String.format(
- "Overriding default value '%s' for flag '%s' with value '%s' "
- + "specified by invocation policy. %sed values are: %s",
- optionDescription.getOptionDefinition().getDefaultValue(),
- flagName,
+ "Overriding default value '%s' for flag '%s' with value '%s' specified by "
+ + "invocation policy. %sed values are: %s",
+ optionDefinition.getDefaultValue(),
+ optionDefinition.getOptionName(),
newValue,
policyType,
policyValues));
- parser.clearValue(flagName);
- setFlagValue(parser, flagName, newValue);
+ parser.clearValue(optionDefinition);
+ setFlagValue(parser, optionDefinition, newValue);
} else {
// The operation disallows the default value, but doesn't supply a new value.
throw new OptionsParsingException(
String.format(
"Default flag value '%s' for flag '%s' is not allowed by invocation policy, but "
- + "the policy does not provide a new value. "
- + "%sed values are: %s",
+ + "the policy does not provide a new value. %sed values are: %s",
optionDescription.getOptionDefinition().getDefaultValue(),
- flagName,
+ optionDefinition.getOptionName(),
policyType,
policyValues));
}
@@ -741,15 +751,14 @@ public final class InvocationPolicyEnforcer {
void checkUserValue(
OptionsParser parser,
+ OptionDescription optionDescription,
+ OptionValueDescription valueDescription,
List<String> policyValues,
String newValue,
boolean useDefault,
- String flagName,
- OptionValueDescription valueDescription,
- OptionDescription optionDescription,
Set<Object> convertedPolicyValues)
throws OptionsParsingException {
-
+ OptionDefinition option = optionDescription.getOptionDefinition();
if (optionDescription.getOptionDefinition().allowsMultiple()) {
// allowMultiple requires that the type of the option be List<T>, so cast from Object
// to List<?>.
@@ -757,16 +766,13 @@ public final class InvocationPolicyEnforcer {
for (Object value : optionValues) {
if (!isFlagValueAllowed(convertedPolicyValues, value)) {
if (useDefault) {
- applyUseDefaultOperation(parser, policyType + "Values", flagName);
+ applyUseDefaultOperation(parser, policyType + "Values", option);
} else {
throw new OptionsParsingException(
String.format(
"Flag value '%s' for flag '%s' is not allowed by invocation policy. "
+ "%sed values are: %s",
- value,
- flagName,
- policyType,
- policyValues));
+ value, option.getOptionName(), policyType, policyValues));
}
}
}
@@ -775,41 +781,38 @@ public final class InvocationPolicyEnforcer {
if (!isFlagValueAllowed(convertedPolicyValues, valueDescription.getValue())) {
if (newValue != null) {
- log.info(
+ logger.info(
String.format(
"Overriding disallowed value '%s' for flag '%s' with value '%s' "
+ "specified by invocation policy. %sed values are: %s",
valueDescription.getValue(),
- flagName,
+ option.getOptionName(),
newValue,
policyType,
policyValues));
- parser.clearValue(flagName);
- setFlagValue(parser, flagName, newValue);
+ parser.clearValue(option);
+ setFlagValue(parser, option, newValue);
} else if (useDefault) {
- applyUseDefaultOperation(parser, policyType + "Values", flagName);
+ applyUseDefaultOperation(parser, policyType + "Values", option);
} else {
throw new OptionsParsingException(
String.format(
"Flag value '%s' for flag '%s' is not allowed by invocation policy and the "
+ "policy does not specify a new value. %sed values are: %s",
- valueDescription.getValue(),
- flagName,
- policyType,
- policyValues));
+ valueDescription.getValue(), option.getOptionName(), policyType, policyValues));
}
}
}
}
}
- private static void setFlagValue(OptionsParser parser, String flagName, String flagValue)
+ private static void setFlagValue(OptionsParser parser, OptionDefinition flag, String flagValue)
throws OptionsParsingException {
parser.parseWithSourceFunction(
OptionPriority.INVOCATION_POLICY,
- INVOCATION_POLICY_SOURCE,
- ImmutableList.of(String.format("--%s=%s", flagName, flagValue)));
+ INVOCATION_POLICY_SOURCE_FUNCTION,
+ ImmutableList.of(String.format("--%s=%s", flag.getOptionName(), flagValue)));
}
}
diff --git a/java/com/google/devtools/common/options/IsolatedOptionsData.java b/java/com/google/devtools/common/options/IsolatedOptionsData.java
index ca91b1d..58eb07d 100644
--- a/java/com/google/devtools/common/options/IsolatedOptionsData.java
+++ b/java/com/google/devtools/common/options/IsolatedOptionsData.java
@@ -19,16 +19,10 @@ import com.google.common.collect.ImmutableMap;
import com.google.devtools.common.options.OptionDefinition.NotAnOptionException;
import com.google.devtools.common.options.OptionsParser.ConstructionException;
import java.lang.reflect.Constructor;
-import java.lang.reflect.Field;
-import java.lang.reflect.Method;
-import java.lang.reflect.Modifier;
-import java.lang.reflect.ParameterizedType;
-import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
-import java.util.List;
import java.util.Map;
import java.util.Objects;
import javax.annotation.concurrent.Immutable;
@@ -51,6 +45,40 @@ import javax.annotation.concurrent.Immutable;
public class IsolatedOptionsData extends OpaqueOptionsData {
/**
+ * Cache for the options in an OptionsBase.
+ *
+ * <p>Mapping from options class to a list of all {@code OptionFields} in that class. The map
+ * entries are unordered, but the fields in the lists are ordered alphabetically. This caches the
+ * work of reflection done for the same {@code optionsBase} across multiple {@link OptionsData}
+ * instances, and must be used through the thread safe {@link
+ * #getAllOptionDefinitionsForClass(Class)}
+ */
+ private static final Map<Class<? extends OptionsBase>, ImmutableList<OptionDefinition>>
+ allOptionsFields = new HashMap<>();
+
+ /** Returns all {@code optionDefinitions}, ordered by their option name (not their field name). */
+ public static synchronized ImmutableList<OptionDefinition> getAllOptionDefinitionsForClass(
+ Class<? extends OptionsBase> optionsClass) {
+ return allOptionsFields.computeIfAbsent(
+ optionsClass,
+ optionsBaseClass ->
+ Arrays.stream(optionsBaseClass.getFields())
+ .map(
+ field -> {
+ try {
+ return OptionDefinition.extractOptionDefinition(field);
+ } catch (NotAnOptionException e) {
+ // Ignore non-@Option annotated fields. Requiring all fields in the
+ // OptionsBase to be @Option-annotated requires a depot cleanup.
+ return null;
+ }
+ })
+ .filter(Objects::nonNull)
+ .sorted(OptionDefinition.BY_OPTION_NAME)
+ .collect(ImmutableList.toImmutableList()));
+ }
+
+ /**
* Mapping from each options class to its no-arg constructor. Entries appear in the same order
* that they were passed to {@link #from(Collection)}.
*/
@@ -63,15 +91,17 @@ public class IsolatedOptionsData extends OpaqueOptionsData {
*/
private final ImmutableMap<String, OptionDefinition> nameToField;
+ /**
+ * For options that have an "OldName", this is a mapping from old name to its corresponding {@code
+ * OptionDefinition}. Entries appear ordered first by their options class (the order in which they
+ * were passed to {@link #from(Collection)}, and then in alphabetic order within each options
+ * class.
+ */
+ private final ImmutableMap<String, OptionDefinition> oldNameToField;
+
/** Mapping from option abbreviation to {@code OptionDefinition} (unordered). */
private final ImmutableMap<Character, OptionDefinition> abbrevToField;
- /**
- * Mapping from options class to a list of all {@code OptionFields} in that class. The map entries
- * are unordered, but the fields in the lists are ordered alphabetically.
- */
- private final ImmutableMap<Class<? extends OptionsBase>, ImmutableList<OptionDefinition>>
- allOptionsFields;
/**
* Mapping from each options class to whether or not it has the {@link UsesOnlyCoreTypes}
@@ -79,20 +109,16 @@ public class IsolatedOptionsData extends OpaqueOptionsData {
*/
private final ImmutableMap<Class<? extends OptionsBase>, Boolean> usesOnlyCoreTypes;
- /** These categories used to indicate OptionUsageRestrictions, but no longer. */
- private static final ImmutableList<String> DEPRECATED_CATEGORIES = ImmutableList.of(
- "undocumented", "hidden", "internal");
-
private IsolatedOptionsData(
Map<Class<? extends OptionsBase>, Constructor<?>> optionsClasses,
Map<String, OptionDefinition> nameToField,
+ Map<String, OptionDefinition> oldNameToField,
Map<Character, OptionDefinition> abbrevToField,
- Map<Class<? extends OptionsBase>, ImmutableList<OptionDefinition>> allOptionsFields,
Map<Class<? extends OptionsBase>, Boolean> usesOnlyCoreTypes) {
this.optionsClasses = ImmutableMap.copyOf(optionsClasses);
this.nameToField = ImmutableMap.copyOf(nameToField);
+ this.oldNameToField = ImmutableMap.copyOf(oldNameToField);
this.abbrevToField = ImmutableMap.copyOf(abbrevToField);
- this.allOptionsFields = ImmutableMap.copyOf(allOptionsFields);
this.usesOnlyCoreTypes = ImmutableMap.copyOf(usesOnlyCoreTypes);
}
@@ -100,8 +126,8 @@ public class IsolatedOptionsData extends OpaqueOptionsData {
this(
other.optionsClasses,
other.nameToField,
+ other.oldNameToField,
other.abbrevToField,
- other.allOptionsFields,
other.usesOnlyCoreTypes);
}
@@ -118,16 +144,20 @@ public class IsolatedOptionsData extends OpaqueOptionsData {
return (Constructor<T>) optionsClasses.get(clazz);
}
- public OptionDefinition getFieldFromName(String name) {
- return nameToField.get(name);
+ /**
+ * Returns the option in this parser by the provided name, or {@code null} if none is found. This
+ * will match both the canonical name of an option, and any old name listed that we still accept.
+ */
+ public OptionDefinition getOptionDefinitionFromName(String name) {
+ return nameToField.getOrDefault(name, oldNameToField.get(name));
}
/**
- * Returns all pairs of option names (not field names) and their corresponding {@link Field}
- * objects. Entries appear ordered first by their options class (the order in which they were
- * passed to {@link #from(Collection)}, and then in alphabetic order within each options class.
+ * Returns all {@link OptionDefinition} objects loaded, mapped by their canonical names. Entries
+ * appear ordered first by their options class (the order in which they were passed to {@link
+ * #from(Collection)}, and then in alphabetic order within each options class.
*/
- public Iterable<Map.Entry<String, OptionDefinition>> getAllNamedFields() {
+ public Iterable<Map.Entry<String, OptionDefinition>> getAllOptionDefinitions() {
return nameToField.entrySet();
}
@@ -135,50 +165,30 @@ public class IsolatedOptionsData extends OpaqueOptionsData {
return abbrevToField.get(abbrev);
}
- /**
- * Returns a list of all {@link Field} objects for options in the given options class, ordered
- * alphabetically by option name.
- */
- public ImmutableList<OptionDefinition> getOptionDefinitionsFromClass(
- Class<? extends OptionsBase> optionsClass) {
- return allOptionsFields.get(optionsClass);
- }
-
public boolean getUsesOnlyCoreTypes(Class<? extends OptionsBase> optionsClass) {
return usesOnlyCoreTypes.get(optionsClass);
}
- /** Returns all {@code optionDefinitions}, ordered by their option name (not their field name). */
- private static ImmutableList<OptionDefinition> getAllOptionDefinitionsSorted(
- Class<? extends OptionsBase> optionsClass) {
- return Arrays.stream(optionsClass.getFields())
- .map(
- field -> {
- try {
- return OptionDefinition.extractOptionDefinition(field);
- } catch (NotAnOptionException e) {
- // Ignore non-@Option annotated fields. Requiring all fields in the OptionsBase to
- // be @Option-annotated requires a depot cleanup.
- return null;
- }
- })
- .filter(Objects::nonNull)
- .sorted(OptionDefinition.BY_OPTION_NAME)
- .collect(ImmutableList.toImmutableList());
- }
-
+ /**
+ * Generic method to check for collisions between the names we give options. Useful for checking
+ * both single-character abbreviations and full names.
+ */
private static <A> void checkForCollisions(
- Map<A, OptionDefinition> aFieldMap, A optionName, String description) {
+ Map<A, OptionDefinition> aFieldMap, A optionName, String description)
+ throws DuplicateOptionDeclarationException {
if (aFieldMap.containsKey(optionName)) {
throw new DuplicateOptionDeclarationException(
"Duplicate option name, due to " + description + ": --" + optionName);
}
}
+ /**
+ * All options, even non-boolean ones, should check that they do not conflict with previously
+ * loaded boolean options.
+ */
private static void checkForBooleanAliasCollisions(
- Map<String, String> booleanAliasMap,
- String optionName,
- String description) {
+ Map<String, String> booleanAliasMap, String optionName, String description)
+ throws DuplicateOptionDeclarationException {
if (booleanAliasMap.containsKey(optionName)) {
throw new DuplicateOptionDeclarationException(
"Duplicate option name, due to "
@@ -190,12 +200,20 @@ public class IsolatedOptionsData extends OpaqueOptionsData {
}
}
+ /**
+ * For an {@code option} of boolean type, this checks that the boolean alias does not conflict
+ * with other names, and adds the boolean alias to a list so that future flags can find if they
+ * conflict with a boolean alias..
+ */
private static void checkAndUpdateBooleanAliases(
Map<String, OptionDefinition> nameToFieldMap,
+ Map<String, OptionDefinition> oldNameToFieldMap,
Map<String, String> booleanAliasMap,
- String optionName) {
+ String optionName)
+ throws DuplicateOptionDeclarationException {
// Check that the negating alias does not conflict with existing flags.
checkForCollisions(nameToFieldMap, "no" + optionName, "boolean option alias");
+ checkForCollisions(oldNameToFieldMap, "no" + optionName, "boolean option alias");
// Record that the boolean option takes up additional namespace for its negating alias.
booleanAliasMap.put("no" + optionName, optionName);
@@ -209,9 +227,8 @@ public class IsolatedOptionsData extends OpaqueOptionsData {
static IsolatedOptionsData from(Collection<Class<? extends OptionsBase>> classes) {
// Mind which fields have to preserve order.
Map<Class<? extends OptionsBase>, Constructor<?>> constructorBuilder = new LinkedHashMap<>();
- Map<Class<? extends OptionsBase>, ImmutableList<OptionDefinition>> allOptionsFieldsBuilder =
- new HashMap<>();
Map<String, OptionDefinition> nameToFieldBuilder = new LinkedHashMap<>();
+ Map<String, OptionDefinition> oldNameToFieldBuilder = new LinkedHashMap<>();
Map<Character, OptionDefinition> abbrevToFieldBuilder = new HashMap<>();
// Maps the negated boolean flag aliases to the original option name.
@@ -219,134 +236,61 @@ public class IsolatedOptionsData extends OpaqueOptionsData {
Map<Class<? extends OptionsBase>, Boolean> usesOnlyCoreTypesBuilder = new HashMap<>();
- // Read all Option annotations:
+ // Combine the option definitions for these options classes, and check that they do not
+ // conflict. The options are individually checked for correctness at compile time in the
+ // OptionProcessor.
for (Class<? extends OptionsBase> parsedOptionsClass : classes) {
try {
- Constructor<? extends OptionsBase> constructor =
- parsedOptionsClass.getConstructor();
+ Constructor<? extends OptionsBase> constructor = parsedOptionsClass.getConstructor();
constructorBuilder.put(parsedOptionsClass, constructor);
} catch (NoSuchMethodException e) {
throw new IllegalArgumentException(parsedOptionsClass
+ " lacks an accessible default constructor");
}
ImmutableList<OptionDefinition> optionDefinitions =
- getAllOptionDefinitionsSorted(parsedOptionsClass);
- allOptionsFieldsBuilder.put(parsedOptionsClass, optionDefinitions);
+ getAllOptionDefinitionsForClass(parsedOptionsClass);
for (OptionDefinition optionDefinition : optionDefinitions) {
- String optionName = optionDefinition.getOptionName();
-
- // Check that the option makes sense on its own, as defined.
- if (optionName == null) {
- throw new ConstructionException("Option cannot have a null name");
- }
-
- if (DEPRECATED_CATEGORIES.contains(optionDefinition.getOptionCategory())) {
- throw new ConstructionException(
- "Documentation level is no longer read from the option category. Category \""
- + optionDefinition.getOptionCategory()
- + "\" in option \""
- + optionName
- + "\" is disallowed.");
- }
-
- Type fieldType = optionDefinition.getFieldSingularType();
- // For simple, static expansions, don't accept non-Void types.
- if (optionDefinition.getOptionExpansion().length != 0 && !optionDefinition.isVoidField()) {
- throw new ConstructionException(
- "Option "
- + optionDefinition.getOptionName()
- + " is an expansion flag with a static expansion, but does not have Void type.");
- }
-
- // Get the converter's return type to check that it matches the option type.
- @SuppressWarnings("rawtypes")
- Class<? extends Converter> converterClass = optionDefinition.getProvidedConverter();
- if (converterClass == Converter.class) {
- Converter<?> actualConverter = Converters.DEFAULT_CONVERTERS.get(fieldType);
- if (actualConverter == null) {
- throw new ConstructionException(
- "Cannot find converter for field of type "
- + optionDefinition.getType()
- + " named "
- + optionDefinition.getField().getName()
- + " in class "
- + optionDefinition.getField().getDeclaringClass().getName());
- }
- converterClass = actualConverter.getClass();
- }
- if (Modifier.isAbstract(converterClass.getModifiers())) {
- throw new ConstructionException(
- "The converter type " + converterClass + " must be a concrete type");
- }
- Type converterResultType;
try {
- Method convertMethod = converterClass.getMethod("convert", String.class);
- converterResultType =
- GenericTypeHelper.getActualReturnType(converterClass, convertMethod);
- } catch (NoSuchMethodException e) {
- throw new ConstructionException(
- "A known converter object doesn't implement the convert method");
- }
-
- if (optionDefinition.allowsMultiple()) {
- if (GenericTypeHelper.getRawType(converterResultType) == List.class) {
- Type elementType =
- ((ParameterizedType) converterResultType).getActualTypeArguments()[0];
- if (!GenericTypeHelper.isAssignableFrom(fieldType, elementType)) {
- throw new ConstructionException(
- "If the converter return type of a multiple occurrence option is a list, then "
- + "the type of list elements ("
- + fieldType
- + ") must be assignable from the converter list element type ("
- + elementType
- + ")");
- }
- } else {
- if (!GenericTypeHelper.isAssignableFrom(fieldType, converterResultType)) {
- throw new ConstructionException(
- "Type of list elements ("
- + fieldType
- + ") for multiple occurrence option must be assignable from the converter "
- + "return type ("
- + converterResultType
- + ")");
- }
+ String optionName = optionDefinition.getOptionName();
+ checkForCollisions(nameToFieldBuilder, optionName, "option name collision");
+ checkForCollisions(
+ oldNameToFieldBuilder,
+ optionName,
+ "option name collision with another option's old name");
+ checkForBooleanAliasCollisions(booleanAliasMap, optionName, "option");
+ if (optionDefinition.usesBooleanValueSyntax()) {
+ checkAndUpdateBooleanAliases(
+ nameToFieldBuilder, oldNameToFieldBuilder, booleanAliasMap, optionName);
}
- } else {
- if (!GenericTypeHelper.isAssignableFrom(fieldType, converterResultType)) {
- throw new ConstructionException(
- "Type of field ("
- + fieldType
- + ") must be assignable from the converter return type ("
- + converterResultType
- + ")");
+ nameToFieldBuilder.put(optionName, optionDefinition);
+
+ if (!optionDefinition.getOldOptionName().isEmpty()) {
+ String oldName = optionDefinition.getOldOptionName();
+ checkForCollisions(
+ nameToFieldBuilder,
+ oldName,
+ "old option name collision with another option's canonical name");
+ checkForCollisions(
+ oldNameToFieldBuilder,
+ oldName,
+ "old option name collision with another old option name");
+ checkForBooleanAliasCollisions(booleanAliasMap, oldName, "old option name");
+ // If boolean, repeat the alias dance for the old name.
+ if (optionDefinition.usesBooleanValueSyntax()) {
+ checkAndUpdateBooleanAliases(
+ nameToFieldBuilder, oldNameToFieldBuilder, booleanAliasMap, oldName);
+ }
+ // Now that we've checked for conflicts, confidently store the old name.
+ oldNameToFieldBuilder.put(oldName, optionDefinition);
}
- }
-
- if (optionDefinition.isBooleanField()) {
- checkAndUpdateBooleanAliases(nameToFieldBuilder, booleanAliasMap, optionName);
- }
-
- checkForCollisions(nameToFieldBuilder, optionName, "option");
- checkForBooleanAliasCollisions(booleanAliasMap, optionName, "option");
- nameToFieldBuilder.put(optionName, optionDefinition);
-
- if (!optionDefinition.getOldOptionName().isEmpty()) {
- String oldName = optionDefinition.getOldOptionName();
- checkForCollisions(nameToFieldBuilder, oldName, "old option name");
- checkForBooleanAliasCollisions(booleanAliasMap, oldName, "old option name");
- nameToFieldBuilder.put(optionDefinition.getOldOptionName(), optionDefinition);
-
- // If boolean, repeat the alias dance for the old name.
- if (optionDefinition.isBooleanField()) {
- checkAndUpdateBooleanAliases(nameToFieldBuilder, booleanAliasMap, oldName);
+ if (optionDefinition.getAbbreviation() != '\0') {
+ checkForCollisions(
+ abbrevToFieldBuilder, optionDefinition.getAbbreviation(), "option abbreviation");
+ abbrevToFieldBuilder.put(optionDefinition.getAbbreviation(), optionDefinition);
}
- }
- if (optionDefinition.getAbbreviation() != '\0') {
- checkForCollisions(
- abbrevToFieldBuilder, optionDefinition.getAbbreviation(), "option abbreviation");
- abbrevToFieldBuilder.put(optionDefinition.getAbbreviation(), optionDefinition);
+ } catch (DuplicateOptionDeclarationException e) {
+ throw new ConstructionException(e);
}
}
@@ -375,8 +319,8 @@ public class IsolatedOptionsData extends OpaqueOptionsData {
return new IsolatedOptionsData(
constructorBuilder,
nameToFieldBuilder,
+ oldNameToFieldBuilder,
abbrevToFieldBuilder,
- allOptionsFieldsBuilder,
usesOnlyCoreTypesBuilder);
}
diff --git a/java/com/google/devtools/common/options/LegacyParamsFilePreProcessor.java b/java/com/google/devtools/common/options/LegacyParamsFilePreProcessor.java
new file mode 100644
index 0000000..9e8eeb0
--- /dev/null
+++ b/java/com/google/devtools/common/options/LegacyParamsFilePreProcessor.java
@@ -0,0 +1,147 @@
+// Copyright 2017 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.common.options;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.FileSystem;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.NoSuchElementException;
+
+/**
+ * A {@link ParamsFilePreProcessor} that processes a parameter file using a custom format. This
+ * format assumes each parameter is separated by whitespace and allows arguments to use single and
+ * double quotes and quote and whitespace escaping.
+ */
+public class LegacyParamsFilePreProcessor extends ParamsFilePreProcessor {
+
+ public LegacyParamsFilePreProcessor(FileSystem fs) {
+ super(fs);
+ }
+
+ @Override
+ protected List<String> parse(Path paramsFile) throws IOException, OptionsParsingException {
+ try (Reader params = Files.newBufferedReader(paramsFile, StandardCharsets.UTF_8)) {
+ List<String> newArgs = new ArrayList<>();
+ StringBuilder arg = new StringBuilder();
+ CharIterator iterator = CharIterator.wrap(params);
+ while (iterator.hasNext()) {
+ char next = iterator.next();
+ if (Character.isWhitespace(next) && !iterator.isInQuote() && !iterator.isEscaped()) {
+ newArgs.add(unescape(arg.toString()));
+ arg = new StringBuilder();
+ } else {
+ arg.append(next);
+ }
+ }
+ // If there is an arg in the buffer, add it.
+ if (arg.length() > 0) {
+ newArgs.add(arg.toString());
+ }
+ // If we're still in a quote by the end of the file, throw an error.
+ if (iterator.isInQuote()) {
+ throw new OptionsParsingException(
+ String.format(ERROR_MESSAGE_FORMAT, paramsFile, iterator.getUnmatchedQuoteMessage()));
+ }
+ return newArgs;
+ }
+ }
+
+ private String unescape(String arg) {
+ if (arg.startsWith("'") && arg.endsWith("'")) {
+ String unescaped = arg.replace("'\\''", "'");
+ return unescaped.substring(1, unescaped.length() - 1);
+ }
+ return arg;
+ }
+
+ // Doesn't implement iterator to avoid autoboxing and to throw exceptions.
+ private static class CharIterator {
+
+ private final Reader reader;
+ private int readerPosition = 0;
+ private int singleQuoteStart = -1;
+ private int doubleQuoteStart = -1;
+ private boolean escaped = false;
+ private char lastChar = (char) -1;
+
+ public static CharIterator wrap(Reader reader) {
+ return new CharIterator(reader);
+ }
+
+ public CharIterator(Reader reader) {
+ this.reader = reader;
+ }
+
+ public boolean hasNext() throws IOException {
+ return peek() != -1;
+ }
+
+ private int peek() throws IOException {
+ reader.mark(1);
+ int next = reader.read();
+ reader.reset();
+ return next;
+ }
+
+ public boolean isInQuote() {
+ return singleQuoteStart != -1 || doubleQuoteStart != -1;
+ }
+
+ public boolean isEscaped() {
+ return escaped;
+ }
+
+ public String getUnmatchedQuoteMessage() {
+ StringBuilder message = new StringBuilder();
+ if (singleQuoteStart != -1) {
+ message.append(String.format(UNFINISHED_QUOTE_MESSAGE_FORMAT, "'", singleQuoteStart));
+ }
+ if (doubleQuoteStart != -1) {
+ message.append(String.format(UNFINISHED_QUOTE_MESSAGE_FORMAT, "\"", doubleQuoteStart));
+ }
+ return message.toString();
+ }
+
+ public char next() throws IOException {
+ if (!hasNext()) {
+ throw new NoSuchElementException();
+ }
+ char current = (char) reader.read();
+
+ // check for \r\n line endings. If found, drop the \r for normalized parsing.
+ if (current == '\r' && peek() == '\n') {
+ current = (char) reader.read();
+ }
+
+ // check to see if the current position is escaped
+ escaped = (lastChar == '\\');
+
+ if (!escaped && current == '\'') {
+ singleQuoteStart = singleQuoteStart == -1 ? readerPosition : -1;
+ }
+ if (!escaped && current == '"') {
+ doubleQuoteStart = doubleQuoteStart == -1 ? readerPosition : -1;
+ }
+
+ readerPosition++;
+ lastChar = current;
+ return current;
+ }
+ }
+}
diff --git a/java/com/google/devtools/common/options/Option.java b/java/com/google/devtools/common/options/Option.java
index 829f9e9..92436fd 100644
--- a/java/com/google/devtools/common/options/Option.java
+++ b/java/com/google/devtools/common/options/Option.java
@@ -23,6 +23,9 @@ import java.lang.annotation.Target;
*
* <p>The fields of this annotation have matching getters in {@link OptionDefinition}. Please do not
* access these fields directly, but instead go through that class.
+ *
+ * <p>A number of checks are run on an Option's fields' values at compile time. See
+ * {@link com.google.devtools.common.options.processor.OptionProcessor} for details.
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@@ -85,9 +88,6 @@ public @interface Option {
*
* <p>For undocumented flags that aren't listed anywhere, set this to
* OptionDocumentationCategory.UNDOCUMENTED.
- *
- * <p>For hidden or internal options, please set this as UNDOCUMENTED and set the specific reason
- * for this state in the metadataTags() field.
*/
OptionDocumentationCategory documentationCategory();
@@ -107,6 +107,8 @@ public @interface Option {
*
* <p>If one or more of the OptionMetadataTag values apply, please include, but otherwise, this
* list can be left blank.
+ *
+ * <p>Hidden or internal options must be UNDOCUMENTED (set in {@link #documentationCategory()}).
*/
OptionMetadataTag[] metadataTags() default {};
@@ -195,5 +197,6 @@ public @interface Option {
* 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 d3f0d34..40da044 100644
--- a/java/com/google/devtools/common/options/OptionDefinition.java
+++ b/java/com/google/devtools/common/options/OptionDefinition.java
@@ -21,7 +21,6 @@ import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Collections;
import java.util.Comparator;
-import java.util.List;
/**
* Everything the {@link OptionsParser} needs to know about how an option is defined.
@@ -33,18 +32,23 @@ import java.util.List;
public class OptionDefinition {
// TODO(b/65049598) make ConstructionException checked, which will make this checked as well.
- public static class NotAnOptionException extends ConstructionException {
- public NotAnOptionException(Field field) {
+ static class NotAnOptionException extends ConstructionException {
+ NotAnOptionException(Field field) {
super(
- "The field " + field + " does not have the right annotation to be considered an option.");
+ "The field "
+ + field.getName()
+ + " does not have the right annotation to be considered an option.");
}
}
/**
* If the {@code field} is annotated with the appropriate @{@link Option} annotation, returns the
* {@code OptionDefinition} for that option. Otherwise, throws a {@link NotAnOptionException}.
+ *
+ * <p>These values are cached in the {@link OptionsData} layer and should be accessed through
+ * {@link OptionsParser#getOptionDefinitions(Class)}.
*/
- public static OptionDefinition extractOptionDefinition(Field field) {
+ static OptionDefinition extractOptionDefinition(Field field) {
Option annotation = field == null ? null : field.getAnnotation(Option.class);
if (annotation == null) {
throw new NotAnOptionException(field);
@@ -157,6 +161,11 @@ public class OptionDefinition {
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);
+ }
+
/** The type of the optionDefinition. */
public Class<?> getType() {
return field.getType();
@@ -176,6 +185,11 @@ public class OptionDefinition {
return (getOptionExpansion().length > 0 || usesExpansionFunction());
}
+ /** Returns whether the arg is an expansion option. */
+ public boolean hasImplicitRequirements() {
+ return (getImplicitRequirements().length > 0);
+ }
+
/**
* Returns whether the arg is an expansion option defined by an expansion function (and not a
* constant expansion value).
@@ -192,20 +206,9 @@ public class OptionDefinition {
Type getFieldSingularType() {
Type fieldType = getField().getGenericType();
if (allowsMultiple()) {
- // If the type isn't a List<T>, this is an error in the option's declaration.
- if (!(fieldType instanceof ParameterizedType)) {
- throw new ConstructionException(
- String.format(
- "Option %s allows multiple occurrences, so must be of type List<...>",
- getField().getName()));
- }
+ // The validity of the converter is checked at compile time. We know the type to be
+ // List<singularType>.
ParameterizedType pfieldType = (ParameterizedType) fieldType;
- if (pfieldType.getRawType() != List.class) {
- throw new ConstructionException(
- String.format(
- "Option %s allows multiple occurrences, so must be of type List<...>",
- getField().getName()));
- }
fieldType = pfieldType.getActualTypeArguments()[0];
}
return fieldType;
@@ -217,7 +220,7 @@ public class OptionDefinition {
*
* <p>Memoizes the converter-finding logic to avoid repeating the computation.
*/
- Converter<?> getConverter() {
+ public Converter<?> getConverter() {
if (converter != null) {
return converter;
}
@@ -226,13 +229,6 @@ public class OptionDefinition {
// No converter provided, use the default one.
Type type = getFieldSingularType();
converter = Converters.DEFAULT_CONVERTERS.get(type);
- if (converter == null) {
- throw new ConstructionException(
- String.format(
- "Option %s expects values of type %s, but no converter was found; possible fix: "
- + "add converter=... to its @Option annotation.",
- getField().getName(), type));
- }
} else {
try {
// Instantiate the given Converter class.
@@ -254,7 +250,7 @@ public class OptionDefinition {
*
* <p>Can be used for usage help and controlling whether the "no" prefix is allowed.
*/
- boolean isBooleanField() {
+ public boolean usesBooleanValueSyntax() {
return getType().equals(boolean.class)
|| getType().equals(TriState.class)
|| getConverter() instanceof BoolOrEnumConverter;
@@ -288,6 +284,25 @@ public class OptionDefinition {
return defaultValue;
}
+ /**
+ * {@link OptionDefinition} is really a wrapper around a {@link Field} that caches information
+ * obtained through reflection. Checking that the fields they represent are equal is sufficient
+ * to check that two {@link OptionDefinition} objects are equal.
+ */
+ @Override
+ public boolean equals(Object object) {
+ if (!(object instanceof OptionDefinition)) {
+ return false;
+ }
+ OptionDefinition otherOption = (OptionDefinition) object;
+ return field.equals(otherOption.field);
+ }
+
+ @Override
+ public int hashCode() {
+ return field.hashCode();
+ }
+
static final Comparator<OptionDefinition> BY_OPTION_NAME =
Comparator.comparing(OptionDefinition::getOptionName);
diff --git a/java/com/google/devtools/common/options/OptionDocumentationCategory.java b/java/com/google/devtools/common/options/OptionDocumentationCategory.java
index e288b13..1f27046 100644
--- a/java/com/google/devtools/common/options/OptionDocumentationCategory.java
+++ b/java/com/google/devtools/common/options/OptionDocumentationCategory.java
@@ -93,4 +93,11 @@ public enum OptionDocumentationCategory {
/** This option relates to query output and semantics. */
QUERY,
+
+ /**
+ * This option specifies or alters a generic input to a Bazel command. This category should only
+ * be used if the input is generic and does not fall into other categories, such as toolchain-
+ * specific inputs.
+ */
+ GENERIC_INPUTS,
}
diff --git a/java/com/google/devtools/common/options/OptionInstanceOrigin.java b/java/com/google/devtools/common/options/OptionInstanceOrigin.java
new file mode 100644
index 0000000..584e75b
--- /dev/null
+++ b/java/com/google/devtools/common/options/OptionInstanceOrigin.java
@@ -0,0 +1,57 @@
+// Copyright 2017 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.common.options;
+
+import javax.annotation.Nullable;
+
+/**
+ * Contains metadata describing the origin of an option. This includes its priority, a message about
+ * where it came from, and whether it was set explicitly or expanded/implied by other flags.
+ */
+public class OptionInstanceOrigin {
+ private final OptionPriority priority;
+ @Nullable private final String source;
+ @Nullable private final OptionDefinition implicitDependent;
+ @Nullable private final OptionDefinition expandedFrom;
+
+ public OptionInstanceOrigin(
+ OptionPriority priority,
+ String source,
+ OptionDefinition implicitDependent,
+ OptionDefinition expandedFrom) {
+ this.priority = priority;
+ this.source = source;
+ this.implicitDependent = implicitDependent;
+ this.expandedFrom = expandedFrom;
+ }
+
+ public OptionPriority getPriority() {
+ return priority;
+ }
+
+ @Nullable
+ public String getSource() {
+ return source;
+ }
+
+ @Nullable
+ public OptionDefinition getImplicitDependent() {
+ return implicitDependent;
+ }
+
+ @Nullable
+ public OptionDefinition getExpandedFrom() {
+ return expandedFrom;
+ }
+}
diff --git a/java/com/google/devtools/common/options/OptionValueDescription.java b/java/com/google/devtools/common/options/OptionValueDescription.java
new file mode 100644
index 0000000..0d81d49
--- /dev/null
+++ b/java/com/google/devtools/common/options/OptionValueDescription.java
@@ -0,0 +1,372 @@
+// Copyright 2017 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.common.options;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ListMultimap;
+import com.google.devtools.common.options.OptionsParser.ConstructionException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * The value of an option.
+ *
+ * <p>This takes care of tracking the final value as multiple instances of an option are parsed.
+ */
+public abstract class OptionValueDescription {
+
+ protected final OptionDefinition optionDefinition;
+
+ public OptionValueDescription(OptionDefinition optionDefinition) {
+ this.optionDefinition = optionDefinition;
+ }
+
+ public OptionDefinition getOptionDefinition() {
+ return optionDefinition;
+ }
+
+ /** Returns the current or final value of this option. */
+ public abstract Object getValue();
+
+ /** Returns the source(s) of this option, if there were multiple, duplicates are removed. */
+ public abstract String getSourceString();
+
+ abstract void addOptionInstance(
+ ParsedOptionDescription parsedOption,
+ List<String> warnings)
+ throws OptionsParsingException;
+
+ /**
+ * For the given option, returns the correct type of OptionValueDescription, to which unparsed
+ * values can be added.
+ *
+ * <p>The categories of option types are non-overlapping, an invariant checked by the
+ * OptionProcessor at compile time.
+ */
+ public static OptionValueDescription createOptionValueDescription(OptionDefinition option) {
+ if (option.allowsMultiple()) {
+ return new RepeatableOptionValueDescription(option);
+ } else if (option.isExpansionOption()) {
+ return new ExpansionOptionValueDescription(option);
+ } else if (option.hasImplicitRequirements()) {
+ return new OptionWithImplicitRequirementsValueDescription(option);
+ } else if (option.isWrapperOption()) {
+ return new WrapperOptionValueDescription(option);
+ } else {
+ return new SingleOptionValueDescription(option);
+ }
+ }
+
+ /**
+ * For options that have not been set, this will return a correct OptionValueDescription for the
+ * default value.
+ */
+ public static OptionValueDescription getDefaultOptionValue(OptionDefinition option) {
+ return new DefaultOptionValueDescription(option);
+ }
+
+ static class DefaultOptionValueDescription extends OptionValueDescription {
+
+ private DefaultOptionValueDescription(OptionDefinition optionDefinition) {
+ super(optionDefinition);
+ }
+
+ @Override
+ public Object getValue() {
+ return optionDefinition.getDefaultValue();
+ }
+
+ @Override
+ public String getSourceString() {
+ return null;
+ }
+
+ @Override
+ void addOptionInstance(
+ ParsedOptionDescription parsedOption,
+ List<String> warnings) {
+ throw new IllegalStateException(
+ "Cannot add values to the default option value. Create a modifiable "
+ + "OptionValueDescription using createOptionValueDescription() instead.");
+ }
+ }
+
+ /**
+ * The form of a value for a default type of flag, one that does not accumulate multiple values
+ * and has no expansion.
+ */
+ static class SingleOptionValueDescription extends OptionValueDescription {
+ private ParsedOptionDescription effectiveOptionInstance;
+ private Object effectiveValue;
+
+ private SingleOptionValueDescription(OptionDefinition optionDefinition) {
+ super(optionDefinition);
+ if (optionDefinition.allowsMultiple()) {
+ throw new ConstructionException("Can't have a single value for an allowMultiple option.");
+ }
+ if (optionDefinition.isExpansionOption()) {
+ throw new ConstructionException("Can't have a single value for an expansion option.");
+ }
+ effectiveOptionInstance = null;
+ effectiveValue = null;
+ }
+
+ @Override
+ public Object getValue() {
+ return effectiveValue;
+ }
+
+ @Override
+ public String getSourceString() {
+ return effectiveOptionInstance.getSource();
+ }
+
+ // Warnings should not end with a '.' because the internal reporter adds one automatically.
+ @Override
+ void addOptionInstance(
+ ParsedOptionDescription parsedOption,
+ List<String> warnings)
+ throws OptionsParsingException {
+ // This might be the first value, in that case, just store it!
+ if (effectiveOptionInstance == null) {
+ effectiveOptionInstance = parsedOption;
+ effectiveValue = effectiveOptionInstance.getConvertedValue();
+ return;
+ }
+
+ // If there was another value, check whether the new one will override it, and if so,
+ // 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 =
+ effectiveOptionInstance.getImplicitDependent();
+ OptionDefinition optionThatExpandedToEffectiveValue =
+ effectiveOptionInstance.getExpandedFrom();
+
+ // Output warnings:
+ if ((implicitDependent != null) && (optionThatDependsOnEffectiveValue != null)) {
+ if (!implicitDependent.equals(optionThatDependsOnEffectiveValue)) {
+ warnings.add(
+ String.format(
+ "Option '%s' is implicitly defined by both option '%s' and option '%s'",
+ optionDefinition.getOptionName(),
+ optionThatDependsOnEffectiveValue.getOptionName(),
+ implicitDependent.getOptionName()));
+ }
+ } else if ((implicitDependent != null)
+ && parsedOption.getPriority().equals(effectiveOptionInstance.getPriority())) {
+ warnings.add(
+ String.format(
+ "Option '%s' is implicitly defined by option '%s'; the implicitly set value "
+ + "overrides the previous one",
+ optionDefinition.getOptionName(), implicitDependent.getOptionName()));
+ } else if (optionThatDependsOnEffectiveValue != null) {
+ warnings.add(
+ String.format(
+ "A new value for option '%s' overrides a previous implicit setting of that "
+ + "option by option '%s'",
+ optionDefinition.getOptionName(),
+ optionThatDependsOnEffectiveValue.getOptionName()));
+ } else if ((parsedOption.getPriority() == effectiveOptionInstance.getPriority())
+ && ((optionThatExpandedToEffectiveValue == null) && (expandedFrom != null))) {
+ // Create a warning if an expansion option overrides an explicit option:
+ warnings.add(
+ String.format(
+ "The option '%s' was expanded and now overrides a previous explicitly specified "
+ + "option '%s'",
+ expandedFrom.getOptionName(), optionDefinition.getOptionName()));
+ } else if ((optionThatExpandedToEffectiveValue != null) && (expandedFrom != null)) {
+ warnings.add(
+ String.format(
+ "The option '%s' was expanded to from both options '%s' and '%s'",
+ optionDefinition.getOptionName(),
+ optionThatExpandedToEffectiveValue.getOptionName(),
+ expandedFrom.getOptionName()));
+ }
+
+ // Record the new value:
+ effectiveOptionInstance = parsedOption;
+ effectiveValue = parsedOption.getConvertedValue();
+ } else {
+ // The new value does not override the old value, as it has lower priority.
+ warnings.add(
+ String.format(
+ "The lower priority option '%s' does not override the previous value '%s'",
+ parsedOption.getCommandLineForm(), effectiveOptionInstance.getCommandLineForm()));
+ }
+ }
+
+ @VisibleForTesting
+ ParsedOptionDescription getEffectiveOptionInstance() {
+ return effectiveOptionInstance;
+ }
+ }
+
+ /** The form of a value for an option that accumulates multiple values on the command line. */
+ static class RepeatableOptionValueDescription extends OptionValueDescription {
+ ListMultimap<OptionPriority, ParsedOptionDescription> parsedOptions;
+ ListMultimap<OptionPriority, Object> optionValues;
+
+ private RepeatableOptionValueDescription(OptionDefinition optionDefinition) {
+ super(optionDefinition);
+ if (!optionDefinition.allowsMultiple()) {
+ throw new ConstructionException(
+ "Can't have a repeated value for a non-allowMultiple option.");
+ }
+ parsedOptions = ArrayListMultimap.create();
+ optionValues = ArrayListMultimap.create();
+ }
+
+ @Override
+ public String getSourceString() {
+ return parsedOptions
+ .asMap()
+ .values()
+ .stream()
+ .flatMap(Collection::stream)
+ .map(ParsedOptionDescription::getSource)
+ .distinct()
+ .collect(Collectors.joining(", "));
+ }
+
+ @Override
+ public List<Object> getValue() {
+ // Sort the results by option priority and return them in a new list. The generic type of
+ // the list is not known at runtime, so we can't use it here. It was already checked in
+ // the constructor, so this is type-safe.
+ List<Object> result = new ArrayList<>();
+ for (OptionPriority priority : OptionPriority.values()) {
+ // If there is no mapping for this key, this check avoids object creation (because
+ // ListMultimap has to return a new object on get) and also an unnecessary addAll call.
+ if (optionValues.containsKey(priority)) {
+ result.addAll(optionValues.get(priority));
+ }
+ }
+ return result;
+ }
+
+ @Override
+ void addOptionInstance(
+ ParsedOptionDescription parsedOption,
+ List<String> warnings)
+ throws OptionsParsingException {
+ // For repeatable options, we allow flags that take both single values and multiple values,
+ // potentially collapsing them down.
+ Object convertedValue = parsedOption.getConvertedValue();
+ OptionPriority priority = parsedOption.getPriority();
+ parsedOptions.put(priority, parsedOption);
+ if (convertedValue instanceof List<?>) {
+ optionValues.putAll(priority, (List<?>) convertedValue);
+ } else {
+ optionValues.put(priority, convertedValue);
+ }
+ }
+ }
+
+ /**
+ * The form of a value for an expansion option, one that does not have its own value but expands
+ * in place to other options. This should be used for both flags with a static expansion defined
+ * in {@link Option#expansion()} and flags with an {@link Option#expansionFunction()}.
+ */
+ static class ExpansionOptionValueDescription extends OptionValueDescription {
+
+ private ExpansionOptionValueDescription(OptionDefinition optionDefinition) {
+ super(optionDefinition);
+ if (!optionDefinition.isExpansionOption()) {
+ throw new ConstructionException(
+ "Options without expansions can't be tracked using ExpansionOptionValueDescription");
+ }
+ }
+
+ @Override
+ public Object getValue() {
+ return null;
+ }
+
+ @Override
+ public String getSourceString() {
+ return null;
+ }
+
+ @Override
+ void addOptionInstance(
+ ParsedOptionDescription parsedOption,
+ List<String> warnings) {
+ // TODO(b/65540004) Deal with expansion options here instead of in parse(), and track their
+ // link to the options they expanded to to.
+ }
+ }
+
+ /** The form of a value for a flag with implicit requirements. */
+ static class OptionWithImplicitRequirementsValueDescription extends SingleOptionValueDescription {
+
+ private OptionWithImplicitRequirementsValueDescription(OptionDefinition optionDefinition) {
+ super(optionDefinition);
+ if (!optionDefinition.hasImplicitRequirements()) {
+ throw new ConstructionException(
+ "Options without implicit requirements can't be tracked using "
+ + "OptionWithImplicitRequirementsValueDescription");
+ }
+ }
+
+ @Override
+ void addOptionInstance(
+ ParsedOptionDescription parsedOption,
+ List<String> warnings)
+ throws OptionsParsingException {
+ // This is a valued flag, its value is handled the same way as a normal
+ // SingleOptionValueDescription.
+ super.addOptionInstance(parsedOption, warnings);
+
+ // Now deal with the implicit requirements.
+ // TODO(b/65540004) Deal with options with implicit requirements here instead of in parse(),
+ // and track their link to the options they implicitly expanded to to.
+ }
+ }
+
+ /** Form for options that contain other options in the value text to which they expand. */
+ static final class WrapperOptionValueDescription extends OptionValueDescription {
+
+ WrapperOptionValueDescription(OptionDefinition optionDefinition) {
+ super(optionDefinition);
+ }
+
+ @Override
+ public Object getValue() {
+ return null;
+ }
+
+ @Override
+ public String getSourceString() {
+ return null;
+ }
+
+ @Override
+ void addOptionInstance(
+ ParsedOptionDescription parsedOption,
+ List<String> warnings)
+ throws OptionsParsingException {
+ // TODO(b/65540004) Deal with options with implicit requirements here instead of in parse(),
+ // and track their link to the options they implicitly expanded to to.
+ }
+ }
+}
+
+
diff --git a/java/com/google/devtools/common/options/OptionsData.java b/java/com/google/devtools/common/options/OptionsData.java
index eb20a9c..5b9a436 100644
--- a/java/com/google/devtools/common/options/OptionsData.java
+++ b/java/com/google/devtools/common/options/OptionsData.java
@@ -128,17 +128,14 @@ final class OptionsData extends IsolatedOptionsData {
// All that's left is to compute expansions.
ImmutableMap.Builder<OptionDefinition, ExpansionData> expansionDataBuilder =
ImmutableMap.<OptionDefinition, ExpansionData>builder();
- for (Map.Entry<String, OptionDefinition> entry : isolatedData.getAllNamedFields()) {
+ for (Map.Entry<String, OptionDefinition> entry : isolatedData.getAllOptionDefinitions()) {
OptionDefinition optionDefinition = entry.getValue();
- // Determine either the hard-coded expansion, or the ExpansionFunction class.
+ // Determine either the hard-coded expansion, or the ExpansionFunction class. The
+ // OptionProcessor checks at compile time that these aren't used together.
String[] constExpansion = optionDefinition.getOptionExpansion();
Class<? extends ExpansionFunction> expansionFunctionClass =
optionDefinition.getExpansionFunction();
- if (constExpansion.length > 0 && optionDefinition.usesExpansionFunction()) {
- throw new AssertionError(
- "Cannot set both expansion and expansionFunction for option --"
- + optionDefinition.getOptionName());
- } else if (constExpansion.length > 0) {
+ if (constExpansion.length > 0) {
expansionDataBuilder.put(
optionDefinition, new ExpansionData(ImmutableList.copyOf(constExpansion)));
} else if (optionDefinition.usesExpansionFunction()) {
diff --git a/java/com/google/devtools/common/options/OptionsParser.java b/java/com/google/devtools/common/options/OptionsParser.java
index 9d68320..28c2206 100644
--- a/java/com/google/devtools/common/options/OptionsParser.java
+++ b/java/com/google/devtools/common/options/OptionsParser.java
@@ -16,9 +16,9 @@ package com.google.devtools.common.options;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
+import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ListMultimap;
import com.google.common.escape.Escaper;
import com.google.devtools.common.options.OptionDefinition.NotAnOptionException;
import java.lang.reflect.Constructor;
@@ -26,7 +26,6 @@ import java.lang.reflect.Field;
import java.nio.file.FileSystem;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
@@ -127,6 +126,7 @@ public class OptionsParser implements OptionsProvider {
try {
result = OptionsData.from(immutableOptionsClasses);
} catch (Exception e) {
+ Throwables.throwIfInstanceOf(e, ConstructionException.class);
throw new ConstructionException(e.getMessage(), e);
}
optionsData.put(immutableOptionsClasses, result);
@@ -200,9 +200,16 @@ public class OptionsParser implements OptionsProvider {
this.impl.setAllowSingleDashLongOptions(allowSingleDashLongOptions);
}
- /** Enables the Parser to handle params files loacted insinde the provided {@link FileSystem}. */
+ /** Enables the Parser to handle params files located inside the provided {@link FileSystem}. */
public void enableParamsFileSupport(FileSystem fs) {
- this.impl.setArgsPreProcessor(new ParamsFilePreProcessor(fs));
+ enableParamsFileSupport(new LegacyParamsFilePreProcessor(fs));
+ }
+
+ /**
+ * Enables the Parser to handle params files using the provided {@link ParamsFilePreProcessor}.
+ */
+ public void enableParamsFileSupport(ParamsFilePreProcessor preProcessor) {
+ this.impl.setArgsPreProcessor(preProcessor);
}
public void parseAndExitUponError(String[] args) {
@@ -235,12 +242,12 @@ public class OptionsParser implements OptionsProvider {
private final OptionDefinition optionDefinition;
private final OptionsData.ExpansionData expansionData;
- private final ImmutableList<OptionValueDescription> implicitRequirements;
+ private final ImmutableList<ParsedOptionDescription> implicitRequirements;
OptionDescription(
OptionDefinition definition,
OptionsData.ExpansionData expansionData,
- ImmutableList<OptionValueDescription> implicitRequirements) {
+ ImmutableList<ParsedOptionDescription> implicitRequirements) {
this.optionDefinition = definition;
this.expansionData = expansionData;
this.implicitRequirements = implicitRequirements;
@@ -250,7 +257,7 @@ public class OptionsParser implements OptionsProvider {
return optionDefinition;
}
- public ImmutableList<OptionValueDescription> getImplicitRequirements() {
+ public ImmutableList<ParsedOptionDescription> getImplicitRequirements() {
return implicitRequirements;
}
@@ -266,234 +273,6 @@ public class OptionsParser implements OptionsProvider {
}
/**
- * The name and value of an option with additional metadata describing its
- * priority, source, whether it was set via an implicit dependency, and if so,
- * by which other option.
- */
- public static class OptionValueDescription {
- private final String name;
- @Nullable private final String originalValueString;
- @Nullable private final Object value;
- @Nullable private final OptionPriority priority;
- @Nullable private final String source;
- @Nullable private final String implicitDependant;
- @Nullable private final String expandedFrom;
- private final boolean allowMultiple;
-
- public OptionValueDescription(
- String name,
- @Nullable String originalValueString,
- @Nullable Object value,
- @Nullable OptionPriority priority,
- @Nullable String source,
- @Nullable String implicitDependant,
- @Nullable String expandedFrom,
- boolean allowMultiple) {
- this.name = name;
- this.originalValueString = originalValueString;
- this.value = value;
- this.priority = priority;
- this.source = source;
- this.implicitDependant = implicitDependant;
- this.expandedFrom = expandedFrom;
- this.allowMultiple = allowMultiple;
- }
-
- public String getName() {
- return name;
- }
-
- public String getOriginalValueString() {
- return originalValueString;
- }
-
- // Need to suppress unchecked warnings, because the "multiple occurrence"
- // options use unchecked ListMultimaps due to limitations of Java generics.
- @SuppressWarnings({"unchecked", "rawtypes"})
- public Object getValue() {
- if (allowMultiple) {
- // Sort the results by option priority and return them in a new list.
- // The generic type of the list is not known at runtime, so we can't
- // use it here. It was already checked in the constructor, so this is
- // type-safe.
- List result = new ArrayList<>();
- ListMultimap realValue = (ListMultimap) value;
- for (OptionPriority priority : OptionPriority.values()) {
- // If there is no mapping for this key, this check avoids object creation (because
- // ListMultimap has to return a new object on get) and also an unnecessary addAll call.
- if (realValue.containsKey(priority)) {
- result.addAll(realValue.get(priority));
- }
- }
- return result;
- }
- return value;
- }
-
- /**
- * @return the priority of the thing that set this value for this flag
- */
- public OptionPriority getPriority() {
- return priority;
- }
-
- /**
- * @return the thing that set this value for this flag
- */
- public String getSource() {
- return source;
- }
-
- public String getImplicitDependant() {
- return implicitDependant;
- }
-
- public boolean isImplicitDependency() {
- return implicitDependant != null;
- }
-
- public String getExpansionParent() {
- return expandedFrom;
- }
-
- public boolean isExpansion() {
- return expandedFrom != null;
- }
-
- public boolean getAllowMultiple() {
- return allowMultiple;
- }
-
- @Override
- public String toString() {
- StringBuilder result = new StringBuilder();
- result.append("option '").append(name).append("' ");
- result.append("set to '").append(value).append("' ");
- result.append("with priority ").append(priority);
- if (source != null) {
- result.append(" and source '").append(source).append("'");
- }
- if (implicitDependant != null) {
- result.append(" implicitly by ");
- }
- return result.toString();
- }
-
- // Need to suppress unchecked warnings, because the "multiple occurrence"
- // options use unchecked ListMultimaps due to limitations of Java generics.
- @SuppressWarnings({"unchecked", "rawtypes"})
- void addValue(OptionPriority addedPriority, Object addedValue) {
- Preconditions.checkState(allowMultiple);
- ListMultimap optionValueList = (ListMultimap) value;
- if (addedValue instanceof List<?>) {
- optionValueList.putAll(addedPriority, (List<?>) addedValue);
- } else {
- optionValueList.put(addedPriority, addedValue);
- }
- }
- }
-
- /**
- * The name and unparsed value of an option with additional metadata describing its
- * priority, source, whether it was set via an implicit dependency, and if so,
- * by which other option.
- *
- * <p>Note that the unparsed value and the source parameters can both be null.
- */
- public static class UnparsedOptionValueDescription {
- private final String name;
- private final OptionDefinition optionDefinition;
- private final String unparsedValue;
- private final OptionPriority priority;
- private final String source;
- private final boolean explicit;
-
- public UnparsedOptionValueDescription(
- String name,
- OptionDefinition optionDefinition,
- String unparsedValue,
- OptionPriority priority,
- String source,
- boolean explicit) {
- this.name = name;
- this.optionDefinition = optionDefinition;
- this.unparsedValue = unparsedValue;
- this.priority = priority;
- this.source = source;
- this.explicit = explicit;
- }
-
- public String getName() {
- return name;
- }
-
- OptionDefinition getOptionDefinition() {
- return optionDefinition;
- }
-
- public boolean isBooleanOption() {
- return optionDefinition.getType().equals(boolean.class);
- }
-
- private OptionDocumentationCategory documentationCategory() {
- return optionDefinition.getDocumentationCategory();
- }
-
- private ImmutableList<OptionMetadataTag> metadataTags() {
- return ImmutableList.copyOf(optionDefinition.getOptionMetadataTags());
- }
-
- public boolean isDocumented() {
- return documentationCategory() != OptionDocumentationCategory.UNDOCUMENTED && !isHidden();
- }
-
- public boolean isHidden() {
- ImmutableList<OptionMetadataTag> tags = metadataTags();
- return tags.contains(OptionMetadataTag.HIDDEN) || tags.contains(OptionMetadataTag.INTERNAL);
- }
-
- boolean isExpansion() {
- return optionDefinition.isExpansionOption();
- }
-
- boolean isImplicitRequirement() {
- return optionDefinition.getImplicitRequirements().length > 0;
- }
-
- boolean allowMultiple() {
- return optionDefinition.allowsMultiple();
- }
-
- public String getUnparsedValue() {
- return unparsedValue;
- }
-
- OptionPriority getPriority() {
- return priority;
- }
-
- public String getSource() {
- return source;
- }
-
- public boolean isExplicit() {
- return explicit;
- }
-
- @Override
- public String toString() {
- StringBuilder result = new StringBuilder();
- result.append("option '").append(name).append("' ");
- result.append("set to '").append(unparsedValue).append("' ");
- result.append("with priority ").append(priority);
- if (source != null) {
- result.append(" and source '").append(source).append("'");
- }
- return result.toString();
- }
- }
-
- /**
* The verbosity with which option help messages are displayed: short (just
* the name), medium (name, type, default, abbreviation), and long (full
* description).
@@ -519,7 +298,7 @@ public class OptionsParser implements OptionsProvider {
if (!data.getOptionsClasses().isEmpty()) {
List<OptionDefinition> allFields = new ArrayList<>();
for (Class<? extends OptionsBase> optionsClass : data.getOptionsClasses()) {
- allFields.addAll(data.getOptionDefinitionsFromClass(optionsClass));
+ allFields.addAll(OptionsData.getAllOptionDefinitionsForClass(optionsClass));
}
Collections.sort(allFields, OptionDefinition.BY_CATEGORY);
String prevCategory = null;
@@ -563,7 +342,7 @@ public class OptionsParser implements OptionsProvider {
if (!data.getOptionsClasses().isEmpty()) {
List<OptionDefinition> allFields = new ArrayList<>();
for (Class<? extends OptionsBase> optionsClass : data.getOptionsClasses()) {
- allFields.addAll(data.getOptionDefinitionsFromClass(optionsClass));
+ allFields.addAll(OptionsData.getAllOptionDefinitionsForClass(optionsClass));
}
Collections.sort(allFields, OptionDefinition.BY_CATEGORY);
String prevCategory = null;
@@ -620,7 +399,7 @@ public class OptionsParser implements OptionsProvider {
data.getOptionsClasses()
// List all options
.stream()
- .flatMap(optionsClass -> data.getOptionDefinitionsFromClass(optionsClass).stream())
+ .flatMap(optionsClass -> OptionsData.getAllOptionDefinitionsForClass(optionsClass).stream())
// Sort field for deterministic ordering
.sorted(OptionDefinition.BY_OPTION_NAME)
.filter(predicate)
@@ -633,20 +412,22 @@ public class OptionsParser implements OptionsProvider {
* @return The {@link OptionDescription} for the option, or null if there is no option by the
* given name.
*/
- OptionDescription getOptionDescription(String name) throws OptionsParsingException {
- return impl.getOptionDescription(name);
+ OptionDescription getOptionDescription(String name, OptionPriority priority, String source)
+ throws OptionsParsingException {
+ return impl.getOptionDescription(name, priority, source);
}
/**
- * Returns a description of the options values that get expanded from this flag with the given
- * flag value.
+ * Returns a description of the options values that get expanded from this option with the given
+ * value.
*
- * @return The {@link ImmutableList<OptionValueDescription>} for the option, or null if there is
- * no option by the given name.
+ * @return The {@link com.google.devtools.common.options.OptionValueDescription>} for the option,
+ * or null if there is no option by the given name.
*/
- ImmutableList<OptionValueDescription> getExpansionOptionValueDescriptions(
- String flagName, @Nullable String flagValue) throws OptionsParsingException {
- return impl.getExpansionOptionValueDescriptions(flagName, flagValue);
+ ImmutableList<ParsedOptionDescription> getExpansionOptionValueDescriptions(
+ OptionDefinition option, @Nullable String optionValue, OptionPriority priority, String source)
+ throws OptionsParsingException {
+ return impl.getExpansionOptionValueDescriptions(option, optionValue, priority, source);
}
/**
@@ -655,8 +436,8 @@ public class OptionsParser implements OptionsProvider {
* of type {@link List}, the description will correspond to any one of the calls, but not
* necessarily the last.
*
- * @return The {@link OptionValueDescription} for the option, or null if the value has not been
- * set.
+ * @return The {@link com.google.devtools.common.options.OptionValueDescription} for the option,
+ * or null if the value has not been set.
* @throws IllegalArgumentException if there is no option by the given name.
*/
OptionValueDescription getOptionValueDescription(String name) {
@@ -695,15 +476,14 @@ public class OptionsParser implements OptionsProvider {
}
/**
- * Parses {@code args}, using the classes registered with this parser.
- * {@link #getOptions(Class)} and {@link #getResidue()} return the results. May be called
- * multiple times; later options override existing ones if they have equal or higher priority.
- * The source of options is given as a function that maps option names to the source of the
- * option. Strings that cannot be parsed as options accumulates as* residue, if this parser
- * allows it.
+ * Parses {@code args}, using the classes registered with this parser. {@link #getOptions(Class)}
+ * and {@link #getResidue()} return the results. May be called multiple times; later options
+ * override existing ones if they have equal or higher priority. The source of options is given as
+ * a function that maps option names to the source of the option. Strings that cannot be parsed as
+ * options accumulates as* residue, if this parser allows it.
*/
- public void parseWithSourceFunction(OptionPriority priority,
- Function<? super String, String> sourceFunction, List<String> args)
+ public void parseWithSourceFunction(
+ OptionPriority priority, Function<OptionDefinition, String> sourceFunction, List<String> args)
throws OptionsParsingException {
Preconditions.checkNotNull(priority);
Preconditions.checkArgument(priority != OptionPriority.DEFAULT);
@@ -720,13 +500,12 @@ public class OptionsParser implements OptionsProvider {
* <p>This will not affect options objects that have already been retrieved from this parser
* through {@link #getOptions(Class)}.
*
- * @param optionName The full name of the option to clear.
- * @return A map of an option name to the old value of the options that were cleared.
+ * @param option The option to clear.
+ * @return The old value of the option that was cleared.
* @throws IllegalArgumentException If the flag does not exist.
*/
- public OptionValueDescription clearValue(String optionName)
- throws OptionsParsingException {
- return impl.clearValue(optionName);
+ public OptionValueDescription clearValue(OptionDefinition option) throws OptionsParsingException {
+ return impl.clearValue(option);
}
@Override
@@ -752,12 +531,12 @@ public class OptionsParser implements OptionsProvider {
}
@Override
- public List<UnparsedOptionValueDescription> asListOfUnparsedOptions() {
- return impl.asListOfUnparsedOptions();
+ public List<ParsedOptionDescription> asCompleteListOfParsedOptions() {
+ return impl.asCompleteListOfParsedOptions();
}
@Override
- public List<UnparsedOptionValueDescription> asListOfExplicitOptions() {
+ public List<ParsedOptionDescription> asListOfExplicitOptions() {
return impl.asListOfExplicitOptions();
}
@@ -772,9 +551,9 @@ public class OptionsParser implements OptionsProvider {
}
/** Returns all options fields of the given options class, in alphabetic order. */
- public static Collection<OptionDefinition> getFields(Class<? extends OptionsBase> optionsClass) {
- OptionsData data = OptionsParser.getOptionsDataInternal(optionsClass);
- return data.getOptionDefinitionsFromClass(optionsClass);
+ public static ImmutableList<OptionDefinition> getOptionDefinitions(
+ Class<? extends OptionsBase> optionsClass) {
+ return OptionsData.getAllOptionDefinitionsForClass(optionsClass);
}
/**
@@ -803,10 +582,10 @@ public class OptionsParser implements OptionsProvider {
* @throws IllegalArgumentException if {@code options} is not an instance of {@link OptionsBase}
*/
public static <O extends OptionsBase> Map<Field, Object> toMap(Class<O> optionsClass, O options) {
- OptionsData data = getOptionsDataInternal(optionsClass);
- // Alphabetized due to getOptionDefinitionsFromClass()'s order.
+ // Alphabetized due to getAllOptionDefinitionsForClass()'s order.
Map<Field, Object> map = new LinkedHashMap<>();
- for (OptionDefinition optionDefinition : data.getOptionDefinitionsFromClass(optionsClass)) {
+ for (OptionDefinition optionDefinition :
+ OptionsData.getAllOptionDefinitionsForClass(optionsClass)) {
try {
// Get the object value of the optionDefinition and place in map.
map.put(optionDefinition.getField(), optionDefinition.getField().get(options));
@@ -843,9 +622,10 @@ public class OptionsParser implements OptionsProvider {
throw new IllegalStateException("Error while instantiating options class", e);
}
- List<OptionDefinition> optionDefinitions = data.getOptionDefinitionsFromClass(optionsClass);
+ List<OptionDefinition> optionDefinitions =
+ OptionsData.getAllOptionDefinitionsForClass(optionsClass);
// Ensure all fields are covered, no extraneous fields.
- validateFieldsSets(data, optionsClass, new LinkedHashSet<Field>(map.keySet()));
+ validateFieldsSets(optionsClass, new LinkedHashSet<Field>(map.keySet()));
// Populate the instance.
for (OptionDefinition optionDefinition : optionDefinitions) {
// Non-null as per above check.
@@ -868,16 +648,12 @@ public class OptionsParser implements OptionsProvider {
* Option} annotation.
*/
private static void validateFieldsSets(
- OptionsData data,
Class<? extends OptionsBase> optionsClass,
LinkedHashSet<Field> fieldsFromMap) {
- ImmutableList<OptionDefinition> optionFieldsFromClasses =
- data.getOptionDefinitionsFromClass(optionsClass);
+ ImmutableList<OptionDefinition> optionDefsFromClasses =
+ OptionsData.getAllOptionDefinitionsForClass(optionsClass);
Set<Field> fieldsFromClass =
- optionFieldsFromClasses
- .stream()
- .map(optionField -> optionField.getField())
- .collect(Collectors.toSet());
+ optionDefsFromClasses.stream().map(OptionDefinition::getField).collect(Collectors.toSet());
if (fieldsFromClass.equals(fieldsFromMap)) {
// They are already equal, avoid additional checks.
@@ -886,7 +662,7 @@ public class OptionsParser implements OptionsProvider {
List<String> extraNamesFromClass = new ArrayList<>();
List<String> extraNamesFromMap = new ArrayList<>();
- for (OptionDefinition optionDefinition : optionFieldsFromClasses) {
+ for (OptionDefinition optionDefinition : optionDefsFromClasses) {
if (!fieldsFromMap.contains(optionDefinition.getField())) {
extraNamesFromClass.add("'" + optionDefinition.getOptionName() + "'");
}
@@ -897,9 +673,10 @@ public class OptionsParser implements OptionsProvider {
if (field == null) {
extraNamesFromMap.add("<null field>");
} else {
-
OptionDefinition optionDefinition = null;
try {
+ // TODO(ccalvarin) This shouldn't be necessary, no option definitions should be found in
+ // this optionsClass that weren't in the cache.
optionDefinition = OptionDefinition.extractOptionDefinition(field);
extraNamesFromMap.add("'" + optionDefinition.getOptionName() + "'");
} catch (NotAnOptionException e) {
diff --git a/java/com/google/devtools/common/options/OptionsParserImpl.java b/java/com/google/devtools/common/options/OptionsParserImpl.java
index 28aeb22..176d51e 100644
--- a/java/com/google/devtools/common/options/OptionsParserImpl.java
+++ b/java/com/google/devtools/common/options/OptionsParserImpl.java
@@ -19,14 +19,11 @@ import static java.util.stream.Collectors.toCollection;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
-import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterators;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.Multimap;
import com.google.devtools.common.options.OptionsParser.OptionDescription;
-import com.google.devtools.common.options.OptionsParser.OptionValueDescription;
-import com.google.devtools.common.options.OptionsParser.UnparsedOptionValueDescription;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.Arrays;
@@ -48,7 +45,9 @@ class OptionsParserImpl {
private final OptionsData optionsData;
/**
- * We store the results of parsing the arguments in here. It'll look like
+ * We store the results of option parsing in here - since there can only be one value per option
+ * field, this is where the different instances of an option have been combined and the final
+ * value is tracked. It'll look like
*
* <pre>
* OptionDefinition("--host") -> "www.google.com"
@@ -57,24 +56,26 @@ class OptionsParserImpl {
*
* This map is modified by repeated calls to {@link #parse(OptionPriority,Function,List)}.
*/
- private final Map<OptionDefinition, OptionValueDescription> parsedValues = new HashMap<>();
+ private final Map<OptionDefinition, OptionValueDescription> optionValues = new HashMap<>();
/**
- * We store the pre-parsed, explicit options for each priority in here.
- * We use partially preparsed options, which can be different from the original
- * representation, e.g. "--nofoo" becomes "--foo=0".
+ * Explicit option tracking, tracking each option as it was provided, after they have been parsed.
+ *
+ * <p>The value is unconverted, still the string as it was read from the input, or partially
+ * altered in cases where the flag was set by non {@code --flag=value} forms; e.g. {@code --nofoo}
+ * becomes {@code --foo=0}.
*/
- private final List<UnparsedOptionValueDescription> unparsedValues = new ArrayList<>();
+ private final List<ParsedOptionDescription> parsedOptions = new ArrayList<>();
/**
- * Unparsed values for use with the canonicalize command are stored separately from unparsedValues
- * so that invocation policy can modify the values for canonicalization (e.g. override
- * user-specified values with default values) without corrupting the data used to represent the
- * user's original invocation for {@link #asListOfExplicitOptions()} and {@link
- * #asListOfUnparsedOptions()}. A LinkedHashMultimap is used so that canonicalization happens in
- * the correct order and multiple values can be stored for flags that allow multiple values.
+ * The options for use with the canonicalize command are stored separately from parsedOptions so
+ * that invocation policy can modify the values for canonicalization (e.g. override user-specified
+ * values with default values) without corrupting the data used to represent the user's original
+ * invocation for {@link #asListOfExplicitOptions()} and {@link #asCompleteListOfParsedOptions()}.
+ * A LinkedHashMultimap is used so that canonicalization happens in the correct order and multiple
+ * values can be stored for flags that allow multiple values.
*/
- private final Multimap<OptionDefinition, UnparsedOptionValueDescription> canonicalizeValues =
+ private final Multimap<OptionDefinition, ParsedOptionDescription> canonicalizeValues =
LinkedHashMultimap.create();
private final List<String> warnings = new ArrayList<>();
@@ -113,28 +114,24 @@ class OptionsParserImpl {
this.argsPreProcessor = Preconditions.checkNotNull(preProcessor);
}
- /**
- * Implements {@link OptionsParser#asListOfUnparsedOptions()}.
- */
- List<UnparsedOptionValueDescription> asListOfUnparsedOptions() {
- return unparsedValues
+ /** Implements {@link OptionsParser#asCompleteListOfParsedOptions()}. */
+ List<ParsedOptionDescription> asCompleteListOfParsedOptions() {
+ return parsedOptions
.stream()
// It is vital that this sort is stable so that options on the same priority are not
// reordered.
- .sorted(comparing(UnparsedOptionValueDescription::getPriority))
+ .sorted(comparing(ParsedOptionDescription::getPriority))
.collect(toCollection(ArrayList::new));
}
- /**
- * Implements {@link OptionsParser#asListOfExplicitOptions()}.
- */
- List<UnparsedOptionValueDescription> asListOfExplicitOptions() {
- return unparsedValues
+ /** Implements {@link OptionsParser#asListOfExplicitOptions()}. */
+ List<ParsedOptionDescription> asListOfExplicitOptions() {
+ return parsedOptions
.stream()
- .filter(UnparsedOptionValueDescription::isExplicit)
+ .filter(ParsedOptionDescription::isExplicit)
// It is vital that this sort is stable so that options on the same priority are not
// reordered.
- .sorted(comparing(UnparsedOptionValueDescription::getPriority))
+ .sorted(comparing(ParsedOptionDescription::getPriority))
.collect(toCollection(ArrayList::new));
}
@@ -149,17 +146,24 @@ class OptionsParserImpl {
// the other options alphabetically.
.sorted(
(v1, v2) -> {
- if (v1.isImplicitRequirement()) {
- return v2.isImplicitRequirement() ? 0 : 1;
+ if (v1.getOptionDefinition().hasImplicitRequirements()) {
+ return v2.getOptionDefinition().hasImplicitRequirements() ? 0 : 1;
}
- if (v2.isImplicitRequirement()) {
+ if (v2.getOptionDefinition().hasImplicitRequirements()) {
return -1;
}
- return v1.getName().compareTo(v2.getName());
+ return v1.getOptionDefinition()
+ .getOptionName()
+ .compareTo(v2.getOptionDefinition().getOptionName());
})
// Ignore expansion options.
- .filter(value -> !value.isExpansion())
- .map(value -> "--" + value.getName() + "=" + value.getUnparsedValue())
+ .filter(value -> !value.getOptionDefinition().isExpansionOption())
+ .map(
+ value ->
+ "--"
+ + value.getOptionDefinition().getOptionName()
+ + "="
+ + value.getUnconvertedValue())
.collect(toCollection(ArrayList::new));
}
@@ -168,24 +172,13 @@ class OptionsParserImpl {
*/
List<OptionValueDescription> asListOfEffectiveOptions() {
List<OptionValueDescription> result = new ArrayList<>();
- for (Map.Entry<String, OptionDefinition> mapEntry : optionsData.getAllNamedFields()) {
- String fieldName = mapEntry.getKey();
+ for (Map.Entry<String, OptionDefinition> mapEntry : optionsData.getAllOptionDefinitions()) {
OptionDefinition optionDefinition = mapEntry.getValue();
- OptionValueDescription entry = parsedValues.get(optionDefinition);
- if (entry == null) {
- Object value = optionDefinition.getDefaultValue();
- result.add(
- new OptionValueDescription(
- fieldName,
- /*originalValueString=*/ null,
- value,
- OptionPriority.DEFAULT,
- /*source=*/ null,
- /*implicitDependant=*/ null,
- /*expandedFrom=*/ null,
- false));
+ OptionValueDescription optionValue = optionValues.get(optionDefinition);
+ if (optionValue == null) {
+ result.add(OptionValueDescription.getDefaultOptionValue(optionDefinition));
} else {
- result.add(entry);
+ result.add(optionValue);
}
}
return result;
@@ -204,124 +197,25 @@ class OptionsParserImpl {
+ (warning.isEmpty() ? "" : ": " + warning));
}
- // Warnings should not end with a '.' because the internal reporter adds one automatically.
- private void setValue(
- OptionDefinition optionDefinition,
- String name,
- Object value,
- OptionPriority priority,
- String source,
- String implicitDependant,
- String expandedFrom) {
- OptionValueDescription entry = parsedValues.get(optionDefinition);
- if (entry != null) {
- // Override existing option if the new value has higher or equal priority.
- if (priority.compareTo(entry.getPriority()) >= 0) {
- // Output warnings:
- if ((implicitDependant != null) && (entry.getImplicitDependant() != null)) {
- if (!implicitDependant.equals(entry.getImplicitDependant())) {
- warnings.add(
- "Option '"
- + name
- + "' is implicitly defined by both option '"
- + entry.getImplicitDependant()
- + "' and option '"
- + implicitDependant
- + "'");
- }
- } else if ((implicitDependant != null) && priority.equals(entry.getPriority())) {
- warnings.add(
- "Option '"
- + name
- + "' is implicitly defined by option '"
- + implicitDependant
- + "'; the implicitly set value overrides the previous one");
- } else if (entry.getImplicitDependant() != null) {
- warnings.add(
- "A new value for option '"
- + name
- + "' overrides a previous implicit setting of that option by option '"
- + entry.getImplicitDependant()
- + "'");
- } else if ((priority == entry.getPriority())
- && ((entry.getExpansionParent() == null) && (expandedFrom != null))) {
- // Create a warning if an expansion option overrides an explicit option:
- warnings.add("The option '" + expandedFrom + "' was expanded and now overrides a "
- + "previous explicitly specified option '" + name + "'");
- } else if ((entry.getExpansionParent() != null) && (expandedFrom != null)) {
- warnings.add(
- "The option '"
- + name
- + "' was expanded to from both options '"
- + entry.getExpansionParent()
- + "' and '"
- + expandedFrom
- + "'");
- }
-
- // Record the new value:
- parsedValues.put(
- optionDefinition,
- new OptionValueDescription(
- name, null, value, priority, source, implicitDependant, expandedFrom, false));
- }
- } else {
- parsedValues.put(
- optionDefinition,
- new OptionValueDescription(
- name, null, value, priority, source, implicitDependant, expandedFrom, false));
- maybeAddDeprecationWarning(optionDefinition);
- }
- }
-
- private void addListValue(
- OptionDefinition optionDefinition,
- String originalName,
- Object value,
- OptionPriority priority,
- String source,
- String implicitDependant,
- String expandedFrom) {
- OptionValueDescription entry = parsedValues.get(optionDefinition);
- if (entry == null) {
- entry =
- new OptionValueDescription(
- originalName,
- /* originalValueString */ null,
- ArrayListMultimap.create(),
- priority,
- source,
- implicitDependant,
- expandedFrom,
- true);
- parsedValues.put(optionDefinition, entry);
- maybeAddDeprecationWarning(optionDefinition);
- }
- entry.addValue(priority, value);
- }
- OptionValueDescription clearValue(String optionName)
+ OptionValueDescription clearValue(OptionDefinition optionDefinition)
throws OptionsParsingException {
- OptionDefinition optionDefinition = optionsData.getFieldFromName(optionName);
- if (optionDefinition == null) {
- throw new IllegalArgumentException("No such option '" + optionName + "'");
- }
-
// Actually remove the value from various lists tracking effective options.
canonicalizeValues.removeAll(optionDefinition);
- return parsedValues.remove(optionDefinition);
+ return optionValues.remove(optionDefinition);
}
OptionValueDescription getOptionValueDescription(String name) {
- OptionDefinition optionDefinition = optionsData.getFieldFromName(name);
+ OptionDefinition optionDefinition = optionsData.getOptionDefinitionFromName(name);
if (optionDefinition == null) {
throw new IllegalArgumentException("No such option '" + name + "'");
}
- return parsedValues.get(optionDefinition);
+ return optionValues.get(optionDefinition);
}
- OptionDescription getOptionDescription(String name) throws OptionsParsingException {
- OptionDefinition optionDefinition = optionsData.getFieldFromName(name);
+ OptionDescription getOptionDescription(String name, OptionPriority priority, String source)
+ throws OptionsParsingException {
+ OptionDefinition optionDefinition = optionsData.getOptionDefinitionFromName(name);
if (optionDefinition == null) {
return null;
}
@@ -329,106 +223,110 @@ class OptionsParserImpl {
return new OptionDescription(
optionDefinition,
optionsData.getExpansionDataForField(optionDefinition),
- getImplicitDependantDescriptions(
- ImmutableList.copyOf(optionDefinition.getImplicitRequirements()), name));
+ getImplicitDependentDescriptions(
+ ImmutableList.copyOf(optionDefinition.getImplicitRequirements()),
+ optionDefinition,
+ priority,
+ source));
}
- /**
- * @return A list of the descriptions corresponding to the implicit dependant flags passed in.
- * These descriptions are are divorced from the command line - there is no correct priority or
- * source for these, as they are not actually set values. The value itself is also a string,
- * no conversion has taken place.
- */
- private ImmutableList<OptionValueDescription> getImplicitDependantDescriptions(
- ImmutableList<String> options, String implicitDependant) throws OptionsParsingException {
- ImmutableList.Builder<OptionValueDescription> builder = ImmutableList.builder();
+ /** @return A list of the descriptions corresponding to the implicit dependent flags passed in. */
+ private ImmutableList<ParsedOptionDescription> getImplicitDependentDescriptions(
+ ImmutableList<String> options,
+ OptionDefinition implicitDependent,
+ OptionPriority priority,
+ String source)
+ throws OptionsParsingException {
+ ImmutableList.Builder<ParsedOptionDescription> builder = ImmutableList.builder();
Iterator<String> optionsIterator = options.iterator();
+ Function<OptionDefinition, String> sourceFunction =
+ o ->
+ String.format(
+ "implicitely required for option %s (source: %s)",
+ implicitDependent.getOptionName(), source);
while (optionsIterator.hasNext()) {
String unparsedFlagExpression = optionsIterator.next();
- ParseOptionResult parseResult = parseOption(unparsedFlagExpression, optionsIterator);
- builder.add(
- new OptionValueDescription(
- parseResult.optionDefinition.getOptionName(),
- parseResult.value,
- /* value */ null,
- /* priority */ null,
- /* source */ null,
- implicitDependant,
- /* expendedFrom */ null,
- parseResult.optionDefinition.allowsMultiple()));
+ ParsedOptionDescription parsedOption =
+ identifyOptionAndPossibleArgument(
+ unparsedFlagExpression,
+ optionsIterator,
+ priority,
+ sourceFunction,
+ implicitDependent,
+ null);
+ builder.add(parsedOption);
}
return builder.build();
}
/**
* @return A list of the descriptions corresponding to options expanded from the flag for the
- * given value. These descriptions are are divorced from the command line - there is no
- * correct priority or source for these, as they are not actually set values. The value itself
- * is also a string, no conversion has taken place.
+ * given value. The value itself is a string, no conversion has taken place.
*/
- ImmutableList<OptionValueDescription> getExpansionOptionValueDescriptions(
- String flagName, @Nullable String flagValue) throws OptionsParsingException {
- ImmutableList.Builder<OptionValueDescription> builder = ImmutableList.builder();
- OptionDefinition optionDefinition = optionsData.getFieldFromName(flagName);
+ ImmutableList<ParsedOptionDescription> getExpansionOptionValueDescriptions(
+ OptionDefinition expansionFlag,
+ @Nullable String flagValue,
+ OptionPriority priority,
+ String source)
+ throws OptionsParsingException {
+ ImmutableList.Builder<ParsedOptionDescription> builder = ImmutableList.builder();
- ImmutableList<String> options = optionsData.getEvaluatedExpansion(optionDefinition, flagValue);
+ ImmutableList<String> options = optionsData.getEvaluatedExpansion(expansionFlag, flagValue);
Iterator<String> optionsIterator = options.iterator();
-
+ Function<OptionDefinition, String> sourceFunction =
+ o -> String.format("expanded from %s (source: %s)", expansionFlag.getOptionName(), source);
while (optionsIterator.hasNext()) {
String unparsedFlagExpression = optionsIterator.next();
- ParseOptionResult parseResult = parseOption(unparsedFlagExpression, optionsIterator);
- builder.add(
- new OptionValueDescription(
- parseResult.optionDefinition.getOptionName(),
- parseResult.value,
- /* value */ null,
- /* priority */ null,
- /* source */ null,
- /* implicitDependant */ null,
- flagName,
- parseResult.optionDefinition.allowsMultiple()));
+ ParsedOptionDescription parsedOption =
+ identifyOptionAndPossibleArgument(
+ unparsedFlagExpression,
+ optionsIterator,
+ priority,
+ sourceFunction,
+ null,
+ expansionFlag);
+ builder.add(parsedOption);
}
return builder.build();
}
boolean containsExplicitOption(String name) {
- OptionDefinition optionDefinition = optionsData.getFieldFromName(name);
+ OptionDefinition optionDefinition = optionsData.getOptionDefinitionFromName(name);
if (optionDefinition == null) {
throw new IllegalArgumentException("No such option '" + name + "'");
}
- return parsedValues.get(optionDefinition) != null;
+ return optionValues.get(optionDefinition) != null;
}
/**
- * Parses the args, and returns what it doesn't parse. May be called multiple
- * times, and may be called recursively. In each call, there may be no
- * duplicates, but separate calls may contain intersecting sets of options; in
- * that case, the arg seen last takes precedence.
+ * Parses the args, and returns what it doesn't parse. May be called multiple times, and may be
+ * called recursively. In each call, there may be no duplicates, but separate calls may contain
+ * intersecting sets of options; in that case, the arg seen last takes precedence.
*/
- List<String> parse(OptionPriority priority, Function<? super String, String> sourceFunction,
- List<String> args) throws OptionsParsingException {
+ List<String> parse(
+ OptionPriority priority, Function<OptionDefinition, String> sourceFunction, List<String> args)
+ throws OptionsParsingException {
return parse(priority, sourceFunction, null, null, args);
}
/**
- * Parses the args, and returns what it doesn't parse. May be called multiple
- * times, and may be called recursively. Calls may contain intersecting sets
- * of options; in that case, the arg seen last takes precedence.
+ * Parses the args, and returns what it doesn't parse. May be called multiple times, and may be
+ * called recursively. Calls may contain intersecting sets of options; in that case, the arg seen
+ * last takes precedence.
*
- * <p>The method uses the invariant that if an option has neither an implicit
- * dependent nor an expanded from value, then it must have been explicitly
- * set.
+ * <p>The method uses the invariant that if an option has neither an implicit dependent nor an
+ * expanded from value, then it must have been explicitly set.
*/
private List<String> parse(
OptionPriority priority,
- Function<? super String, String> sourceFunction,
- String implicitDependent,
- String expandedFrom,
- List<String> args) throws OptionsParsingException {
-
+ Function<OptionDefinition, String> sourceFunction,
+ OptionDefinition implicitDependent,
+ OptionDefinition expandedFrom,
+ List<String> args)
+ throws OptionsParsingException {
List<String> unparsedArgs = new ArrayList<>();
- LinkedHashMap<String, List<String>> implicitRequirements = new LinkedHashMap<>();
+ LinkedHashMap<OptionDefinition, List<String>> implicitRequirements = new LinkedHashMap<>();
Iterator<String> argsIterator = argsPreProcessor.preProcess(args).iterator();
while (argsIterator.hasNext()) {
@@ -444,22 +342,32 @@ class OptionsParserImpl {
break;
}
- ParseOptionResult parseOptionResult = parseOption(arg, argsIterator);
- OptionDefinition optionDefinition = parseOptionResult.optionDefinition;
- @Nullable String value = parseOptionResult.value;
+ ParsedOptionDescription parsedOption =
+ identifyOptionAndPossibleArgument(
+ arg, argsIterator, priority, sourceFunction, implicitDependent, expandedFrom);
+ OptionDefinition optionDefinition = parsedOption.getOptionDefinition();
+ // All options can be deprecated; check and warn before doing any option-type specific work.
+ maybeAddDeprecationWarning(optionDefinition);
- final String originalName = optionDefinition.getOptionName();
+ // Track the value, before any remaining option-type specific work that is done outside of
+ // the OptionValueDescription.
+ OptionValueDescription entry =
+ optionValues.computeIfAbsent(
+ optionDefinition, OptionValueDescription::createOptionValueDescription);
+ entry.addOptionInstance(parsedOption, warnings);
+ @Nullable String unconvertedValue = parsedOption.getUnconvertedValue();
if (optionDefinition.isWrapperOption()) {
- if (value.startsWith("-")) {
- String sourceMessage = "Unwrapped from wrapper option --" + originalName;
+ if (unconvertedValue.startsWith("-")) {
+ String sourceMessage =
+ "Unwrapped from wrapper option --" + optionDefinition.getOptionName();
List<String> unparsed =
parse(
priority,
o -> sourceMessage,
null, // implicitDependent
null, // expandedFrom
- ImmutableList.of(value));
+ ImmutableList.of(unconvertedValue));
if (!unparsed.isEmpty()) {
throw new OptionsParsingException(
@@ -470,13 +378,19 @@ class OptionsParserImpl {
}
// Don't process implicitRequirements or expansions for wrapper options. In particular,
- // don't record this option in unparsedValues, so that only the wrapped option shows
+ // don't record this option in parsedOptions, so that only the wrapped option shows
// up in canonicalized options.
continue;
} else {
- throw new OptionsParsingException("Invalid --" + originalName + " value format. "
- + "You may have meant --" + originalName + "=--" + value);
+ throw new OptionsParsingException(
+ "Invalid --"
+ + optionDefinition.getOptionName()
+ + " value format. "
+ + "You may have meant --"
+ + optionDefinition.getOptionName()
+ + "=--"
+ + unconvertedValue);
}
}
@@ -484,104 +398,61 @@ class OptionsParserImpl {
// Log explicit options and expanded options in the order they are parsed (can be sorted
// later). Also remember whether they were expanded or not. This information is needed to
// correctly canonicalize flags.
- UnparsedOptionValueDescription unparsedOptionValueDescription =
- new UnparsedOptionValueDescription(
- originalName,
- optionDefinition,
- value,
- priority,
- sourceFunction.apply(originalName),
- expandedFrom == null);
- unparsedValues.add(unparsedOptionValueDescription);
+ parsedOptions.add(parsedOption);
if (optionDefinition.allowsMultiple()) {
- canonicalizeValues.put(optionDefinition, unparsedOptionValueDescription);
+ canonicalizeValues.put(optionDefinition, parsedOption);
} else {
- canonicalizeValues.replaceValues(
- optionDefinition, ImmutableList.of(unparsedOptionValueDescription));
+ canonicalizeValues.replaceValues(optionDefinition, ImmutableList.of(parsedOption));
}
}
// Handle expansion options.
if (optionDefinition.isExpansionOption()) {
ImmutableList<String> expansion =
- optionsData.getEvaluatedExpansion(optionDefinition, value);
-
- String sourceMessage = "expanded from option --"
- + originalName
- + " from "
- + sourceFunction.apply(originalName);
- Function<Object, String> expansionSourceFunction = o -> sourceMessage;
- maybeAddDeprecationWarning(optionDefinition);
+ optionsData.getEvaluatedExpansion(optionDefinition, unconvertedValue);
+
+ String sourceFunctionApplication = sourceFunction.apply(optionDefinition);
+ String sourceMessage =
+ (sourceFunctionApplication == null)
+ ? String.format("expanded from option --%s", optionDefinition.getOptionName())
+ : String.format(
+ "expanded from option --%s from %s",
+ optionDefinition.getOptionName(), sourceFunctionApplication);
+ Function<OptionDefinition, String> expansionSourceFunction = o -> sourceMessage;
List<String> unparsed =
- parse(priority, expansionSourceFunction, null, originalName, expansion);
+ parse(priority, expansionSourceFunction, null, optionDefinition, expansion);
if (!unparsed.isEmpty()) {
- // Throw an assertion, because this indicates an error in the code that specified the
- // expansion for the current option.
+ // Throw an assertion, because this indicates an error in the definition of this
+ // option's expansion, not with the input as provided by the user.
throw new AssertionError(
"Unparsed options remain after parsing expansion of "
+ arg
+ ": "
+ Joiner.on(' ').join(unparsed));
}
- } else {
- Converter<?> converter = optionDefinition.getConverter();
- Object convertedValue;
- try {
- convertedValue = converter.convert(value);
- } catch (OptionsParsingException e) {
- // The converter doesn't know the option name, so we supply it here by
- // re-throwing:
- throw new OptionsParsingException("While parsing option " + arg
- + ": " + e.getMessage(), e);
- }
-
- // ...but allow duplicates of single-use options across separate calls to
- // parse(); latest wins:
- if (!optionDefinition.allowsMultiple()) {
- setValue(
- optionDefinition,
- originalName,
- convertedValue,
- priority,
- sourceFunction.apply(originalName),
- implicitDependent,
- expandedFrom);
- } else {
- // But if it's a multiple-use option, then just accumulate the
- // values, in the order in which they were seen.
- // Note: The type of the list member is not known; Java introspection
- // only makes it available in String form via the signature string
- // for the field declaration.
- addListValue(
- optionDefinition,
- originalName,
- convertedValue,
- priority,
- sourceFunction.apply(originalName),
- implicitDependent,
- expandedFrom);
- }
}
// Collect any implicit requirements.
- if (optionDefinition.getImplicitRequirements().length > 0) {
+ if (optionDefinition.hasImplicitRequirements()) {
implicitRequirements.put(
- optionDefinition.getOptionName(),
- Arrays.asList(optionDefinition.getImplicitRequirements()));
+ optionDefinition, Arrays.asList(optionDefinition.getImplicitRequirements()));
}
}
// Now parse any implicit requirements that were collected.
// TODO(bazel-team): this should happen when the option is encountered.
if (!implicitRequirements.isEmpty()) {
- for (Map.Entry<String, List<String>> entry : implicitRequirements.entrySet()) {
+ for (Map.Entry<OptionDefinition, List<String>> entry : implicitRequirements.entrySet()) {
+ OptionDefinition optionDefinition = entry.getKey();
+ String sourceFunctionApplication = sourceFunction.apply(optionDefinition);
String sourceMessage =
- "implicit requirement of option --"
- + entry.getKey()
- + " from "
- + sourceFunction.apply(entry.getKey());
- Function<Object, String> requirementSourceFunction =
- o -> sourceMessage;
+ (sourceFunctionApplication == null)
+ ? String.format(
+ "implicit requirement of option --%s", optionDefinition.getOptionName())
+ : String.format(
+ "implicit requirement of option --%s from %s",
+ optionDefinition.getOptionName(), sourceFunctionApplication);
+ Function<OptionDefinition, String> requirementSourceFunction = o -> sourceMessage;
List<String> unparsed = parse(priority, requirementSourceFunction, entry.getKey(), null,
entry.getValue());
@@ -594,23 +465,29 @@ class OptionsParserImpl {
}
}
- return unparsedArgs;
- }
-
- private static final class ParseOptionResult {
- final OptionDefinition optionDefinition;
- @Nullable final String value;
-
- ParseOptionResult(OptionDefinition optionDefinition, @Nullable String value) {
- this.optionDefinition = optionDefinition;
- this.value = value;
+ // Go through the final values and make sure they are valid values for their option. Unlike any
+ // checks that happened above, this also checks that flags that were not set have a valid
+ // default value. getValue() will throw if the value is invalid.
+ for (OptionValueDescription valueDescription : asListOfEffectiveOptions()) {
+ valueDescription.getValue();
}
+
+ return unparsedArgs;
}
- private ParseOptionResult parseOption(String arg, Iterator<String> nextArgs)
+ private ParsedOptionDescription identifyOptionAndPossibleArgument(
+ String arg,
+ Iterator<String> nextArgs,
+ OptionPriority priority,
+ Function<OptionDefinition, String> sourceFunction,
+ OptionDefinition implicitDependent,
+ OptionDefinition expandedFrom)
throws OptionsParsingException {
- String value = null;
+ // Store the way this option was parsed on the command line.
+ StringBuilder commandLineForm = new StringBuilder();
+ commandLineForm.append(arg);
+ String unconvertedValue = null;
OptionDefinition optionDefinition;
boolean booleanValue = true;
@@ -632,26 +509,26 @@ class OptionsParserImpl {
if (name.trim().isEmpty()) {
throw new OptionsParsingException("Invalid options syntax: " + arg, arg);
}
- value = equalsAt == -1 ? null : arg.substring(equalsAt + 1);
- optionDefinition = optionsData.getFieldFromName(name);
+ unconvertedValue = equalsAt == -1 ? null : arg.substring(equalsAt + 1);
+ optionDefinition = optionsData.getOptionDefinitionFromName(name);
// Look for a "no"-prefixed option name: "no<optionName>".
if (optionDefinition == null && name.startsWith("no")) {
name = name.substring(2);
- optionDefinition = optionsData.getFieldFromName(name);
+ optionDefinition = optionsData.getOptionDefinitionFromName(name);
booleanValue = false;
if (optionDefinition != null) {
// TODO(bazel-team): Add tests for these cases.
- if (!optionDefinition.isBooleanField()) {
+ if (!optionDefinition.usesBooleanValueSyntax()) {
throw new OptionsParsingException(
"Illegal use of 'no' prefix on non-boolean option: " + arg, arg);
}
- if (value != null) {
+ if (unconvertedValue != null) {
throw new OptionsParsingException(
"Unexpected value after boolean option: " + arg, arg);
}
// "no<optionname>" signifies a boolean option w/ false value
- value = "0";
+ unconvertedValue = "0";
}
}
} else {
@@ -665,21 +542,28 @@ class OptionsParserImpl {
throw new OptionsParsingException("Unrecognized option: " + arg, arg);
}
- if (value == null) {
+ if (unconvertedValue == null) {
// Special-case boolean to supply value based on presence of "no" prefix.
- if (optionDefinition.isBooleanField()) {
- value = booleanValue ? "1" : "0";
+ 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 (nextArgs.hasNext()) {
- value = nextArgs.next(); // "--flag value" form
+ // "--flag value" form
+ unconvertedValue = nextArgs.next();
+ commandLineForm.append(" ").append(unconvertedValue);
} else {
throw new OptionsParsingException("Expected value after " + arg);
}
}
- return new ParseOptionResult(optionDefinition, value);
+ return new ParsedOptionDescription(
+ optionDefinition,
+ commandLineForm.toString(),
+ unconvertedValue,
+ new OptionInstanceOrigin(
+ priority, sourceFunction.apply(optionDefinition), implicitDependent, expandedFrom));
}
/**
@@ -700,18 +584,27 @@ class OptionsParserImpl {
// Set the fields
for (OptionDefinition optionDefinition :
- optionsData.getOptionDefinitionsFromClass(optionsClass)) {
+ OptionsData.getAllOptionDefinitionsForClass(optionsClass)) {
Object value;
- OptionValueDescription entry = parsedValues.get(optionDefinition);
- if (entry == null) {
+ OptionValueDescription optionValue = optionValues.get(optionDefinition);
+ if (optionValue == null) {
value = optionDefinition.getDefaultValue();
} else {
- value = entry.getValue();
+ value = optionValue.getValue();
}
try {
optionDefinition.getField().set(optionsInstance, value);
+ } catch (IllegalArgumentException e) {
+ throw new IllegalStateException(
+ String.format(
+ "Unable to set option '%s' to value '%s'.",
+ optionDefinition.getOptionName(), value),
+ e);
} catch (IllegalAccessException e) {
- throw new IllegalStateException(e);
+ throw new IllegalStateException(
+ "Could not set the field due to access issues. This is impossible, as the "
+ + "OptionProcessor checks that all options are non-final public fields.",
+ e);
}
}
return optionsInstance;
diff --git a/java/com/google/devtools/common/options/OptionsProvider.java b/java/com/google/devtools/common/options/OptionsProvider.java
index 040aa05..1c7737f 100644
--- a/java/com/google/devtools/common/options/OptionsProvider.java
+++ b/java/com/google/devtools/common/options/OptionsProvider.java
@@ -14,9 +14,6 @@
package com.google.devtools.common.options;
-import com.google.devtools.common.options.OptionsParser.OptionValueDescription;
-import com.google.devtools.common.options.OptionsParser.UnparsedOptionValueDescription;
-
import java.util.List;
/**
@@ -37,27 +34,25 @@ public interface OptionsProvider extends OptionsClassProvider {
boolean containsExplicitOption(String string);
/**
- * Returns a mutable copy of the list of all options that were specified
- * either explicitly or implicitly. These options are sorted by priority, and
- * by the order in which they were specified. If an option was specified
- * multiple times, it is included in the result multiple times. Does not
- * include the residue.
+ * Returns a mutable copy of the list of all options that were specified either explicitly or
+ * implicitly. These options are sorted by priority, and by the order in which they were
+ * specified. If an option was specified multiple times, it is included in the result multiple
+ * times. Does not include the residue.
*
- * <p>The returned list can be filtered if undocumented, hidden or implicit
- * options should not be displayed.
+ * <p>The returned list can be filtered if undocumented, hidden or implicit options should not be
+ * displayed.
*/
- List<UnparsedOptionValueDescription> asListOfUnparsedOptions();
+ List<ParsedOptionDescription> asCompleteListOfParsedOptions();
/**
- * Returns a list of all explicitly specified options, suitable for logging
- * or for displaying back to the user. These options are sorted by priority,
- * and by the order in which they were specified. If an option was
- * explicitly specified multiple times, it is included in the result
+ * Returns a list of all explicitly specified options, suitable for logging or for displaying back
+ * to the user. These options are sorted by priority, and by the order in which they were
+ * specified. If an option was explicitly specified multiple times, it is included in the result
* multiple times. Does not include the residue.
*
* <p>The list includes undocumented options.
*/
- List<UnparsedOptionValueDescription> asListOfExplicitOptions();
+ List<ParsedOptionDescription> asListOfExplicitOptions();
/**
* Returns a list of all options, including undocumented ones, and their
diff --git a/java/com/google/devtools/common/options/OptionsUsage.java b/java/com/google/devtools/common/options/OptionsUsage.java
index 0ab30da..68a460e 100644
--- a/java/com/google/devtools/common/options/OptionsUsage.java
+++ b/java/com/google/devtools/common/options/OptionsUsage.java
@@ -39,7 +39,7 @@ class OptionsUsage {
static void getUsage(Class<? extends OptionsBase> optionsClass, StringBuilder usage) {
OptionsData data = OptionsParser.getOptionsDataInternal(optionsClass);
List<OptionDefinition> optionDefinitions =
- new ArrayList<>(data.getOptionDefinitionsFromClass(optionsClass));
+ new ArrayList<>(OptionsData.getAllOptionDefinitionsForClass(optionsClass));
optionDefinitions.sort(OptionDefinition.BY_OPTION_NAME);
for (OptionDefinition optionDefinition : optionDefinitions) {
getUsage(optionDefinition, usage, OptionsParser.HelpVerbosity.LONG, data);
@@ -146,7 +146,7 @@ class OptionsUsage {
usage.append(paragraphFill(expandsMsg.toString(), /*indent=*/ 6, /*width=*/ 80));
usage.append('\n');
}
- if (optionDefinition.getImplicitRequirements().length > 0) {
+ if (optionDefinition.hasImplicitRequirements()) {
StringBuilder requiredMsg = new StringBuilder("Using this option will also add: ");
for (String req : optionDefinition.getImplicitRequirements()) {
requiredMsg.append(req).append(" ");
@@ -168,7 +168,7 @@ class OptionsUsage {
String typeDescription = getTypeDescription(optionDefinition);
usage.append("<dt><code><a name=\"flag--").append(plainFlagName).append("\"></a>--");
usage.append(flagName);
- if (optionDefinition.isBooleanField() || optionDefinition.isVoidField()) {
+ if (optionDefinition.usesBooleanValueSyntax() || optionDefinition.isVoidField()) {
// Nothing for boolean, tristate, boolean_or_enum, or void options.
} else if (!valueDescription.isEmpty()) {
usage.append("=").append(escaper.escape(valueDescription));
@@ -285,6 +285,6 @@ class OptionsUsage {
static String getFlagName(OptionDefinition optionDefinition) {
String name = optionDefinition.getOptionName();
- return optionDefinition.isBooleanField() ? "[no]" + name : name;
+ return optionDefinition.usesBooleanValueSyntax() ? "[no]" + name : name;
}
}
diff --git a/java/com/google/devtools/common/options/ParamsFilePreProcessor.java b/java/com/google/devtools/common/options/ParamsFilePreProcessor.java
index 791265a..87f87f6 100644
--- a/java/com/google/devtools/common/options/ParamsFilePreProcessor.java
+++ b/java/com/google/devtools/common/options/ParamsFilePreProcessor.java
@@ -14,14 +14,9 @@
package com.google.devtools.common.options;
import java.io.IOException;
-import java.io.Reader;
-import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystem;
-import java.nio.file.Files;
import java.nio.file.Path;
-import java.util.ArrayList;
import java.util.List;
-import java.util.NoSuchElementException;
/**
* Defines an {@link ArgsPreProcessor} that will determine if the arguments list contains a "params"
@@ -31,7 +26,7 @@ import java.util.NoSuchElementException;
* length. A params file argument is defined as a path starting with @. It will also be the only
* entry in an argument list.
*/
-public class ParamsFilePreProcessor implements ArgsPreProcessor {
+public abstract class ParamsFilePreProcessor implements ArgsPreProcessor {
static final String ERROR_MESSAGE_FORMAT = "Error reading params file: %s %s";
@@ -50,7 +45,7 @@ public class ParamsFilePreProcessor implements ArgsPreProcessor {
* Parses the param file path and replaces the arguments list with the contents if one exists.
*
* @param args A list of arguments that may contain @&lt;path&gt; to a params file.
- * @return A list of areguments suitable for parsing.
+ * @return A list of arguments suitable for parsing.
* @throws OptionsParsingException if the path does not exist.
*/
@Override
@@ -61,29 +56,8 @@ public class ParamsFilePreProcessor implements ArgsPreProcessor {
String.format(TOO_MANY_ARGS_ERROR_MESSAGE_FORMAT, args), args.get(0));
}
Path path = fs.getPath(args.get(0).substring(1));
- try (Reader params = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
- List<String> newArgs = new ArrayList<>();
- StringBuilder arg = new StringBuilder();
- CharIterator iterator = CharIterator.wrap(params);
- while (iterator.hasNext()) {
- char next = iterator.next();
- if (Character.isWhitespace(next) && !iterator.isInQuote() && !iterator.isEscaped()) {
- newArgs.add(unescape(arg.toString()));
- arg = new StringBuilder();
- } else {
- arg.append(next);
- }
- }
- // If there is an arg in the buffer, add it.
- if (arg.length() > 0) {
- newArgs.add(arg.toString());
- }
- // If we're still in a quote by the end of the file, throw an error.
- if (iterator.isInQuote()) {
- throw new OptionsParsingException(
- String.format(ERROR_MESSAGE_FORMAT, path, iterator.getUnmatchedQuoteMessage()));
- }
- return newArgs;
+ try {
+ return parse(path);
} catch (RuntimeException | IOException e) {
throw new OptionsParsingException(
String.format(ERROR_MESSAGE_FORMAT, path, e.getMessage()), args.get(0), e);
@@ -92,86 +66,15 @@ public class ParamsFilePreProcessor implements ArgsPreProcessor {
return args;
}
- private String unescape(String arg) {
- if (arg.startsWith("'") && arg.endsWith("'")) {
- String unescaped = arg.replace("'\\''", "'");
- return unescaped.substring(1, unescaped.length() - 1);
- }
- return arg;
- }
-
- // Doesn't implement iterator to avoid autoboxing and to throw exceptions.
- static class CharIterator {
-
- private final Reader reader;
- private int readerPosition = 0;
- private int singleQuoteStart = -1;
- private int doubleQuoteStart = -1;
- private boolean escaped = false;
- private char lastChar = (char) -1;
-
- public static CharIterator wrap(Reader reader) {
- return new CharIterator(reader);
- }
-
- public CharIterator(Reader reader) {
- this.reader = reader;
- }
-
- public boolean hasNext() throws IOException {
- return peek() != -1;
- }
-
- private int peek() throws IOException {
- reader.mark(1);
- int next = reader.read();
- reader.reset();
- return next;
- }
-
- public boolean isInQuote() {
- return singleQuoteStart != -1 || doubleQuoteStart != -1;
- }
-
- public boolean isEscaped() {
- return escaped;
- }
-
- public String getUnmatchedQuoteMessage() {
- StringBuilder message = new StringBuilder();
- if (singleQuoteStart != -1) {
- message.append(String.format(UNFINISHED_QUOTE_MESSAGE_FORMAT, "'", singleQuoteStart));
- }
- if (doubleQuoteStart != -1) {
- message.append(String.format(UNFINISHED_QUOTE_MESSAGE_FORMAT, "\"", doubleQuoteStart));
- }
- return message.toString();
- }
-
- public char next() throws IOException {
- if (!hasNext()) {
- throw new NoSuchElementException();
- }
- char current = (char) reader.read();
-
- // check for \r\n line endings. If found, drop the \r for normalized parsing.
- if (current == '\r' && peek() == '\n') {
- current = (char) reader.read();
- }
-
- // check to see if the current position is escaped
- escaped = (lastChar == '\\');
-
- if (!escaped && current == '\'') {
- singleQuoteStart = singleQuoteStart == -1 ? readerPosition : -1;
- }
- if (!escaped && current == '"') {
- doubleQuoteStart = doubleQuoteStart == -1 ? readerPosition : -1;
- }
-
- readerPosition++;
- lastChar = current;
- return current;
- }
- }
+ /**
+ * Parses the paramsFile and returns a list of argument tokens to be further processed by the
+ * {@link OptionsParser}.
+ *
+ * @param paramsFile The path of the params file to parse.
+ * @return a list of argument tokens.
+ * @throws IOException if there is an error reading paramsFile.
+ * @throws OptionsParsingException if there is an error reading paramsFile.
+ */
+ protected abstract List<String> parse(Path paramsFile)
+ throws IOException, OptionsParsingException;
}
diff --git a/java/com/google/devtools/common/options/ParsedOptionDescription.java b/java/com/google/devtools/common/options/ParsedOptionDescription.java
new file mode 100644
index 0000000..0910579
--- /dev/null
+++ b/java/com/google/devtools/common/options/ParsedOptionDescription.java
@@ -0,0 +1,120 @@
+// Copyright 2017 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.common.options;
+
+import com.google.common.collect.ImmutableList;
+import javax.annotation.Nullable;
+
+/**
+ * The representation of a parsed option instance.
+ *
+ * <p>An option instance is distinct from the final value of an option, as multiple instances
+ * provide values may be overridden or combined in some way.
+ */
+public final class ParsedOptionDescription {
+
+ private final OptionDefinition optionDefinition;
+ private final String commandLineForm;
+ @Nullable private final String unconvertedValue;
+ private final OptionInstanceOrigin origin;
+
+ public ParsedOptionDescription(
+ OptionDefinition optionDefinition,
+ String commandLineForm,
+ @Nullable String unconvertedValue,
+ OptionInstanceOrigin origin) {
+ this.optionDefinition = optionDefinition;
+ this.commandLineForm = commandLineForm;
+ this.unconvertedValue = unconvertedValue;
+ this.origin = origin;
+ }
+
+ public OptionDefinition getOptionDefinition() {
+ return optionDefinition;
+ }
+
+ public String getCommandLineForm() {
+ return commandLineForm;
+ }
+
+ public boolean isBooleanOption() {
+ return optionDefinition.getType().equals(boolean.class);
+ }
+
+ private OptionDocumentationCategory documentationCategory() {
+ return optionDefinition.getDocumentationCategory();
+ }
+
+ private ImmutableList<OptionMetadataTag> metadataTags() {
+ return ImmutableList.copyOf(optionDefinition.getOptionMetadataTags());
+ }
+
+ public boolean isDocumented() {
+ return documentationCategory() != OptionDocumentationCategory.UNDOCUMENTED && !isHidden();
+ }
+
+ public boolean isHidden() {
+ ImmutableList<OptionMetadataTag> tags = metadataTags();
+ return tags.contains(OptionMetadataTag.HIDDEN) || tags.contains(OptionMetadataTag.INTERNAL);
+ }
+
+ public String getUnconvertedValue() {
+ return unconvertedValue;
+ }
+
+ OptionPriority getPriority() {
+ return origin.getPriority();
+ }
+
+ public String getSource() {
+ return origin.getSource();
+ }
+
+ OptionDefinition getImplicitDependent() {
+ return origin.getImplicitDependent();
+ }
+
+ OptionDefinition getExpandedFrom() {
+ return origin.getExpandedFrom();
+ }
+
+ public boolean isExplicit() {
+ return origin.getExpandedFrom() == null && origin.getImplicitDependent() == null;
+ }
+
+ public Object getConvertedValue() throws OptionsParsingException {
+ Converter<?> converter = optionDefinition.getConverter();
+ try {
+ return converter.convert(unconvertedValue);
+ } catch (OptionsParsingException e) {
+ // The converter doesn't know the option name, so we supply it here by re-throwing:
+ throw new OptionsParsingException(
+ String.format("While parsing option %s: %s", commandLineForm, e.getMessage()), e);
+ }
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder result = new StringBuilder();
+ result.append("option '").append(optionDefinition.getOptionName()).append("' ");
+ 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("'");
+ }
+ return result.toString();
+ }
+
+}
diff --git a/java/com/google/devtools/common/options/ShellQuotedParamsFilePreProcessor.java b/java/com/google/devtools/common/options/ShellQuotedParamsFilePreProcessor.java
new file mode 100644
index 0000000..525cb77
--- /dev/null
+++ b/java/com/google/devtools/common/options/ShellQuotedParamsFilePreProcessor.java
@@ -0,0 +1,138 @@
+// Copyright 2017 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.common.options;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import java.io.IOException;
+import java.io.PushbackReader;
+import java.io.Reader;
+import java.nio.file.FileSystem;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A {@link ParamsFilePreProcessor} that processes a parameter file using the {@code
+ * com.google.devtools.build.lib.actions.ParameterFile.ParameterFileType.SHELL_QUOTED} format. This
+ * format assumes each parameter is separated by whitespace and is quoted using singe quotes
+ * ({@code '}) if it contains any special characters or is an empty string.
+ */
+public class ShellQuotedParamsFilePreProcessor extends ParamsFilePreProcessor {
+
+ public ShellQuotedParamsFilePreProcessor(FileSystem fs) {
+ super(fs);
+ }
+
+ @Override
+ protected List<String> parse(Path paramsFile) throws IOException {
+ List<String> args = new ArrayList<>();
+ try (ShellQuotedReader reader =
+ new ShellQuotedReader(Files.newBufferedReader(paramsFile, UTF_8))) {
+ String arg;
+ while ((arg = reader.readArg()) != null) {
+ args.add(arg);
+ }
+ }
+ return args;
+ }
+
+ private static class ShellQuotedReader implements AutoCloseable {
+
+ private final PushbackReader reader;
+ private int position = -1;
+
+ public ShellQuotedReader(Reader reader) {
+ this.reader = new PushbackReader(reader, 10);
+ }
+
+ private char read() throws IOException {
+ int value = reader.read();
+ position++;
+ return (char) value;
+ }
+
+ private void unread(char value) throws IOException {
+ reader.unread(value);
+ position--;
+ }
+
+ private boolean hasNext() throws IOException {
+ char value = read();
+ boolean hasNext = value != (char) -1;
+ unread(value);
+ return hasNext;
+ }
+
+ @Override
+ public void close() throws IOException {
+ reader.close();
+ }
+
+ public String readArg() throws IOException {
+ if (!hasNext()) {
+ return null;
+ }
+
+ StringBuilder arg = new StringBuilder();
+
+ int quoteStart = -1;
+ boolean quoted = false;
+ char current;
+
+ while ((current = read()) != (char) -1) {
+ if (quoted) {
+ if (current == '\'') {
+ StringBuilder escapedQuoteRemainder =
+ new StringBuilder().append(read()).append(read()).append(read());
+ if (escapedQuoteRemainder.toString().equals("\\''")) {
+ arg.append("'");
+ } else {
+ for (char c : escapedQuoteRemainder.reverse().toString().toCharArray()) {
+ unread(c);
+ }
+ quoted = false;
+ quoteStart = -1;
+ }
+ } else {
+ arg.append(current);
+ }
+ } else {
+ if (current == '\'') {
+ quoted = true;
+ quoteStart = position;
+ } else if (current == '\r') {
+ char next = read();
+ if (next == '\n') {
+ return arg.toString();
+ } else {
+ unread(next);
+ return arg.toString();
+ }
+ } else if (Character.isWhitespace(current)) {
+ return arg.toString();
+ } else {
+ arg.append(current);
+ }
+ }
+ }
+ if (quoted) {
+ throw new IOException(
+ String.format(UNFINISHED_QUOTE_MESSAGE_FORMAT, "'", quoteStart));
+ }
+ return arg.toString();
+ }
+ }
+}
diff --git a/java/com/google/devtools/common/options/UnquotedParamsFilePreProcessor.java b/java/com/google/devtools/common/options/UnquotedParamsFilePreProcessor.java
new file mode 100644
index 0000000..a754a68
--- /dev/null
+++ b/java/com/google/devtools/common/options/UnquotedParamsFilePreProcessor.java
@@ -0,0 +1,40 @@
+// Copyright 2017 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.common.options;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import java.io.IOException;
+import java.nio.file.FileSystem;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+
+/**
+ * A {@link ParamsFilePreProcessor} that processes a parameter file using the {@code
+ * com.google.devtools.build.lib.actions.ParameterFile.ParameterFileType.UNQUOTED} format. This
+ * format assumes each parameter is on a separate line and does not perform any special handling on
+ * non-newline whitespace or special characters.
+ */
+public class UnquotedParamsFilePreProcessor extends ParamsFilePreProcessor {
+
+ public UnquotedParamsFilePreProcessor(FileSystem fs) {
+ super(fs);
+ }
+
+ @Override
+ protected List<String> parse(Path paramsFile) throws IOException {
+ return Files.readAllLines(paramsFile, UTF_8);
+ }
+}
diff --git a/java/com/google/devtools/common/options/processor/OptionProcessor.java b/java/com/google/devtools/common/options/processor/OptionProcessor.java
index 5afcc39..fd7c023 100644
--- a/java/com/google/devtools/common/options/processor/OptionProcessor.java
+++ b/java/com/google/devtools/common/options/processor/OptionProcessor.java
@@ -14,13 +14,23 @@
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;
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.OptionMetadataTag;
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.Set;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
@@ -28,11 +38,17 @@ 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;
import javax.lang.model.element.ElementKind;
+import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
+import javax.lang.model.type.DeclaredType;
+import javax.lang.model.type.ExecutableType;
+import javax.lang.model.type.PrimitiveType;
+import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
@@ -41,9 +57,21 @@ import javax.tools.Diagnostic;
/**
* Annotation processor for {@link Option}.
*
- * <p>The {@link OptionsParser} only accepts publicly declared options in {@link
- * OptionsBase}-inheriting classes, and there is no support for {@link Option} annotated fields
- * declared elsewhere or privately. Prevent such uses from compiling.
+ * <p>Checks the following invariants about {@link Option}-annotated fields ("options"):
+ * <ul>
+ * <li>The {@link OptionsParser} only accepts options in {@link OptionsBase}-inheriting classes
+ * <li>All options must be declared publicly and be neither static nor final.
+ * <li>All options that must be used on the command line must have sensible names without
+ * whitespace or other confusing characters, such as equal signs.
+ * <li>The type of the option must match the converter that will convert the unparsed string value
+ * into the option type. For options that do not specify a converter, check that there is a
+ * valid match in the {@link Converters#DEFAULT_CONVERTERS} list.
+ * <li>Options must list valid combinations of tags and documentation categories.
+ * <li>Expansion options and options with implicit requirements cannot expand in more than one way,
+ * how multiple expansions would interact is not defined and should not be necessary.
+ * </ul>
+ *
+ * <p>These properties can be relied upon at runtime without additional checks.
*/
@SupportedAnnotationTypes({"com.google.devtools.common.options.Option"})
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@@ -52,6 +80,8 @@ public final class OptionProcessor extends AbstractProcessor {
private Types typeUtils;
private Elements elementUtils;
private Messager messager;
+ private ImmutableMap<TypeMirror, Converter<?>> defaultConverters;
+ private ImmutableMap<Class<?>, PrimitiveType> primitiveTypeMap;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
@@ -59,15 +89,44 @@ public final class OptionProcessor extends AbstractProcessor {
typeUtils = processingEnv.getTypeUtils();
elementUtils = processingEnv.getElementUtils();
messager = processingEnv.getMessager();
- }
- private static class OptionProcessorException extends Exception {
- private Element elementInError;
+ // 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<>();
- OptionProcessorException(Element element, String message, Object... args) {
- super(String.format(message, args));
- elementInError = element;
+ // 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<>();
+ 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()) {
+ Class<?> converterClass = entry.getKey();
+ String typeName = converterClass.getCanonicalName();
+ TypeElement typeElement = elementUtils.getTypeElement(typeName);
+ // Check that we can get a type mirror, either through the type element or the primitive type.
+ if (typeElement != null) {
+ converterMapBuilder.put(typeElement.asType(), entry.getValue());
+ } else {
+ if (!primitiveTypeMap.containsKey(converterClass)) {
+ messager.printMessage(
+ Diagnostic.Kind.ERROR,
+ String.format("Can't get a TypeElement for Type %s", typeName));
+ continue;
+ }
+ // Add the primitive types to the map, both in primitive TypeMirror form, and the boxed
+ // classes, such as java.lang.Integer, because primitives must be boxed in collections,
+ // such as allowMultiple options, which have type List<singleOptionType>.
+ PrimitiveType primitiveType = primitiveTypeMap.get(converterClass);
+ converterMapBuilder.put(primitiveType, entry.getValue());
+ converterMapBuilder.put(typeUtils.boxedClass(primitiveType).asType(), entry.getValue());
+ }
}
+ defaultConverters = converterMapBuilder.build();
}
/** Check that the Option variables only occur in OptionBase-inheriting classes. */
@@ -107,6 +166,190 @@ public final class OptionProcessor extends AbstractProcessor {
}
}
+ private ImmutableList<TypeMirror> getAcceptedConverterReturnTypes(VariableElement optionField)
+ throws OptionProcessorException {
+ TypeMirror optionType = optionField.asType();
+ Option annotation = optionField.getAnnotation(Option.class);
+ TypeMirror listType = elementUtils.getTypeElement(List.class.getCanonicalName()).asType();
+ // Options that accumulate multiple mentions in an arglist must have type List<T>, where each
+ // individual mention has type T. Identify type T to use it for checking the converter's return
+ // type.
+ if (annotation.allowMultiple()) {
+ // Check that the option type is in fact a list.
+ if (optionType.getKind() != TypeKind.DECLARED) {
+ throw new OptionProcessorException(
+ optionField,
+ "Option that allows multiple occurrences must be of type %s, but is of type %s",
+ listType,
+ optionType);
+ }
+ DeclaredType optionDeclaredType = (DeclaredType) optionType;
+ // optionDeclaredType.asElement().asType() gets us from List<actualType> to List<E>, so this
+ // is unfortunately necessary.
+ if (!typeUtils.isAssignable(optionDeclaredType.asElement().asType(), listType)) {
+ throw new OptionProcessorException(
+ optionField,
+ "Option that allows multiple occurrences must be of type %s, but is of type %s",
+ listType,
+ optionType);
+ }
+
+ // Check that there is only one generic parameter, and store it as the singular option type.
+ List<? extends TypeMirror> genericParameters = optionDeclaredType.getTypeArguments();
+ if (genericParameters.size() != 1) {
+ throw new OptionProcessorException(
+ optionField,
+ "Option that allows multiple occurrences must be of type %s, "
+ + "where E is the type of an individual command-line mention of this option, "
+ + "but is of type %s",
+ listType,
+ optionType);
+ }
+
+ // For repeated options, we also accept cases where each option itself contains a list, which
+ // are then concatenated into the final single list type. For this reason, we will accept both
+ // converters that return the type of a single option, and List<singleOption>, which,
+ // incidentally, is the original optionType.
+ // Example: --foo=a,b,c --foo=d,e,f could have a final value of type List<Char>,
+ // value {a,b,c,e,d,f}, instead of requiring a final value of type List<List<Char>>
+ // value {{a,b,c},{d,e,f}}
+ TypeMirror singularOptionType = genericParameters.get(0);
+
+ return ImmutableList.of(singularOptionType, optionType);
+ } else {
+ return ImmutableList.of(optionField.asType());
+ }
+ }
+
+ private void checkForDefaultConverter(
+ VariableElement optionField,
+ List<TypeMirror> acceptedConverterReturnTypes,
+ String defaultValue)
+ throws OptionProcessorException {
+ for (TypeMirror acceptedConverterReturnType : acceptedConverterReturnTypes) {
+ Converter<?> converterInstance = defaultConverters.get(acceptedConverterReturnType);
+ if (converterInstance == null) {
+ // This return type isn't a match, move on to the next one in case.
+ continue;
+ }
+ TypeElement converter =
+ elementUtils.getTypeElement(converterInstance.getClass().getCanonicalName());
+ try {
+ // For the default converters, it so happens we have access to the convert methods
+ // at compile time, since we already have the OptionsParser source. Take advantage of
+ // this to test that the provided defaultValue is valid.
+ converterInstance.convert(defaultValue);
+ } catch (OptionsParsingException e) {
+ throw new OptionProcessorException(
+ optionField,
+ /* throwable = */ e,
+ "Option lists a default value (%s) that is not parsable by the option's converter "
+ + "(s)",
+ defaultValue,
+ converter);
+ }
+ return; // This one passes the test.
+ }
+
+ // We didn't find a default converter.
+ throw new OptionProcessorException(
+ optionField,
+ "Cannot find valid converter for option of type %s",
+ acceptedConverterReturnTypes.get(0));
+ }
+
+ private void checkProvidedConverter(
+ VariableElement optionField,
+ ImmutableList<TypeMirror> acceptedConverterReturnTypes,
+ TypeElement converterElement)
+ throws OptionProcessorException {
+ if (converterElement.getModifiers().contains(Modifier.ABSTRACT)) {
+ throw new OptionProcessorException(
+ optionField, "The converter type %s must be a concrete type", converterElement.asType());
+ }
+
+ DeclaredType converterType = (DeclaredType) converterElement.asType();
+
+ // Unfortunately, for provided classes, we do not have access to the compiled convert
+ // method at this time, and cannot check that the default value is parseable. We will
+ // instead check that T of Converter<T> matches the option's type, but this is all we can
+ // do.
+ List<ExecutableElement> methodList =
+ elementUtils
+ .getAllMembers(converterElement)
+ .stream()
+ .filter(element -> element.getKind() == ElementKind.METHOD)
+ .map(methodElement -> (ExecutableElement) methodElement)
+ .filter(methodElement -> methodElement.getSimpleName().contentEquals("convert"))
+ .filter(
+ methodElement ->
+ methodElement.getParameters().size() == 1
+ && typeUtils.isSameType(
+ methodElement.getParameters().get(0).asType(),
+ elementUtils.getTypeElement(String.class.getCanonicalName()).asType()))
+ .collect(Collectors.toList());
+ // Check that there is just the one method
+ if (methodList.size() != 1) {
+ throw new OptionProcessorException(
+ optionField,
+ "Converter %s has methods 'convert(String)': %s",
+ converterElement,
+ methodList.stream().map(Object::toString).collect(Collectors.joining(", ")));
+ }
+
+ ExecutableType convertMethodType =
+ (ExecutableType) typeUtils.asMemberOf(converterType, methodList.get(0));
+ TypeMirror convertMethodResultType = convertMethodType.getReturnType();
+ // Check that the converter's return type is in the accepted list.
+ for (TypeMirror acceptedConverterReturnType : acceptedConverterReturnTypes) {
+ if (typeUtils.isAssignable(convertMethodResultType, acceptedConverterReturnType)) {
+ return; // This one passes the test.
+ }
+ }
+ throw new OptionProcessorException(
+ optionField,
+ "Type of field (%s) must be assignable from the converter's return type (%s)",
+ acceptedConverterReturnTypes.get(0),
+ convertMethodResultType);
+ }
+
+ private void checkConverter(VariableElement optionField) throws OptionProcessorException {
+ TypeMirror optionType = optionField.asType();
+ Option annotation = optionField.getAnnotation(Option.class);
+ ImmutableList<TypeMirror> acceptedConverterReturnTypes =
+ getAcceptedConverterReturnTypes(optionField);
+
+ // For simple, static expansions, don't accept non-Void types.
+ if (annotation.expansion().length != 0
+ && !typeUtils.isSameType(
+ optionType, elementUtils.getTypeElement(Void.class.getCanonicalName()).asType())) {
+ throw new OptionProcessorException(
+ optionField,
+ "Option is an expansion flag with a static expansion, but does not have Void type.");
+ }
+
+ // Obtain the converter for this option.
+ AnnotationMirror optionMirror =
+ ProcessorUtils.getAnnotation(elementUtils, typeUtils, optionField, Option.class);
+ TypeElement defaultConverterElement =
+ elementUtils.getTypeElement(Converter.class.getCanonicalName());
+ TypeElement converterElement =
+ ProcessorUtils.getClassTypeFromAnnotationField(elementUtils, optionMirror, "converter");
+ if (converterElement == null) {
+ throw new OptionProcessorException(optionField, "Null converter found.");
+ }
+
+ if (typeUtils.isSameType(converterElement.asType(), defaultConverterElement.asType())) {
+ // Find a matching converter in the default converter list, and check that it successfully
+ // parses the default value for this option.
+ checkForDefaultConverter(
+ optionField, acceptedConverterReturnTypes, annotation.defaultValue());
+ } else {
+ // Check that the provided converter has an accepted return type.
+ checkProvidedConverter(optionField, acceptedConverterReturnTypes, converterElement);
+ }
+ }
+
/**
* Check that the option lists at least one effect, and that no nonsensical combinations are
* listed, such as having a known effect listed with UNKNOWN.
@@ -162,6 +405,113 @@ public final class OptionProcessor extends AbstractProcessor {
}
}
+ /** These categories used to indicate whether a flag was documented, but no longer. */
+ private static final ImmutableList<String> DEPRECATED_CATEGORIES =
+ ImmutableList.of("undocumented", "hidden", "internal");
+
+ private void checkOldCategoriesAreNotUsed(VariableElement optionField)
+ throws OptionProcessorException {
+ Option annotation = optionField.getAnnotation(Option.class);
+ if (DEPRECATED_CATEGORIES.contains(annotation.category())) {
+ throw new OptionProcessorException(
+ optionField,
+ "Documentation level is no longer read from the option category. Category \""
+ + annotation.category()
+ + "\" is disallowed, see OptionMetadataTags for the relevant tags.");
+ }
+ }
+
+ private void checkOptionName(VariableElement optionField) throws OptionProcessorException {
+ Option annotation = optionField.getAnnotation(Option.class);
+ String optionName = annotation.name();
+ if (optionName.isEmpty()) {
+ throw new OptionProcessorException(optionField, "Option must have an actual name.");
+ }
+
+ // Specifically for non-internal options, which are flags intended to be used on the command
+ // line, check that there are no weird characters or whitespace.
+ if (!ImmutableList.copyOf(annotation.metadataTags()).contains(OptionMetadataTag.INTERNAL)) {
+ if (!Pattern.matches("([\\w:-])*", optionName)) {
+ // Ideally, this would be just \w, but - and : are needed for legacy options. We can lie in
+ // the error though, no harm in encouraging good behavior.
+ throw new OptionProcessorException(
+ optionField,
+ "Options that are used on the command line as flags must have names made from word "
+ + "characters only.");
+ }
+ }
+ }
+
+ /**
+ * Some flags expand to other flags, either in place, or with "implicit requirements" that get
+ * added on top of the flag's value. Don't let these flags do too many crazy things, dealing with
+ * this is enough.
+ */
+ private void checkExpansionOptions(VariableElement optionField) throws OptionProcessorException {
+ Option annotation = optionField.getAnnotation(Option.class);
+ boolean isStaticExpansion = annotation.expansion().length > 0;
+ boolean hasImplicitRequirements = annotation.implicitRequirements().length > 0;
+
+ AnnotationMirror annotationMirror =
+ ProcessorUtils.getAnnotation(elementUtils, typeUtils, optionField, Option.class);
+ TypeElement expansionFunction =
+ ProcessorUtils.getClassTypeFromAnnotationField(
+ elementUtils, annotationMirror, "expansionFunction");
+ TypeElement defaultExpansionFunction =
+ elementUtils.getTypeElement(ExpansionFunction.class.getCanonicalName());
+ boolean isFunctionalExpansion =
+ !typeUtils.isSameType(expansionFunction.asType(), defaultExpansionFunction.asType());
+
+ if (isStaticExpansion && isFunctionalExpansion) {
+ throw new OptionProcessorException(
+ optionField,
+ "Options cannot expand using both a static expansion list and an expansion function.");
+ }
+ boolean isExpansion = isStaticExpansion || isFunctionalExpansion;
+
+ if (isExpansion && hasImplicitRequirements) {
+ throw new OptionProcessorException(
+ optionField,
+ "Can't set an option to be both an expansion option and have implicit requirements.");
+ }
+
+ 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,
+ "Can't set an option to accumulate multiple values and let it expand to other flags.");
+ }
+ }
+ }
+
+ /**
+ * 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)) {
@@ -172,10 +522,15 @@ public final class OptionProcessor extends AbstractProcessor {
checkModifiers(optionField);
checkInOptionBase(optionField);
+ checkOptionName(optionField);
+ checkOldCategoriesAreNotUsed(optionField);
+ checkExpansionOptions(optionField);
+ checkConverter(optionField);
checkEffectTagRationality(optionField);
checkMetadataTagAndCategoryRationality(optionField);
+ checkWrapperOptions(optionField);
} catch (OptionProcessorException e) {
- error(e.elementInError, e.getMessage());
+ error(e.getElementInError(), e.getMessage());
}
}
// Claim all Option annotated fields.
diff --git a/java/com/google/devtools/common/options/processor/OptionProcessorException.java b/java/com/google/devtools/common/options/processor/OptionProcessorException.java
new file mode 100644
index 0000000..0a35f4c
--- /dev/null
+++ b/java/com/google/devtools/common/options/processor/OptionProcessorException.java
@@ -0,0 +1,35 @@
+// Copyright 2017 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.common.options.processor;
+
+import javax.lang.model.element.Element;
+
+/** Exception that indicates a problem in the processing of an {@link Option}. */
+class OptionProcessorException extends Exception {
+ private final Element elementInError;
+
+ OptionProcessorException(Element element, String message, Object... args) {
+ super(String.format(message, args));
+ elementInError = element;
+ }
+
+ OptionProcessorException(Element element, Throwable throwable, String message, Object... args) {
+ super(String.format(message, args), throwable);
+ elementInError = element;
+ }
+
+ Element getElementInError() {
+ return elementInError;
+ }
+}
diff --git a/java/com/google/devtools/common/options/processor/ProcessorUtils.java b/java/com/google/devtools/common/options/processor/ProcessorUtils.java
new file mode 100644
index 0000000..cce8f18
--- /dev/null
+++ b/java/com/google/devtools/common/options/processor/ProcessorUtils.java
@@ -0,0 +1,98 @@
+// Copyright 2017 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.common.options.processor;
+
+import java.lang.annotation.Annotation;
+import java.util.Map;
+import javax.lang.model.element.AnnotationMirror;
+import javax.lang.model.element.AnnotationValue;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.type.DeclaredType;
+import javax.lang.model.type.TypeMirror;
+import javax.lang.model.util.Elements;
+import javax.lang.model.util.Types;
+
+/** Convenient utilities for dealing with the javax.lang.model types. */
+public class ProcessorUtils {
+
+ /** Return the AnnotationMirror for the annotation of the given type on the element provided. */
+ static AnnotationMirror getAnnotation(
+ Elements elementUtils,
+ Types typeUtils,
+ Element element,
+ Class<? extends Annotation> annotation)
+ throws OptionProcessorException {
+ TypeElement annotationElement = elementUtils.getTypeElement(annotation.getCanonicalName());
+ if (annotationElement == null) {
+ // This can happen if the annotation is on the -processorpath but not on the -classpath.
+ throw new OptionProcessorException(
+ element, "Unable to find the type of annotation %s.", annotation);
+ }
+ TypeMirror annotationMirror = annotationElement.asType();
+
+ for (AnnotationMirror annot : element.getAnnotationMirrors()) {
+ if (typeUtils.isSameType(annot.getAnnotationType(), annotationMirror)) {
+ return annot;
+ }
+ }
+ // No annotation of this requested type found.
+ throw new OptionProcessorException(
+ element, "No annotation %s found for this element.", annotation);
+ }
+
+ /**
+ * Returns the contents of a {@code Class}-typed field in an annotation.
+ *
+ * <p>Taken & adapted from AutoValueProcessor.java
+ *
+ * <p>This method is needed because directly reading the value of such a field from an
+ * AnnotationMirror throws:
+ *
+ * <pre>
+ * javax.lang.model.type.MirroredTypeException: Attempt to access Class object for TypeMirror Foo.
+ * </pre>
+ *
+ * @param annotation The annotation to read from.
+ * @param fieldName The name of the field to read, e.g. "exclude".
+ * @return a set of fully-qualified names of classes appearing in 'fieldName' on 'annotation' on
+ * 'element'.
+ */
+ static TypeElement getClassTypeFromAnnotationField(
+ Elements elementUtils, AnnotationMirror annotation, String fieldName)
+ throws OptionProcessorException {
+ for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> entry :
+ elementUtils.getElementValuesWithDefaults(annotation).entrySet()) {
+ if (entry.getKey().getSimpleName().contentEquals(fieldName)) {
+ Object annotationField = entry.getValue().getValue();
+ if (!(annotationField instanceof DeclaredType)) {
+ throw new IllegalStateException(
+ String.format(
+ "The fieldName provided should only apply to Class<> type annotation fields, "
+ + "but the field's value (%s) couldn't get cast to a DeclaredType",
+ entry));
+ }
+ String qualifiedName =
+ ((TypeElement) ((DeclaredType) annotationField).asElement())
+ .getQualifiedName()
+ .toString();
+ return elementUtils.getTypeElement(qualifiedName);
+ }
+ }
+ // Annotation missing the requested field.
+ throw new OptionProcessorException(
+ null, "No member %s of the %s annotation found for element.", fieldName, annotation);
+ }
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/Bug62456849TestDataGenerator.java b/test/java/com/google/devtools/build/android/desugar/Bug62456849TestDataGenerator.java
new file mode 100644
index 0000000..627b6c7
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/Bug62456849TestDataGenerator.java
@@ -0,0 +1,104 @@
+// Copyright 2017 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 com.google.common.collect.Iterators;
+import com.google.common.collect.UnmodifiableIterator;
+import com.google.common.io.ByteStreams;
+import java.io.BufferedOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+import java.util.zip.ZipOutputStream;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+
+/**
+ * Test data generator for b/62456849. This class converts methods satisfying the following
+ * conditions to synthetic methods.
+ * <li>The name starts with "lambda$"
+ * <li>Not synthetic
+ */
+public class Bug62456849TestDataGenerator {
+
+ public static void main(String[] args) throws IOException {
+ checkArgument(
+ args.length == 2,
+ "Usage: %s <input-jar> <output-jar>",
+ Bug62456849TestDataGenerator.class.getName());
+ Path inputJar = Paths.get(args[0]);
+ checkArgument(Files.isRegularFile(inputJar), "The input jar %s is not a file", inputJar);
+ Path outputJar = Paths.get(args[1]);
+
+ try (ZipFile inputZip = new ZipFile(inputJar.toFile());
+ ZipOutputStream outZip =
+ new ZipOutputStream(new BufferedOutputStream(Files.newOutputStream(outputJar)))) {
+ for (UnmodifiableIterator<? extends ZipEntry> it =
+ Iterators.forEnumeration(inputZip.entries());
+ it.hasNext(); ) {
+ ZipEntry entry = it.next();
+ String entryName = entry.getName();
+ byte[] content =
+ entryName.endsWith(".class")
+ ? convertClass(inputZip, entry)
+ : readEntry(inputZip, entry);
+ writeToZipFile(outZip, entryName, content);
+ }
+ }
+ }
+
+ private static void writeToZipFile(ZipOutputStream outZip, String entryName, byte[] content)
+ throws IOException {
+ ZipEntry result = new ZipEntry(entryName);
+ result.setTime(0L);
+ outZip.putNextEntry(result);
+ outZip.write(content);
+ outZip.closeEntry();
+ }
+
+ private static byte[] readEntry(ZipFile file, ZipEntry entry) throws IOException {
+ try (InputStream is = file.getInputStream(entry)) {
+ return ByteStreams.toByteArray(is);
+ }
+ }
+
+ private static byte[] convertClass(ZipFile file, ZipEntry entry) throws IOException {
+ try (InputStream content = file.getInputStream(entry)) {
+ ClassReader reader = new ClassReader(content);
+ ClassWriter writer = new ClassWriter(0);
+ ClassVisitor converter =
+ new ClassVisitor(Opcodes.ASM5, writer) {
+ @Override
+ public MethodVisitor visitMethod(
+ int access, String name, String desc, String signature, String[] exceptions) {
+ if (name.startsWith("lambda$") && (access & Opcodes.ACC_SYNTHETIC) == 0) {
+ access |= Opcodes.ACC_SYNTHETIC;
+ }
+ return super.visitMethod(access, name, desc, signature, exceptions);
+ }
+ };
+ reader.accept(converter, 0);
+ return writer.toByteArray();
+ }
+ }
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixerTest.java b/test/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixerTest.java
new file mode 100644
index 0000000..cdc3263
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixerTest.java
@@ -0,0 +1,234 @@
+// Copyright 2017 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.checkNotNull;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.devtools.build.android.desugar.DefaultMethodClassFixer.InterfaceComparator.INSTANCE;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.io.Closer;
+import com.google.devtools.build.android.desugar.Desugar.ThrowingClassLoader;
+import java.io.File;
+import java.io.IOException;
+import java.io.Serializable;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.List;
+import java.util.TreeSet;
+import java.util.concurrent.Callable;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.tree.AbstractInsnNode;
+import org.objectweb.asm.tree.InsnNode;
+import org.objectweb.asm.tree.MethodInsnNode;
+import org.objectweb.asm.tree.MethodNode;
+
+/** Unit Test for {@link DefaultMethodClassFixer} */
+@RunWith(JUnit4.class)
+public class DefaultMethodClassFixerTest {
+
+ private ClassReaderFactory classpathReader;
+ private ClassReaderFactory bootclassPath;
+ private ClassLoader classLoader;
+ private Closer closer;
+
+ @Before
+ public void setup() throws IOException {
+ closer = Closer.create();
+ CoreLibraryRewriter rewriter = new CoreLibraryRewriter("");
+
+ IndexedInputs indexedInputs =
+ toIndexedInputs(closer, System.getProperty("DefaultMethodClassFixerTest.input"));
+ IndexedInputs indexedClasspath =
+ toIndexedInputs(closer, System.getProperty("DefaultMethodClassFixerTest.classpath"));
+ IndexedInputs indexedBootclasspath =
+ toIndexedInputs(closer, System.getProperty("DefaultMethodClassFixerTest.bootclasspath"));
+
+ bootclassPath = new ClassReaderFactory(indexedBootclasspath, rewriter);
+ IndexedInputs indexedClasspathAndInputFiles = indexedClasspath.withParent(indexedInputs);
+ classpathReader = new ClassReaderFactory(indexedClasspathAndInputFiles, rewriter);
+ ClassLoader bootclassloader =
+ new HeaderClassLoader(indexedBootclasspath, rewriter, new ThrowingClassLoader());
+ classLoader = new HeaderClassLoader(indexedClasspathAndInputFiles, rewriter, bootclassloader);
+ }
+
+ @After
+ public void teardown() throws IOException {
+ closer.close();
+ }
+
+ private static IndexedInputs toIndexedInputs(Closer closer, String stringPathList)
+ throws IOException {
+ final List<Path> pathList = readPathListFromString(stringPathList);
+ return new IndexedInputs(Desugar.toRegisteredInputFileProvider(closer, pathList));
+ }
+
+ private static List<Path> readPathListFromString(String pathList) {
+ return Arrays.stream(checkNotNull(pathList).split(File.pathSeparator))
+ .map(Paths::get)
+ .collect(ImmutableList.toImmutableList());
+ }
+
+ private byte[] desugar(String classname) {
+ ClassReader reader = classpathReader.readIfKnown(classname);
+ return desugar(reader);
+ }
+
+ private byte[] desugar(ClassReader reader) {
+ ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
+ DefaultMethodClassFixer fixer =
+ new DefaultMethodClassFixer(writer, classpathReader, bootclassPath, classLoader);
+ reader.accept(fixer, 0);
+ return writer.toByteArray();
+ }
+
+ private byte[] desugar(byte[] classContent) {
+ ClassReader reader = new ClassReader(classContent);
+ return desugar(reader);
+ }
+
+ @Test
+ public void testDesugaringDirectImplementation() {
+ byte[] desugaredClass =
+ desugar(
+ ("com.google.devtools.build.android.desugar.testdata.java8."
+ + "DefaultInterfaceMethodWithStaticInitializer$TestInterfaceSetOne$C")
+ .replace('.', '/'));
+ checkClinitForDefaultInterfaceMethodWithStaticInitializerTestInterfaceSetOneC(desugaredClass);
+
+ byte[] desugaredClassAgain = desugar(desugaredClass);
+ checkClinitForDefaultInterfaceMethodWithStaticInitializerTestInterfaceSetOneC(
+ desugaredClassAgain);
+
+ desugaredClassAgain = desugar(desugaredClassAgain);
+ checkClinitForDefaultInterfaceMethodWithStaticInitializerTestInterfaceSetOneC(
+ desugar(desugaredClassAgain));
+ }
+
+ private void checkClinitForDefaultInterfaceMethodWithStaticInitializerTestInterfaceSetOneC(
+ byte[] classContent) {
+ ClassReader reader = new ClassReader(classContent);
+ reader.accept(
+ new ClassVisitor(Opcodes.ASM5) {
+
+ class ClinitMethod extends MethodNode {
+
+ public ClinitMethod(
+ int access, String name, String desc, String signature, String[] exceptions) {
+ super(Opcodes.ASM5, access, name, desc, signature, exceptions);
+ }
+ }
+
+ private ClinitMethod clinit;
+
+ @Override
+ public MethodVisitor visitMethod(
+ int access, String name, String desc, String signature, String[] exceptions) {
+ if ("<clinit>".equals(name)) {
+ assertThat(clinit).isNull();
+ clinit = new ClinitMethod(access, name, desc, signature, exceptions);
+ return clinit;
+ }
+ return super.visitMethod(access, name, desc, signature, exceptions);
+ }
+
+ @Override
+ public void visitEnd() {
+ assertThat(clinit).isNotNull();
+ assertThat(clinit.instructions.size()).isEqualTo(3);
+ AbstractInsnNode instruction = clinit.instructions.getFirst();
+ {
+ assertThat(instruction).isInstanceOf(MethodInsnNode.class);
+ MethodInsnNode field = (MethodInsnNode) instruction;
+ assertThat(field.owner)
+ .isEqualTo(
+ "com/google/devtools/build/android/desugar/testdata/java8/"
+ + "DefaultInterfaceMethodWithStaticInitializer"
+ + "$TestInterfaceSetOne$I1$$CC");
+ assertThat(field.name)
+ .isEqualTo(InterfaceDesugaring.COMPANION_METHOD_TO_TRIGGER_INTERFACE_CLINIT_NAME);
+ assertThat(field.desc)
+ .isEqualTo(InterfaceDesugaring.COMPANION_METHOD_TO_TRIGGER_INTERFACE_CLINIT_DESC);
+ }
+ {
+ instruction = instruction.getNext();
+ assertThat(instruction).isInstanceOf(MethodInsnNode.class);
+ MethodInsnNode field = (MethodInsnNode) instruction;
+ assertThat(field.owner)
+ .isEqualTo(
+ "com/google/devtools/build/android/desugar/testdata/java8/"
+ + "DefaultInterfaceMethodWithStaticInitializer"
+ + "$TestInterfaceSetOne$I2$$CC");
+ assertThat(field.name)
+ .isEqualTo(InterfaceDesugaring.COMPANION_METHOD_TO_TRIGGER_INTERFACE_CLINIT_NAME);
+ assertThat(field.desc)
+ .isEqualTo(InterfaceDesugaring.COMPANION_METHOD_TO_TRIGGER_INTERFACE_CLINIT_DESC);
+ }
+ {
+ instruction = instruction.getNext();
+ assertThat(instruction).isInstanceOf(InsnNode.class);
+ assertThat(instruction.getOpcode()).isEqualTo(Opcodes.RETURN);
+ }
+ }
+ },
+ 0);
+ }
+
+ @Test
+ public void testInterfaceComparator() {
+ assertThat(INSTANCE.compare(Runnable.class, Runnable.class)).isEqualTo(0);
+ assertThat(INSTANCE.compare(Runnable.class, MyRunnable1.class)).isEqualTo(1);
+ assertThat(INSTANCE.compare(MyRunnable2.class, Runnable.class)).isEqualTo(-1);
+ assertThat(INSTANCE.compare(MyRunnable3.class, Runnable.class)).isEqualTo(-1);
+ assertThat(INSTANCE.compare(MyRunnable1.class, MyRunnable3.class)).isEqualTo(1);
+ assertThat(INSTANCE.compare(MyRunnable3.class, MyRunnable2.class)).isEqualTo(-1);
+ assertThat(INSTANCE.compare(MyRunnable2.class, MyRunnable1.class)).isGreaterThan(0);
+ assertThat(INSTANCE.compare(Runnable.class, Serializable.class)).isGreaterThan(0);
+ assertThat(INSTANCE.compare(Serializable.class, Runnable.class)).isLessThan(0);
+
+ TreeSet<Class<?>> orderedSet = new TreeSet<>(INSTANCE);
+ orderedSet.add(Serializable.class);
+ orderedSet.add(Runnable.class);
+ orderedSet.add(MyRunnable2.class);
+ orderedSet.add(Callable.class);
+ orderedSet.add(Serializable.class);
+ orderedSet.add(MyRunnable1.class);
+ orderedSet.add(MyRunnable3.class);
+ assertThat(orderedSet)
+ .containsExactly(
+ MyRunnable3.class, // subtype before supertype(s)
+ MyRunnable1.class,
+ MyRunnable2.class,
+ Serializable.class, // java... comes textually after com.google...
+ Runnable.class,
+ Callable.class)
+ .inOrder();
+ }
+
+ private static interface MyRunnable1 extends Runnable {}
+
+ private static interface MyRunnable2 extends Runnable {}
+
+ private static interface MyRunnable3 extends MyRunnable1, MyRunnable2 {}
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/DesugarCoreLibraryFunctionalTest.java b/test/java/com/google/devtools/build/android/desugar/DesugarCoreLibraryFunctionalTest.java
new file mode 100644
index 0000000..cebbfc9
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/DesugarCoreLibraryFunctionalTest.java
@@ -0,0 +1,34 @@
+// Copyright 2017 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 org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Test that exercises classes in the {@code testdata} package. This is meant to be run against a
+ * desugared version of those classes, which in turn exercise various desugaring features.
+ */
+@RunWith(JUnit4.class)
+public class DesugarCoreLibraryFunctionalTest {
+
+ @Test
+ public void testAutoboxedTypeLambda() {
+ AutoboxedTypes.Lambda lambdaUse = AutoboxedTypes.autoboxedTypeLambda(1);
+ assertThat(lambdaUse.charAt("Karen")).isEqualTo("a");
+ }
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/DesugarDefaultMethodsFunctionalTest.java b/test/java/com/google/devtools/build/android/desugar/DesugarDefaultMethodsFunctionalTest.java
new file mode 100644
index 0000000..97d02a4
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/DesugarDefaultMethodsFunctionalTest.java
@@ -0,0 +1,29 @@
+// Copyright 2017 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.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Variant of {@link DesugarJava8FunctionalTest} that expects default and static interface methods
+ * to be desugared
+ */
+@RunWith(JUnit4.class)
+public final class DesugarDefaultMethodsFunctionalTest extends DesugarJava8FunctionalTest {
+
+ public DesugarDefaultMethodsFunctionalTest() {
+ super(/*expectBridgesFromSeparateTarget*/ true, /*expectDefaultMethods*/ false);
+ }
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/DesugarFunctionalTest.java b/test/java/com/google/devtools/build/android/desugar/DesugarFunctionalTest.java
new file mode 100644
index 0000000..14e8f37
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/DesugarFunctionalTest.java
@@ -0,0 +1,339 @@
+// 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;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.lang.reflect.Modifier.isFinal;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.io.ByteStreams;
+import com.google.devtools.build.android.desugar.testdata.CaptureLambda;
+import com.google.devtools.build.android.desugar.testdata.ConcreteFunction;
+import com.google.devtools.build.android.desugar.testdata.ConstructorReference;
+import com.google.devtools.build.android.desugar.testdata.GuavaLambda;
+import com.google.devtools.build.android.desugar.testdata.InnerClassLambda;
+import com.google.devtools.build.android.desugar.testdata.InterfaceWithLambda;
+import com.google.devtools.build.android.desugar.testdata.Lambda;
+import com.google.devtools.build.android.desugar.testdata.LambdaInOverride;
+import com.google.devtools.build.android.desugar.testdata.MethodReference;
+import com.google.devtools.build.android.desugar.testdata.MethodReferenceInSubclass;
+import com.google.devtools.build.android.desugar.testdata.MethodReferenceSuperclass;
+import com.google.devtools.build.android.desugar.testdata.OuterReferenceLambda;
+import com.google.devtools.build.android.desugar.testdata.SpecializedFunction;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.concurrent.Callable;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Test that exercises classes in the {@code testdata} package. This is meant to be run against a
+ * desugared version of those classes, which in turn exercise various desugaring features.
+ */
+@RunWith(JUnit4.class)
+public class DesugarFunctionalTest {
+
+ private final int expectedBridgesFromSameTarget;
+ private final int expectedBridgesFromSeparateTarget;
+ private final boolean expectLambdaMethodsInInterfaces;
+
+ public DesugarFunctionalTest() {
+ this(3, 1, false);
+ }
+
+ /** Constructor for testing desugar while allowing default and static interface methods. */
+ protected DesugarFunctionalTest(
+ boolean expectBridgesFromSeparateTarget, boolean expectDefaultMethods) {
+ this(
+ expectDefaultMethods ? 0 : 3,
+ expectBridgesFromSeparateTarget ? 1 : 0,
+ expectDefaultMethods);
+ }
+
+ private DesugarFunctionalTest(int bridgesFromSameTarget, int bridgesFromSeparateTarget,
+ boolean lambdaMethodsInInterfaces) {
+ this.expectedBridgesFromSameTarget = bridgesFromSameTarget;
+ this.expectedBridgesFromSeparateTarget = bridgesFromSeparateTarget;
+ this.expectLambdaMethodsInInterfaces = lambdaMethodsInInterfaces;
+ }
+
+ @Test
+ public void testGuavaLambda() {
+ GuavaLambda lambdaUse = new GuavaLambda(ImmutableList.of("Sergey", "Larry", "Alex"));
+ assertThat(lambdaUse.as()).containsExactly("Alex");
+ }
+
+ @Test
+ public void testJavaLambda() {
+ Lambda lambdaUse = new Lambda(ImmutableList.of("Sergey", "Larry", "Alex"));
+ assertThat(lambdaUse.as()).containsExactly("Alex");
+ }
+
+ @Test
+ public void testLambdaForIntersectionType() throws Exception {
+ assertThat(Lambda.hello().call()).isEqualTo("hello");
+ }
+
+ @Test
+ public void testCapturingLambda() {
+ CaptureLambda lambdaUse = new CaptureLambda(ImmutableList.of("Sergey", "Larry", "Alex"));
+ assertThat(lambdaUse.prefixed("L")).containsExactly("Larry");
+ }
+
+ @Test
+ public void testOuterReferenceLambda() throws Exception {
+ OuterReferenceLambda lambdaUse = new OuterReferenceLambda(ImmutableList.of("Sergey", "Larry"));
+ assertThat(lambdaUse.filter(ImmutableList.of("Larry", "Alex"))).containsExactly("Larry");
+ assertThat(
+ isFinal(
+ OuterReferenceLambda.class
+ .getDeclaredMethod("lambda$filter$0$OuterReferenceLambda", String.class)
+ .getModifiers()))
+ .isTrue();
+ }
+
+ /**
+ * Tests a lambda in a subclass whose generated lambda$ method has the same name and signature
+ * as a lambda$ method generated by Javac in a superclass and both of these methods are used
+ * in the implementation of the subclass (by calling super). Naively this leads to wrong
+ * behavior (in this case, return a non-empty list) because the lambda$ in the superclass is never
+ * used once its made non-private during desugaring.
+ */
+ @Test
+ public void testOuterReferenceLambdaInOverride() throws Exception {
+ OuterReferenceLambda lambdaUse = new LambdaInOverride(ImmutableList.of("Sergey", "Larry"));
+ assertThat(lambdaUse.filter(ImmutableList.of("Larry", "Alex"))).isEmpty();
+ assertThat(
+ isFinal(
+ LambdaInOverride.class
+ .getDeclaredMethod("lambda$filter$0$LambdaInOverride", String.class)
+ .getModifiers()))
+ .isTrue();
+ }
+
+ @Test
+ public void testLambdaInAnonymousClassReferencesSurroundingMethodParameter() throws Exception {
+ assertThat(Lambda.mult(21).apply(2).call()).isEqualTo(42);
+ }
+
+ /** Tests a lambda that accesses a method parameter across 2 nested anonymous classes. */
+ @Test
+ public void testLambdaInNestedAnonymousClass() throws Exception {
+ InnerClassLambda lambdaUse = new InnerClassLambda(ImmutableList.of("Sergey", "Larry"));
+ assertThat(lambdaUse.prefixFilter("L").apply(ImmutableList.of("Lois", "Larry")).call())
+ .containsExactly("Larry");
+ }
+
+ @Test
+ public void testClassMethodReference() {
+ MethodReference methodrefUse = new MethodReference(ImmutableList.of("Sergey", "Larry", "Alex"));
+ StringBuilder dest = new StringBuilder();
+ methodrefUse.appendAll(dest);
+ assertThat(dest.toString()).isEqualTo("SergeyLarryAlex");
+ }
+
+ // Regression test for b/33378312
+ @Test
+ public void testHiddenMethodReference() {
+ MethodReference methodrefUse = new MethodReference(ImmutableList.of("Sergey", "Larry", "Alex"));
+ assertThat(methodrefUse.intersect(ImmutableList.of("Alex", "Sundar"))).containsExactly("Alex");
+ }
+
+ // Regression test for b/33378312
+ @Test
+ public void testHiddenStaticMethodReference() {
+ MethodReference methodrefUse =
+ new MethodReference(ImmutableList.of("Sergey", "Larry", "Sundar"));
+ assertThat(methodrefUse.some()).containsExactly("Sergey", "Sundar");
+ }
+
+ @Test
+ public void testDuplicateHiddenMethodReference() {
+ MethodReference methodrefUse = new MethodReference(ImmutableList.of("Sergey", "Larry", "Alex"));
+ assertThat(methodrefUse.onlyIn(ImmutableList.of("Alex", "Sundar"))).containsExactly("Sundar");
+ }
+
+ // Regression test for b/36201257
+ @Test
+ public void testMethodReferenceThatNeedsBridgeInSubclass() {
+ MethodReferenceInSubclass methodrefUse =
+ new MethodReferenceInSubclass(ImmutableList.of("Sergey", "Larry", "Alex"));
+ assertThat(methodrefUse.containsE()).containsExactly("Sergey", "Alex");
+ assertThat(methodrefUse.startsWithL()).containsExactly("Larry");
+ // Test sanity: make sure sub- and superclass have bridge methods with matching descriptors but
+ // different names
+ Method superclassBridge = findOnlyBridge(MethodReferenceSuperclass.class);
+ Method subclassBridge = findOnlyBridge(MethodReferenceInSubclass.class);
+ assertThat(superclassBridge.getName()).isNotEqualTo(subclassBridge.getName());
+ assertThat(superclassBridge.getParameterTypes()).isEqualTo(subclassBridge.getParameterTypes());
+ }
+
+ private Method findOnlyBridge(Class<?> clazz) {
+ Method result = null;
+ for (Method m : clazz.getDeclaredMethods()) {
+ if (m.getName().startsWith("bridge$")) {
+ assertThat(result).named(m.getName()).isNull();
+ result = m;
+ }
+ }
+ assertThat(result).named(clazz.getSimpleName()).isNotNull();
+ return result;
+ }
+
+ // Regression test for b/33378312
+ @Test
+ public void testThrowingPrivateMethodReference() throws Exception {
+ MethodReference methodrefUse = new MethodReference(ImmutableList.of("Sergey", "Larry"));
+ Callable<?> stringer = methodrefUse.stringer();
+ try {
+ stringer.call();
+ fail("IOException expected");
+ } catch (IOException expected) {
+ assertThat(expected).hasMessage("SergeyLarry");
+ } catch (Exception e) {
+ throw e;
+ }
+ }
+
+ // Regression test for b/33304582
+ @Test
+ public void testInterfaceMethodReference() {
+ MethodReference methodrefUse = new MethodReference(ImmutableList.of("Sergey", "Larry", "Alex"));
+ MethodReference.Transformer<String> transform = new MethodReference.Transformer<String>() {
+ @Override
+ public String transform(String input) {
+ return input.substring(1);
+ }
+ };
+ assertThat(methodrefUse.transform(transform)).containsExactly("ergey", "arry", "lex");
+ }
+
+ @Test
+ public void testConstructorReference() {
+ ConstructorReference initRefUse = new ConstructorReference(ImmutableList.of("1", "2", "42"));
+ assertThat(initRefUse.toInt()).containsExactly(1, 2, 42);
+ }
+
+ // Regression test for b/33304582
+ @Test
+ public void testPrivateConstructorReference() {
+ ConstructorReference initRefUse = ConstructorReference.singleton().apply("17");
+ assertThat(initRefUse.toInt()).containsExactly(17);
+ }
+
+ // This test is similar to testPrivateConstructorReference but the private constructor of an inner
+ // class is used as a method reference. That causes Javac to generate a bridge constructor and
+ // a lambda body method that calls it, so the desugaring step doesn't need to do anything to make
+ // the private constructor visible. This is mostly to double-check that we don't interfere with
+ // this "already-working" scenario.
+ @Test
+ public void testPrivateConstructorAccessedThroughJavacGeneratedBridge() {
+ try {
+ @SuppressWarnings("unused") // local is needed to make ErrorProne happy
+ ConstructorReference unused = ConstructorReference.emptyThroughJavacGeneratedBridge().get();
+ fail("RuntimeException expected");
+ } catch (RuntimeException expected) {
+ assertThat(expected).hasMessage("got it!");
+ }
+ }
+
+ @Test
+ public void testExpressionMethodReference() {
+ assertThat(
+ MethodReference.stringChars(new StringBuilder().append("Larry").append("Sergey"))
+ .apply(5))
+ .isEqualTo('S');
+ }
+
+ @Test
+ public void testFieldMethodReference() {
+ MethodReference methodrefUse = new MethodReference(ImmutableList.of("Sergey", "Larry", "Alex"));
+ assertThat(methodrefUse.toPredicate().test("Larry")).isTrue();
+ assertThat(methodrefUse.toPredicate().test("Sundar")).isFalse();
+ }
+
+ @Test
+ public void testConcreteFunctionWithInheritedBridgeMethods() {
+ assertThat(new ConcreteFunction().apply("1234567890987654321")).isEqualTo(1234567890987654321L);
+ assertThat(ConcreteFunction.parseAll(ImmutableList.of("5", "17"), new ConcreteFunction()))
+ .containsExactly(5L, 17L);
+ }
+
+ @Test
+ public void testLambdaWithInheritedBridgeMethods() throws Exception {
+ assertThat(ConcreteFunction.toInt().apply("123456789")).isEqualTo(123456789);
+ assertThat(ConcreteFunction.parseAll(ImmutableList.of("5", "17"), ConcreteFunction.toInt()))
+ .containsExactly(5, 17);
+ // Expect String apply(Number) and any expected bridges
+ assertThat(ConcreteFunction.toInt().getClass().getDeclaredMethods())
+ .hasLength(expectedBridgesFromSameTarget + 1);
+ // Sanity check that we only copied over methods, no fields, from the functional interface
+ try {
+ ConcreteFunction.toInt().getClass().getDeclaredField("DO_NOT_COPY_INTO_LAMBDA_CLASSES");
+ fail("NoSuchFieldException expected");
+ } catch (NoSuchFieldException expected) {}
+ assertThat(SpecializedFunction.class.getDeclaredField("DO_NOT_COPY_INTO_LAMBDA_CLASSES"))
+ .isNotNull(); // test sanity
+ }
+
+ /** Tests lambdas with bridge methods when the implemented interface is in a separate target.*/
+ @Test
+ public void testLambdaWithBridgeMethodsForInterfaceInSeparateTarget() {
+ assertThat(ConcreteFunction.isInt().test(123456789L)).isTrue();
+ assertThat(
+ ConcreteFunction.doFilter(
+ ImmutableList.of(123456789L, 1234567890987654321L),
+ ConcreteFunction.isInt()))
+ .containsExactly(123456789L);
+ // Expect test(Number) and any expected bridges
+ assertThat(ConcreteFunction.isInt().getClass().getDeclaredMethods())
+ .hasLength(expectedBridgesFromSeparateTarget + 1);
+ }
+
+ @Test
+ public void testLambdaInInterfaceStaticInitializer() {
+ assertThat(InterfaceWithLambda.DIGITS).containsExactly("0", "1").inOrder();
+ // <clinit> doesn't count but if there's a lambda method then Jacoco adds more methods
+ assertThat(InterfaceWithLambda.class.getDeclaredMethods().length != 0)
+ .isEqualTo(expectLambdaMethodsInInterfaces);
+ }
+
+ /**
+ * Sanity-checks that the resource file included in the original Jar is still there unchanged.
+ */
+ @Test
+ public void testResourcePreserved() throws Exception {
+ try (InputStream content = Lambda.class.getResource("testresource.txt").openStream()) {
+ assertThat(new String(ByteStreams.toByteArray(content), UTF_8)).isEqualTo("test");
+ }
+ }
+
+ /**
+ * Test for b/62456849. After desugar, the method {@code lambda$mult$0} should still be in the
+ * class.
+ */
+ @Test
+ public void testCallMethodWithLambdaNamingConvention()
+ throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
+ Method method = Lambda.class.getDeclaredMethod("lambda$mult$0");
+ Object value = method.invoke(null);
+ assertThat(value).isInstanceOf(Integer.class);
+ assertThat((Integer) value).isEqualTo(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
new file mode 100644
index 0000000..8321d75
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/DesugarJava8FunctionalTest.java
@@ -0,0 +1,397 @@
+// Copyright 2017 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 static com.google.devtools.build.lib.testutil.MoreAsserts.assertThrows;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.android.desugar.testdata.java8.AnnotationsOfDefaultMethodsShouldBeKept.AnnotatedInterface;
+import com.google.devtools.build.android.desugar.testdata.java8.AnnotationsOfDefaultMethodsShouldBeKept.SomeAnnotation;
+import com.google.devtools.build.android.desugar.testdata.java8.ConcreteDefaultInterfaceWithLambda;
+import com.google.devtools.build.android.desugar.testdata.java8.ConcreteOverridesDefaultWithLambda;
+import com.google.devtools.build.android.desugar.testdata.java8.DefaultInterfaceMethodWithStaticInitializer;
+import com.google.devtools.build.android.desugar.testdata.java8.DefaultInterfaceWithBridges;
+import com.google.devtools.build.android.desugar.testdata.java8.FunctionWithDefaultMethod;
+import com.google.devtools.build.android.desugar.testdata.java8.FunctionalInterfaceWithInitializerAndDefaultMethods;
+import com.google.devtools.build.android.desugar.testdata.java8.GenericDefaultInterfaceWithLambda;
+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.Java7InterfaceWithBridges;
+import com.google.devtools.build.android.desugar.testdata.java8.Named;
+import com.google.devtools.build.android.desugar.testdata.java8.TwoInheritedDefaultMethods;
+import com.google.devtools.build.android.desugar.testdata.java8.VisibilityTestClass;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Function;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Test that exercises classes in the {@code testdata_java8} package. This is meant to be run
+ * against a desugared version of those classes, which in turn exercise various desugaring features.
+ */
+@RunWith(JUnit4.class)
+public class DesugarJava8FunctionalTest extends DesugarFunctionalTest {
+
+ public DesugarJava8FunctionalTest() {
+ this(true, true);
+ }
+
+ protected DesugarJava8FunctionalTest(
+ boolean expectBridgesFromSeparateTarget, boolean expectDefaultMethods) {
+ super(expectBridgesFromSeparateTarget, expectDefaultMethods);
+ }
+
+ @Test
+ public void testLambdaInDefaultMethod() {
+ assertThat(new ConcreteDefaultInterfaceWithLambda().defaultWithLambda())
+ .containsExactly("0", "1")
+ .inOrder();
+ }
+
+ @Test
+ public void testLambdaInDefaultCallsInterfaceMethod() {
+ assertThat(new ConcreteDefaultInterfaceWithLambda().defaultCallsInterfaceMethod())
+ .containsExactly("1", "2")
+ .inOrder();
+ }
+
+ @Test
+ public void testOverrideLambdaInDefault() {
+ assertThat(new ConcreteOverridesDefaultWithLambda().defaultWithLambda())
+ .containsExactly("2", "3")
+ .inOrder();
+ }
+
+ @Test
+ public void testLambdaInDefaultCallsOverrideMethod() {
+ assertThat(new ConcreteOverridesDefaultWithLambda().defaultCallsInterfaceMethod())
+ .containsExactly("3", "4")
+ .inOrder();
+ }
+
+ @Test
+ public void testDefaultInterfaceMethodReference() {
+ InterfaceMethod methodrefUse = new InterfaceMethod.Concrete();
+ List<String> dest =
+ methodrefUse.defaultMethodReference(ImmutableList.of("Sergey", "Larry", "Alex"));
+ assertThat(dest).containsExactly("Sergey");
+ }
+
+ @Test
+ public void testStaticInterfaceMethodReference() {
+ InterfaceMethod methodrefUse = new InterfaceMethod.Concrete();
+ List<String> dest =
+ methodrefUse.staticMethodReference(ImmutableList.of("Sergey", "Larry", "Alex"));
+ assertThat(dest).containsExactly("Alex");
+ }
+
+ @Test
+ public void testLambdaCallsDefaultMethod() {
+ InterfaceMethod methodrefUse = new InterfaceMethod.Concrete();
+ List<String> dest =
+ methodrefUse.lambdaCallsDefaultMethod(ImmutableList.of("Sergey", "Larry", "Alex"));
+ assertThat(dest).containsExactly("Sergey");
+ }
+
+ @Test
+ public void testStaticMethodsInInterface_explicitAndLambdaBody() {
+ List<Long> result = FunctionWithDefaultMethod.DoubleInts.add(ImmutableList.of(7, 39, 8), 3);
+ assertThat(result).containsExactly(10L, 42L, 11L).inOrder();
+ }
+
+ @Test
+ public void testOverriddenDefaultMethod_inHandwrittenClass() {
+ FunctionWithDefaultMethod<Integer> doubler = new FunctionWithDefaultMethod.DoubleInts();
+ assertThat(doubler.apply(7)).isEqualTo(14);
+ assertThat(doubler.twice(7)).isEqualTo(35);
+ }
+
+ @Test
+ public void testOverriddenDefaultMethod_inHandwrittenSuperclass() {
+ FunctionWithDefaultMethod<Integer> doubler = new FunctionWithDefaultMethod.DoubleInts2();
+ assertThat(doubler.apply(7)).isEqualTo(14);
+ assertThat(doubler.twice(7)).isEqualTo(35);
+ }
+
+ @Test
+ public void testInheritedDefaultMethod_inLambda() {
+ FunctionWithDefaultMethod<Integer> doubler =
+ FunctionWithDefaultMethod.DoubleInts.doubleLambda();
+ assertThat(doubler.apply(7)).isEqualTo(14);
+ assertThat(doubler.twice(7)).isEqualTo(28);
+ }
+
+ @Test
+ public void testDefaultMethodReference_onLambda() {
+ FunctionWithDefaultMethod<Integer> plus6 = FunctionWithDefaultMethod.DoubleInts.incTwice(3);
+ assertThat(plus6.apply(18)).isEqualTo(24);
+ assertThat(plus6.twice(18)).isEqualTo(30);
+ }
+
+ @Test
+ public void testDefaultMethodReference_onHandwrittenClass() {
+ FunctionWithDefaultMethod<Integer> times5 = FunctionWithDefaultMethod.DoubleInts.times5();
+ assertThat(times5.apply(6)).isEqualTo(30);
+ assertThat(times5.twice(6)).isEqualTo(150); // Irrelevant that DoubleInts overrides twice()
+ }
+
+ @Test
+ public void testStaticInterfaceMethodReferenceReturned() {
+ Function<Integer, FunctionWithDefaultMethod<Integer>> factory =
+ FunctionWithDefaultMethod.DoubleInts.incFactory();
+ assertThat(factory.apply(6).apply(7)).isEqualTo(13);
+ assertThat(factory.apply(6).twice(7)).isEqualTo(19);
+ }
+
+ @Test
+ public void testSuperDefaultMethodInvocation() {
+ assertThat(new TwoInheritedDefaultMethods().name()).isEqualTo("One:Two");
+ assertThat(new Named.DefaultName().name()).isEqualTo("DefaultName-once");
+ assertThat(new Named.DefaultNameSubclass().name()).isEqualTo("DefaultNameSubclass-once-twice");
+ }
+
+ @Test
+ public void testInheritedPreferredOverDefault() throws Exception {
+ assertThat(new Named.ExplicitName("hello").name()).isEqualTo("hello");
+ // Make sure AbstractName remains abstract, despite default method from implemented interface
+ assertThat(Modifier.isAbstract(Named.AbstractName.class.getMethod("name").getModifiers()))
+ .isTrue();
+ }
+
+ @Test
+ public void testRedefinedDefaultMethod() throws Exception {
+ assertThat(new InterfaceWithDefaultMethod.Version2().version()).isEqualTo(2);
+ }
+
+ @Test
+ public void testDefaultMethodRedefinedInSubclass() throws Exception {
+ assertThat(new InterfaceWithDefaultMethod.AlsoVersion2().version()).isEqualTo(2);
+ }
+
+ @Test
+ public void testDefaultMethodVisibility() {
+ assertThat(new VisibilityTestClass().m()).isEqualTo(42);
+ }
+
+ /** Test for b/38302860 */
+ @Test
+ public void testAnnotationsOfDefaultMethodsAreKept() throws Exception {
+ {
+ Annotation[] annotations = AnnotatedInterface.class.getAnnotations();
+ assertThat(annotations).hasLength(1);
+ assertThat(annotations[0]).isInstanceOf(SomeAnnotation.class);
+ assertThat(((SomeAnnotation) annotations[0]).value()).isEqualTo(1);
+ }
+ {
+ Annotation[] annotations =
+ AnnotatedInterface.class.getMethod("annotatedAbstractMethod").getAnnotations();
+ assertThat(annotations).hasLength(1);
+ assertThat(annotations[0]).isInstanceOf(SomeAnnotation.class);
+ assertThat(((SomeAnnotation) annotations[0]).value()).isEqualTo(2);
+ }
+ {
+ Annotation[] annotations =
+ AnnotatedInterface.class.getMethod("annotatedDefaultMethod").getAnnotations();
+ assertThat(annotations).hasLength(1);
+ assertThat(annotations[0]).isInstanceOf(SomeAnnotation.class);
+ assertThat(((SomeAnnotation) annotations[0]).value()).isEqualTo(3);
+ }
+ }
+ /** Test for b/38308515 */
+ @Test
+ public void testDefaultAndStaticMethodNameClash() {
+ final ClassWithDuplicateMethods instance = new ClassWithDuplicateMethods();
+ assertThat(instance.getZero()).isEqualTo(0);
+ assertThat(instance.getZeroFromStaticInterfaceMethod()).isEqualTo(1);
+ }
+
+ /**
+ * Test for b/38257037
+ *
+ * <p>Note that, we intentionally suppress unchecked warnings, because we expect some
+ * ClassCastException to test bridge methods.
+ */
+ @SuppressWarnings("unchecked")
+ @Test
+ public void testBridgeAndDefaultMethods() {
+ {
+ DefaultInterfaceWithBridges object = new DefaultInterfaceWithBridges();
+ Integer one = 1;
+ assertThat(object.copy(one)).isEqualTo(one);
+ assertThat(object.copy((Number) one)).isEqualTo(one);
+ assertThrows(ClassCastException.class, () -> object.copy(Double.valueOf(1)));
+
+ assertThat(object.getNumber()).isInstanceOf(Double.class);
+ assertThat(object.getNumber()).isEqualTo(Double.valueOf(2.3d));
+ assertThat(object.getDouble()).isEqualTo(Double.valueOf(2.3d));
+ }
+ {
+ Java7InterfaceWithBridges.ClassAddTwo testObject =
+ new Java7InterfaceWithBridges.ClassAddTwo();
+ assertThat(testObject.add(Integer.valueOf(2))).isEqualTo(4);
+
+ @SuppressWarnings("rawtypes")
+ Java7InterfaceWithBridges top = testObject;
+ assertThat(top.add(Integer.valueOf(2))).isEqualTo(4);
+ assertThrows(ClassCastException.class, () -> top.add(new Object()));
+ assertThrows(ClassCastException.class, () -> top.add(Double.valueOf(1)));
+ assertThrows(ClassCastException.class, () -> top.add(Long.valueOf(1)));
+
+ @SuppressWarnings("rawtypes")
+ Java7InterfaceWithBridges.LevelOne levelOne = testObject;
+ assertThat(levelOne.add(Integer.valueOf(2))).isEqualTo(4);
+ assertThrows(ClassCastException.class, () -> top.add(new Object()));
+ assertThrows(ClassCastException.class, () -> top.add(Double.valueOf(1)));
+ assertThrows(ClassCastException.class, () -> top.add(Long.valueOf(1)));
+
+ @SuppressWarnings("rawtypes")
+ Java7InterfaceWithBridges.LevelOne levelTwo = testObject;
+ assertThat(levelTwo.add(Integer.valueOf(2))).isEqualTo(4);
+ assertThrows(ClassCastException.class, () -> levelTwo.add(Double.valueOf(1)));
+ assertThrows(ClassCastException.class, () -> levelTwo.add(Long.valueOf(1)));
+ }
+ {
+ GenericDefaultInterfaceWithLambda.ClassTwo testObject =
+ new GenericDefaultInterfaceWithLambda.ClassTwo();
+
+ assertThat(testObject.increment(Integer.valueOf(0))).isEqualTo(1);
+ assertThat(testObject.toString(Integer.valueOf(0))).isEqualTo("0");
+ assertThat(testObject.getBaseValue()).isEqualTo(Integer.valueOf(0));
+
+ assertThat(testObject.toList(0)).isEmpty();
+ assertThat(testObject.toList(1)).containsExactly(0).inOrder();
+ assertThat(testObject.toList(2)).containsExactly(0, 1).inOrder();
+
+ assertThat(((Function<Integer, ArrayList<Integer>>) testObject.toListSupplier()).apply(0))
+ .isEmpty();
+ assertThat(((Function<Integer, ArrayList<Integer>>) testObject.toListSupplier()).apply(1))
+ .containsExactly(0)
+ .inOrder();
+ assertThat(((Function<Integer, ArrayList<Integer>>) testObject.toListSupplier()).apply(2))
+ .containsExactly(0, 1)
+ .inOrder();
+
+ assertThat(testObject.convertToStringList(ImmutableList.of(0)))
+ .containsExactly("0")
+ .inOrder();
+ assertThat(testObject.convertToStringList(ImmutableList.of(0, 1)))
+ .containsExactly("0", "1")
+ .inOrder();
+
+ @SuppressWarnings("rawtypes")
+ GenericDefaultInterfaceWithLambda top = testObject;
+ assertThrows(ClassCastException.class, () -> top.increment(Long.valueOf(1)));
+ assertThrows(ClassCastException.class, () -> top.toString(Long.valueOf(1)));
+ assertThat(top.increment(Integer.valueOf(0))).isEqualTo(1);
+ assertThat(top.toString(Integer.valueOf(0))).isEqualTo("0");
+ assertThat(top.getBaseValue()).isEqualTo(Integer.valueOf(0));
+
+ assertThat(top.toList(0)).isEmpty();
+ assertThat(top.toList(1)).containsExactly(0).inOrder();
+ assertThat(top.toList(2)).containsExactly(0, 1).inOrder();
+
+ assertThat(((Function<Integer, ArrayList<Integer>>) top.toListSupplier()).apply(0)).isEmpty();
+ assertThat(((Function<Integer, ArrayList<Integer>>) top.toListSupplier()).apply(1))
+ .containsExactly(0)
+ .inOrder();
+ assertThat(((Function<Integer, ArrayList<Integer>>) top.toListSupplier()).apply(2))
+ .containsExactly(0, 1)
+ .inOrder();
+
+ assertThat(top.convertToStringList(ImmutableList.of(0))).containsExactly("0").inOrder();
+ assertThat(top.convertToStringList(ImmutableList.of(0, 1)))
+ .containsExactly("0", "1")
+ .inOrder();
+ }
+ {
+ @SuppressWarnings("rawtypes")
+ GenericDefaultInterfaceWithLambda testObject =
+ new GenericDefaultInterfaceWithLambda.ClassThree();
+ assertThat(testObject.getBaseValue()).isEqualTo(Long.valueOf(0));
+ assertThat(testObject.increment(Long.valueOf(0))).isEqualTo(Long.valueOf(0 + 1));
+ assertThat(testObject.toString(Long.valueOf(0))).isEqualTo(Long.valueOf(0).toString());
+ assertThrows(ClassCastException.class, () -> testObject.increment(Integer.valueOf(0)));
+ assertThrows(ClassCastException.class, () -> testObject.toString(Integer.valueOf(0)));
+ assertThat(testObject.toList(2)).containsExactly(Long.valueOf(0), Long.valueOf(1)).inOrder();
+ assertThat(testObject.convertToStringList(testObject.toList(1))).containsExactly("0");
+ assertThat(((Function<Integer, ArrayList<Long>>) testObject.toListSupplier()).apply(2))
+ .containsExactly(Long.valueOf(0), Long.valueOf(1));
+ }
+ }
+
+ /**
+ * Test for b/62047432.
+ *
+ * <p>When desugaring a functional interface with an executable clinit and default methods, we
+ * erase the body of clinit to avoid executing it during desugaring. This test makes sure that all
+ * the constants defined in the interface are still there after desugaring.
+ */
+ @Test
+ public void testFunctionalInterfaceWithExecutableClinitStillWorkAfterDesugar() {
+ assertThat(FunctionalInterfaceWithInitializerAndDefaultMethods.CONSTANT.length("").convert())
+ .isEqualTo(0);
+ assertThat(FunctionalInterfaceWithInitializerAndDefaultMethods.CONSTANT.length("1").convert())
+ .isEqualTo(1);
+ assertThat(FunctionalInterfaceWithInitializerAndDefaultMethods.BOOLEAN).isFalse();
+ assertThat(FunctionalInterfaceWithInitializerAndDefaultMethods.CHAR).isEqualTo('h');
+ assertThat(FunctionalInterfaceWithInitializerAndDefaultMethods.BYTE).isEqualTo(0);
+ assertThat(FunctionalInterfaceWithInitializerAndDefaultMethods.SHORT).isEqualTo(0);
+
+ assertThat(FunctionalInterfaceWithInitializerAndDefaultMethods.INT).isEqualTo(0);
+ assertThat(FunctionalInterfaceWithInitializerAndDefaultMethods.FLOAT).isEqualTo(0f);
+ assertThat(FunctionalInterfaceWithInitializerAndDefaultMethods.LONG).isEqualTo(0);
+ assertThat(FunctionalInterfaceWithInitializerAndDefaultMethods.DOUBLE).isEqualTo(0d);
+ }
+
+ /** Test for b/38255926. */
+ @Test
+ public void testDefaultMethodInitializationOrder() {
+ {
+ assertThat(new DefaultInterfaceMethodWithStaticInitializer.TestInterfaceSetOne.C().sum())
+ .isEqualTo(11); // To trigger loading the class C and its super interfaces.
+ assertThat(
+ DefaultInterfaceMethodWithStaticInitializer.TestInterfaceSetOne
+ .getRealInitializationOrder())
+ .isEqualTo(
+ DefaultInterfaceMethodWithStaticInitializer.TestInterfaceSetOne
+ .getExpectedInitializationOrder());
+ }
+ {
+ assertThat(new DefaultInterfaceMethodWithStaticInitializer.TestInterfaceSetTwo.C().sum())
+ .isEqualTo(3);
+ assertThat(
+ DefaultInterfaceMethodWithStaticInitializer.TestInterfaceSetTwo
+ .getRealInitializationOrder())
+ .isEqualTo(
+ DefaultInterfaceMethodWithStaticInitializer.TestInterfaceSetTwo
+ .getExpectedInitializationOrder());
+ }
+ {
+ assertThat(new DefaultInterfaceMethodWithStaticInitializer.TestInterfaceSetThree.C().sum())
+ .isEqualTo(11);
+ assertThat(
+ DefaultInterfaceMethodWithStaticInitializer.TestInterfaceSetThree
+ .getRealInitializationOrder())
+ .isEqualTo(
+ DefaultInterfaceMethodWithStaticInitializer.TestInterfaceSetThree
+ .getExpectedInitializationOrder());
+ }
+ }
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/DesugarJava8LikeAndroidStudioFunctionalTest.java b/test/java/com/google/devtools/build/android/desugar/DesugarJava8LikeAndroidStudioFunctionalTest.java
new file mode 100644
index 0000000..13edc57
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/DesugarJava8LikeAndroidStudioFunctionalTest.java
@@ -0,0 +1,31 @@
+// Copyright 2017 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.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Variant of {@link DesugarJava8FunctionalTest} that doesn't expect any bridge methods already
+ * present on functional interfaces to be also present on generated classes, even where functional
+ * interfaces are defined in other compilations, which requires compiling against regular jar files
+ * instead of a classpath of -hjars.
+ */
+@RunWith(JUnit4.class)
+public final class DesugarJava8LikeAndroidStudioFunctionalTest extends DesugarJava8FunctionalTest {
+
+ public DesugarJava8LikeAndroidStudioFunctionalTest() {
+ super(/*expectBridgesFromSeparateTarget*/ false, /*expectDefaultMethods*/ true);
+ }
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/DesugarLongCompareTest.java b/test/java/com/google/devtools/build/android/desugar/DesugarLongCompareTest.java
new file mode 100644
index 0000000..708fc8e
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/DesugarLongCompareTest.java
@@ -0,0 +1,135 @@
+// Copyright 2017 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 static org.junit.Assert.fail;
+import static org.objectweb.asm.Opcodes.INVOKESTATIC;
+
+import com.google.devtools.build.android.desugar.testdata.ClassCallingLongCompare;
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+
+/** The test case for the rewriter rewriting a call of Long.compare(long, long) to lcmp. */
+@RunWith(JUnit4.class)
+public class DesugarLongCompareTest {
+
+ @Test
+ public void testClassCallingLongCompareHasNoReferenceToLong_compare() {
+ try {
+ ClassReader reader = new ClassReader(ClassCallingLongCompare.class.getName());
+
+ AtomicInteger counter = new AtomicInteger(0);
+
+ reader.accept(
+ new ClassVisitor(Opcodes.ASM5) {
+ @Override
+ public MethodVisitor visitMethod(
+ int access, String name, String desc, String signature, String[] exceptions) {
+ return new MethodVisitor(api) {
+ @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")) {
+ counter.incrementAndGet();
+ }
+ }
+ };
+ }
+ },
+ 0);
+ assertThat(counter.get()).isEqualTo(0);
+ } catch (IOException e) {
+ fail();
+ }
+ }
+
+ @Test
+ public void testCompareLongWithLambda() {
+ assertThat(ClassCallingLongCompare.compareLongWithLambda(1, 0)).isEqualTo(1);
+ assertThat(ClassCallingLongCompare.compareLongWithLambda(1, 1)).isEqualTo(0);
+ assertThat(ClassCallingLongCompare.compareLongWithLambda(1, 2)).isEqualTo(-1);
+ assertThat(ClassCallingLongCompare.compareLongWithLambda(Long.MAX_VALUE, Long.MIN_VALUE))
+ .isEqualTo(1);
+ assertThat(ClassCallingLongCompare.compareLongWithLambda(Long.MAX_VALUE, Long.MAX_VALUE))
+ .isEqualTo(0);
+ assertThat(ClassCallingLongCompare.compareLongWithLambda(Long.MIN_VALUE, Long.MAX_VALUE))
+ .isEqualTo(-1);
+ }
+
+ @Test
+ public void testCompareLongWithMethodReference() {
+ assertThat(ClassCallingLongCompare.compareLongWithMethodReference(1, 0)).isEqualTo(1);
+ assertThat(ClassCallingLongCompare.compareLongWithMethodReference(1, 1)).isEqualTo(0);
+ assertThat(ClassCallingLongCompare.compareLongWithMethodReference(1, 2)).isEqualTo(-1);
+ assertThat(
+ ClassCallingLongCompare.compareLongWithMethodReference(Long.MAX_VALUE, Long.MIN_VALUE))
+ .isEqualTo(1);
+ assertThat(
+ ClassCallingLongCompare.compareLongWithMethodReference(Long.MAX_VALUE, Long.MAX_VALUE))
+ .isEqualTo(0);
+ assertThat(
+ ClassCallingLongCompare.compareLongWithMethodReference(Long.MIN_VALUE, Long.MAX_VALUE))
+ .isEqualTo(-1);
+ }
+
+ @Test
+ public void testcompareLongByCallingLong_compare() {
+ assertThat(ClassCallingLongCompare.compareLongByCallingLong_compare(1, 0)).isEqualTo(1);
+ assertThat(ClassCallingLongCompare.compareLongByCallingLong_compare(1, 1)).isEqualTo(0);
+ assertThat(ClassCallingLongCompare.compareLongByCallingLong_compare(1, 2)).isEqualTo(-1);
+ assertThat(
+ ClassCallingLongCompare.compareLongByCallingLong_compare(
+ Long.MAX_VALUE, Long.MIN_VALUE))
+ .isEqualTo(1);
+ assertThat(
+ ClassCallingLongCompare.compareLongByCallingLong_compare(
+ Long.MAX_VALUE, Long.MAX_VALUE))
+ .isEqualTo(0);
+ assertThat(
+ ClassCallingLongCompare.compareLongByCallingLong_compare(
+ Long.MIN_VALUE, Long.MAX_VALUE))
+ .isEqualTo(-1);
+ }
+
+ @Test
+ public void testcompareLongByCallingLong_compare2() {
+ assertThat(ClassCallingLongCompare.compareLongByCallingLong_compare2(1, 0)).isEqualTo("g");
+ assertThat(ClassCallingLongCompare.compareLongByCallingLong_compare2(1, 1)).isEqualTo("e");
+ assertThat(ClassCallingLongCompare.compareLongByCallingLong_compare2(0, 1)).isEqualTo("l");
+
+ assertThat(
+ ClassCallingLongCompare.compareLongByCallingLong_compare2(
+ Long.MAX_VALUE, Long.MIN_VALUE))
+ .isEqualTo("g");
+ assertThat(
+ ClassCallingLongCompare.compareLongByCallingLong_compare2(
+ Long.MAX_VALUE, Long.MAX_VALUE))
+ .isEqualTo("e");
+ assertThat(
+ ClassCallingLongCompare.compareLongByCallingLong_compare2(
+ Long.MIN_VALUE, Long.MAX_VALUE))
+ .isEqualTo("l");
+ }
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/DesugarMainClassTest.java b/test/java/com/google/devtools/build/android/desugar/DesugarMainClassTest.java
new file mode 100644
index 0000000..0392012
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/DesugarMainClassTest.java
@@ -0,0 +1,82 @@
+// 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;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.devtools.build.android.desugar.LambdaClassMaker.LAMBDA_METAFACTORY_DUMPER_PROPERTY;
+import static org.junit.Assert.fail;
+
+import com.google.common.base.Strings;
+import java.io.IOError;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.function.Supplier;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Test for {@link Desugar} */
+@RunWith(JUnit4.class)
+public class DesugarMainClassTest {
+
+ @Test
+ public void testVerifyLambdaDumpDirectoryRegistration() throws Exception {
+ if (Strings.isNullOrEmpty(System.getProperty(LAMBDA_METAFACTORY_DUMPER_PROPERTY))) {
+ testLambdaDumpDirSpecifiedInProgramFail();
+ } else {
+ testLambdaDumpDirPassSpecifiedInCmdPass();
+ }
+ }
+
+ private void testLambdaDumpDirSpecifiedInProgramFail() throws Exception {
+ // This lambda will fail the dump directory registration, which is intended.
+ Supplier<Path> supplier =
+ () -> {
+ Path path = Paths.get(".").toAbsolutePath();
+ System.setProperty(LAMBDA_METAFACTORY_DUMPER_PROPERTY, path.toString());
+ return path;
+ };
+ try {
+ Desugar.verifyLambdaDumpDirectoryRegistered(supplier.get());
+ fail("Expected NullPointerException");
+ } catch (NullPointerException e) {
+ // Expected
+ }
+ }
+
+ /**
+ * Test the LambdaMetafactory can be correctly set up by specifying the system property {@code
+ * LAMBDA_METAFACTORY_DUMPER_PROPERTY} in the command line.
+ */
+ private void testLambdaDumpDirPassSpecifiedInCmdPass() throws IOException {
+ // The following lambda ensures that the LambdaMetafactory is loaded at the beggining of this
+ // test, so that the dump directory can be registered.
+ Supplier<Path> supplier =
+ () -> {
+ try {
+ return Desugar.createAndRegisterLambdaDumpDirectory();
+ } catch (IOException e) {
+ throw new IOError(e);
+ }
+ };
+ Path dumpDirectory = supplier.get();
+ assertThat(dumpDirectory.toAbsolutePath().toString())
+ .isEqualTo(
+ Paths.get(System.getProperty(LAMBDA_METAFACTORY_DUMPER_PROPERTY))
+ .toAbsolutePath()
+ .toString());
+ Desugar.verifyLambdaDumpDirectoryRegistered(dumpDirectory);
+ }
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/DesugarObjectsRequireNonNullTest.java b/test/java/com/google/devtools/build/android/desugar/DesugarObjectsRequireNonNullTest.java
new file mode 100644
index 0000000..6f3be3c
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/DesugarObjectsRequireNonNullTest.java
@@ -0,0 +1,153 @@
+// 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;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+import static org.objectweb.asm.Opcodes.INVOKESTATIC;
+
+import com.google.devtools.build.android.desugar.testdata.ClassCallingRequireNonNull;
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+
+/**
+ * This test case tests the desugaring feature for Objects.requireNonNull. This feature replaces any
+ * call to this method with o.getClass() to check whether 'o' is null.
+ */
+@RunWith(JUnit4.class)
+public class DesugarObjectsRequireNonNullTest {
+
+ @Test
+ public void testClassCallingRequireNonNullHasNoReferenceToRequiresNonNull() {
+ try {
+ ClassReader reader = new ClassReader(ClassCallingRequireNonNull.class.getName());
+
+ AtomicInteger counterForSingleArgument = new AtomicInteger(0);
+ AtomicInteger counterForString = new AtomicInteger(0);
+ AtomicInteger counterForSupplier = new AtomicInteger(0);
+
+ reader.accept(
+ new ClassVisitor(Opcodes.ASM5) {
+ @Override
+ public MethodVisitor visitMethod(
+ int access, String name, String desc, String signature, String[] exceptions) {
+ return new MethodVisitor(api) {
+ @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")) {
+ switch (desc) {
+ case "(Ljava/lang/Object;)Ljava/lang/Object;":
+ counterForSingleArgument.incrementAndGet();
+ break;
+ case "(Ljava/lang/Object;Ljava/lang/String;)Ljava/lang/Object;":
+ counterForString.incrementAndGet();
+ break;
+ case "(Ljava/lang/Object;Ljava/util/function/Supplier;)Ljava/lang/Object;":
+ counterForSupplier.incrementAndGet();
+ break;
+ default:
+ fail("Unknown overloaded requireNonNull is found: " + desc);
+ }
+ }
+ }
+ };
+ }
+ },
+ 0);
+ assertThat(counterForSingleArgument.get()).isEqualTo(0);
+ // we do not desugar requireNonNull(Object, String) or requireNonNull(Object, Supplier)
+ assertThat(counterForString.get()).isEqualTo(1);
+ assertThat(counterForSupplier.get()).isEqualTo(1);
+ } catch (IOException e) {
+ fail();
+ }
+ }
+
+ @Test
+ public void testInliningImplicitCallToObjectsRequireNonNull() {
+ try {
+ ClassCallingRequireNonNull.getStringLengthWithMethodReference(null);
+ fail ("NullPointerException expected");
+ } catch (NullPointerException e) {
+ // Expected
+ }
+
+ assertThat(ClassCallingRequireNonNull.getStringLengthWithMethodReference("")).isEqualTo(0);
+ assertThat(ClassCallingRequireNonNull.getStringLengthWithMethodReference("1")).isEqualTo(1);
+
+ try {
+ ClassCallingRequireNonNull.getStringLengthWithLambdaAndExplicitCallToRequireNonNull(null);
+ fail ("NullPointerException expected");
+ } catch (NullPointerException e) {
+ // Expected
+ }
+
+ assertThat(
+ ClassCallingRequireNonNull.getStringLengthWithLambdaAndExplicitCallToRequireNonNull(""))
+ .isEqualTo(0);
+ assertThat(
+ ClassCallingRequireNonNull.getStringLengthWithLambdaAndExplicitCallToRequireNonNull(
+ "1"))
+ .isEqualTo(1);
+ }
+
+ @Test
+ public void testInliningExplicitCallToObjectsRequireNonNull() {
+ try {
+ ClassCallingRequireNonNull.getFirstCharVersionOne(null);
+ fail ("NullPointerException expected");
+ } catch (NullPointerException e) {
+ // Expected
+ }
+
+ try {
+ ClassCallingRequireNonNull.getFirstCharVersionTwo(null);
+ fail ("NullPointerException expected");
+ } catch (NullPointerException e) {
+ // Expected
+ }
+
+ try {
+ ClassCallingRequireNonNull.callRequireNonNullWithArgumentString(null);
+ fail ("NullPointerException expected");
+ } catch (NullPointerException e) {
+ // Expected
+ }
+
+ try {
+ ClassCallingRequireNonNull.callRequireNonNullWithArgumentSupplier(null);
+ fail ("NullPointerException expected");
+ } catch (NullPointerException e) {
+ // Expected
+ }
+
+ assertThat(ClassCallingRequireNonNull.getFirstCharVersionOne("hello")).isEqualTo('h');
+ assertThat(ClassCallingRequireNonNull.getFirstCharVersionTwo("hello")).isEqualTo('h');
+
+ assertThat(ClassCallingRequireNonNull.callRequireNonNullWithArgumentString("hello"))
+ .isEqualTo('h');
+ assertThat(ClassCallingRequireNonNull.callRequireNonNullWithArgumentSupplier("hello"))
+ .isEqualTo('h');
+ }
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/DesugarTryWithResourcesFunctionalTest.java b/test/java/com/google/devtools/build/android/desugar/DesugarTryWithResourcesFunctionalTest.java
new file mode 100644
index 0000000..38df9e3
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/DesugarTryWithResourcesFunctionalTest.java
@@ -0,0 +1,99 @@
+// Copyright 2017 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 static com.google.devtools.build.android.desugar.runtime.ThrowableExtensionTestUtility.getStrategyClassName;
+import static com.google.devtools.build.android.desugar.runtime.ThrowableExtensionTestUtility.getTwrStrategyClassNameSpecifiedInSystemProperty;
+import static com.google.devtools.build.android.desugar.runtime.ThrowableExtensionTestUtility.isMimicStrategy;
+import static com.google.devtools.build.android.desugar.runtime.ThrowableExtensionTestUtility.isNullStrategy;
+import static com.google.devtools.build.android.desugar.runtime.ThrowableExtensionTestUtility.isReuseStrategy;
+import static org.junit.Assert.fail;
+
+import com.google.devtools.build.android.desugar.runtime.ThrowableExtension;
+import com.google.devtools.build.android.desugar.testdata.ClassUsingTryWithResources;
+import java.io.IOException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** The functional test for desugaring try-with-resources. */
+@RunWith(JUnit4.class)
+public class DesugarTryWithResourcesFunctionalTest {
+
+ @Test
+ public void testCheckSuppressedExceptionsReturningEmptySuppressedExceptions() {
+ Throwable[] suppressed = ClassUsingTryWithResources.checkSuppressedExceptions(false);
+ assertThat(suppressed).isEmpty();
+ }
+
+ @Test
+ public void testPrintStackTraceOfCaughtException() {
+ String trace = ClassUsingTryWithResources.printStackTraceOfCaughtException();
+ if (isMimicStrategy()) {
+ assertThat(trace.toLowerCase()).contains("suppressed");
+ } else if (isReuseStrategy()) {
+ assertThat(trace.toLowerCase()).contains("suppressed");
+ } else if (isNullStrategy()) {
+ assertThat(trace.toLowerCase()).doesNotContain("suppressed");
+ } else {
+ fail("unexpected desugaring strategy " + ThrowableExtension.getStrategy());
+ }
+ }
+
+ @Test
+ public void testCheckSuppressedExceptionReturningOneSuppressedException() {
+ Throwable[] suppressed = ClassUsingTryWithResources.checkSuppressedExceptions(true);
+
+ if (isMimicStrategy()) {
+ assertThat(suppressed).hasLength(1);
+ } else if (isReuseStrategy()) {
+ assertThat(suppressed).hasLength(1);
+ } else if (isNullStrategy()) {
+ assertThat(suppressed).isEmpty();
+ } else {
+ fail("unexpected desugaring strategy " + ThrowableExtension.getStrategy());
+ }
+ }
+
+ @Test
+ public void testSimpleTryWithResources() {
+
+ try {
+ ClassUsingTryWithResources.simpleTryWithResources();
+ 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/FieldInfoTest.java b/test/java/com/google/devtools/build/android/desugar/FieldInfoTest.java
new file mode 100644
index 0000000..afb2bea
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/FieldInfoTest.java
@@ -0,0 +1,33 @@
+// 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;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Test for {@link FieldInfo} */
+@RunWith(JUnit4.class)
+public class FieldInfoTest {
+
+ @Test
+ public void testFieldsAreCorrectlySet() {
+ FieldInfo info = FieldInfo.create("owner", "name", "desc");
+ assertThat(info.owner()).isEqualTo("owner");
+ assertThat(info.name()).isEqualTo("name");
+ assertThat(info.desc()).isEqualTo("desc");
+ }
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/IndexedInputsTest.java b/test/java/com/google/devtools/build/android/desugar/IndexedInputsTest.java
new file mode 100644
index 0000000..bac3fc9
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/IndexedInputsTest.java
@@ -0,0 +1,136 @@
+// Copyright 2017 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 java.io.File;
+import java.io.FileOutputStream;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Test that exercises the behavior of the IndexedInputs class.
+ */
+@RunWith(JUnit4.class)
+public final class IndexedInputsTest {
+
+ private static File lib1;
+
+ private static String lib1Name;
+
+ private static File lib2;
+
+ private static String lib2Name;
+
+ private static InputFileProvider lib1InputFileProvider;
+
+ private static InputFileProvider lib2InputFileProvider;
+
+ @BeforeClass
+ public static void setUpClass() throws Exception {
+ lib1 = File.createTempFile("lib1", ".jar");
+ lib1Name = lib1.getName();
+ try (ZipOutputStream zos1 = new ZipOutputStream(new FileOutputStream(lib1))) {
+ zos1.putNextEntry(new ZipEntry("a/b/C.class"));
+ zos1.putNextEntry(new ZipEntry("a/b/D.class"));
+ zos1.closeEntry();
+ }
+
+ lib2 = File.createTempFile("lib2", ".jar");
+ lib2Name = lib2.getName();
+ try (ZipOutputStream zos2 = new ZipOutputStream(new FileOutputStream(lib2))) {
+ zos2.putNextEntry(new ZipEntry("a/b/C.class"));
+ zos2.putNextEntry(new ZipEntry("a/b/E.class"));
+ zos2.closeEntry();
+ }
+ }
+
+ @Before
+ public void createProviders() throws Exception {
+ lib1InputFileProvider = new ZipInputFileProvider(lib1.toPath());
+ lib2InputFileProvider = new ZipInputFileProvider(lib2.toPath());
+ }
+
+ @After
+ public void closeProviders() throws Exception {
+ lib1InputFileProvider.close();
+ lib2InputFileProvider.close();
+ }
+
+ @AfterClass
+ public static void tearDownClass() throws Exception {
+ lib1.delete();
+ lib2.delete();
+ }
+
+ @Test
+ public void testClassFoundWithParentLibrary() throws Exception {
+ IndexedInputs indexedLib2 = new IndexedInputs(ImmutableList.of(lib2InputFileProvider));
+ IndexedInputs indexedLib1 = new IndexedInputs(ImmutableList.of(lib1InputFileProvider));
+ IndexedInputs indexedLib2AndLib1 = indexedLib1.withParent(indexedLib2);
+ assertThat(indexedLib2AndLib1.getInputFileProvider("a/b/C.class").toString())
+ .isEqualTo(lib2Name);
+ assertThat(indexedLib2AndLib1.getInputFileProvider("a/b/D.class").toString())
+ .isEqualTo(lib1Name);
+ assertThat(indexedLib2AndLib1.getInputFileProvider("a/b/E.class").toString())
+ .isEqualTo(lib2Name);
+
+ indexedLib2 = new IndexedInputs(ImmutableList.of(lib2InputFileProvider));
+ indexedLib1 = new IndexedInputs(ImmutableList.of(lib1InputFileProvider));
+ IndexedInputs indexedLib1AndLib2 = indexedLib2.withParent(indexedLib1);
+ assertThat(indexedLib1AndLib2.getInputFileProvider("a/b/C.class").toString())
+ .isEqualTo(lib1Name);
+ assertThat(indexedLib1AndLib2.getInputFileProvider("a/b/D.class").toString())
+ .isEqualTo(lib1Name);
+ assertThat(indexedLib1AndLib2.getInputFileProvider("a/b/E.class").toString())
+ .isEqualTo(lib2Name);
+ }
+
+ @Test
+ public void testClassFoundWithoutParentLibrary() throws Exception {
+ IndexedInputs ijLib1Lib2 =
+ new IndexedInputs(ImmutableList.of(lib1InputFileProvider, lib2InputFileProvider));
+ assertThat(ijLib1Lib2.getInputFileProvider("a/b/C.class").toString())
+ .isEqualTo(lib1Name);
+ assertThat(ijLib1Lib2.getInputFileProvider("a/b/D.class").toString())
+ .isEqualTo(lib1Name);
+ assertThat(ijLib1Lib2.getInputFileProvider("a/b/E.class").toString())
+ .isEqualTo(lib2Name);
+
+ IndexedInputs ijLib2Lib1 =
+ new IndexedInputs(ImmutableList.of(lib2InputFileProvider, lib1InputFileProvider));
+ assertThat(ijLib2Lib1.getInputFileProvider("a/b/C.class").toString())
+ .isEqualTo(lib2Name);
+ assertThat(ijLib2Lib1.getInputFileProvider("a/b/D.class").toString())
+ .isEqualTo(lib1Name);
+ assertThat(ijLib2Lib1.getInputFileProvider("a/b/E.class").toString())
+ .isEqualTo(lib2Name);
+ }
+
+ @Test
+ public void testClassNotFound() throws Exception {
+ IndexedInputs ijLib1Lib2 =
+ new IndexedInputs(ImmutableList.of(lib1InputFileProvider, lib2InputFileProvider));
+ assertThat(ijLib1Lib2.getInputFileProvider("a/b/F.class")).isNull();
+ }
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/Java7CompatibilityTest.java b/test/java/com/google/devtools/build/android/desugar/Java7CompatibilityTest.java
new file mode 100644
index 0000000..b8c8b54
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/Java7CompatibilityTest.java
@@ -0,0 +1,122 @@
+// 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;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+
+@RunWith(JUnit4.class)
+public class Java7CompatibilityTest {
+
+ @Test
+ public void testJava7CompatibleInterface() throws Exception {
+ ClassReader reader = new ClassReader(ExtendsDefault.class.getName());
+ ClassTester tester = new ClassTester();
+ reader.accept(new Java7Compatibility(tester, null), 0);
+ assertThat(tester.version).isEqualTo(Opcodes.V1_7);
+ assertThat(tester.bridgeMethods).isEqualTo(0); // make sure we strip bridge methods
+ assertThat(tester.clinitMethods).isEqualTo(1); // make sure we don't strip <clinit>
+ }
+
+ @Test
+ public void testDefaultMethodFails() throws Exception {
+ ClassReader reader = new ClassReader(WithDefault.class.getName());
+ try {
+ reader.accept(new Java7Compatibility(null, null), 0);
+ fail("IllegalArgumentException expected");
+ } catch (IllegalArgumentException expected) {
+ assertThat(expected).hasMessageThat().contains("getVersion()I");
+ }
+ }
+
+ /**
+ * Tests that a class implementing interfaces with bridge methods redeclares those bridges.
+ * This is behavior of javac that we rely on.
+ */
+ @Test
+ public void testConcreteClassRedeclaresBridges() throws Exception {
+ ClassReader reader = new ClassReader(Impl.class.getName());
+ ClassTester tester = new ClassTester();
+ reader.accept(new Java7Compatibility(tester, null), 0);
+ assertThat(tester.version).isEqualTo(Opcodes.V1_7);
+ assertThat(tester.bridgeMethods).isEqualTo(2);
+ }
+
+ private static class ClassTester extends ClassVisitor {
+
+ int version;
+ int bridgeMethods;
+ int clinitMethods;
+
+ private ClassTester() {
+ super(Opcodes.ASM5, null);
+ }
+
+ @Override
+ public void visit(
+ int version,
+ int access,
+ String name,
+ String signature,
+ String superName,
+ String[] interfaces) {
+ this.version = version;
+ super.visit(version, access, name, signature, superName, interfaces);
+ }
+
+ @Override
+ public MethodVisitor visitMethod(
+ int access, String name, String desc, String signature, String[] exceptions) {
+ if (BitFlags.isSet(access, Opcodes.ACC_BRIDGE)) {
+ ++bridgeMethods;
+ }
+ if ("<clinit>".equals(name)) {
+ ++clinitMethods;
+ }
+ return super.visitMethod(access, name, desc, signature, exceptions);
+ }
+ }
+
+ interface WithDefault<T> {
+ default int getVersion() {
+ return 18;
+ }
+ T get();
+ }
+
+ // Javac will generate a default bridge method "Object get()" that Java7Compatibility will remove
+ interface ExtendsDefault<T extends Number> extends WithDefault<T> {
+ public static final Integer X = Integer.valueOf(37);
+ String name();
+ @Override T get();
+ }
+
+ // Javac will generate 2 bridge methods that we *don't* want to remove
+ static class Impl implements ExtendsDefault<Integer> {
+ @Override public Integer get() {
+ return X;
+ }
+ @Override public String name() {
+ return "test";
+ }
+ }
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/MethodInfoTest.java b/test/java/com/google/devtools/build/android/desugar/MethodInfoTest.java
new file mode 100644
index 0000000..ebe9ba6
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/MethodInfoTest.java
@@ -0,0 +1,33 @@
+// Copyright 2017 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 org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Test for {@link MethodInfo} */
+@RunWith(JUnit4.class)
+public class MethodInfoTest {
+
+ @Test
+ public void testMethodInfoAreCorrectlySet() {
+ MethodInfo method = MethodInfo.create("owner", "name", "desc");
+ assertThat(method.owner()).isEqualTo("owner");
+ assertThat(method.name()).isEqualTo("name");
+ assertThat(method.desc()).isEqualTo("desc");
+ }
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/StackMapBugTest.java b/test/java/com/google/devtools/build/android/desugar/StackMapBugTest.java
new file mode 100644
index 0000000..26aa528
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/StackMapBugTest.java
@@ -0,0 +1,45 @@
+// Copyright 2017 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 org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import test.util.TestClassForStackMapFrame;
+
+/** This test case is for testing the fix for b/36654936. */
+@RunWith(JUnit4.class)
+public class StackMapBugTest {
+
+ /** This is a regression test for b/36654936 (external ASM bug 317785) */
+ @Test
+ public void testAsmBug317785() {
+ int result = TestClassForStackMapFrame.testInputForAsmBug317785();
+ assertThat(result).isEqualTo(20);
+ }
+
+ /**
+ * This is a regression test for b/36654936 (external ASM bug 317785). The first attempted fix
+ * cl/152199391 caused stack map frame corruption, which caused the following test to fail.
+ */
+ @Test
+ public void testStackMapFrameCorrectness() {
+ TestClassForStackMapFrame testObject = new TestClassForStackMapFrame();
+ assertThat(testObject.joinIntegers(0)).isEmpty();
+ assertThat(testObject.joinIntegers(1)).isEqualTo("0=Even");
+ assertThat(testObject.joinIntegers(2)).isEqualTo("0=Even,1=Odd");
+ }
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriterTest.java b/test/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriterTest.java
new file mode 100644
index 0000000..587d4f7
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriterTest.java
@@ -0,0 +1,414 @@
+// Copyright 2017 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 static com.google.devtools.build.android.desugar.runtime.ThrowableExtensionTestUtility.getStrategyClassName;
+import static com.google.devtools.build.android.desugar.runtime.ThrowableExtensionTestUtility.getTwrStrategyClassNameSpecifiedInSystemProperty;
+import static com.google.devtools.build.android.desugar.runtime.ThrowableExtensionTestUtility.isMimicStrategy;
+import static com.google.devtools.build.android.desugar.runtime.ThrowableExtensionTestUtility.isNullStrategy;
+import static com.google.devtools.build.android.desugar.runtime.ThrowableExtensionTestUtility.isReuseStrategy;
+import static org.junit.Assert.fail;
+import static org.objectweb.asm.ClassWriter.COMPUTE_MAXS;
+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.runtime.ThrowableExtension;
+import com.google.devtools.build.android.desugar.testdata.ClassUsingTryWithResources;
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.Type;
+
+/** This is the unit test for {@link TryWithResourcesRewriter} */
+@RunWith(JUnit4.class)
+public class TryWithResourcesRewriterTest {
+
+ private final DesugaringClassLoader classLoader =
+ new DesugaringClassLoader(ClassUsingTryWithResources.class.getName());
+ private Class<?> desugaredClass;
+
+ @Before
+ public void setup() {
+ try {
+ desugaredClass = classLoader.findClass(ClassUsingTryWithResources.class.getName());
+ } catch (ClassNotFoundException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ @Test
+ public void testMethodsAreDesugared() {
+ // verify whether the desugared class is indeed desugared.
+ DesugaredThrowableMethodCallCounter origCounter =
+ countDesugaredThrowableMethodCalls(ClassUsingTryWithResources.class);
+ DesugaredThrowableMethodCallCounter desugaredCounter =
+ countDesugaredThrowableMethodCalls(classLoader.classContent, classLoader);
+ /**
+ * In java9, javac creates a helper method {@code $closeResource(Throwable, AutoCloseable)
+ * to close resources. So, the following number 3 is highly dependant on the version of javac.
+ */
+ assertThat(hasAutoCloseable(classLoader.classContent)).isFalse();
+ assertThat(classLoader.numOfTryWithResourcesInvoked.intValue()).isAtLeast(2);
+ assertThat(classLoader.visitedExceptionTypes)
+ .containsExactly(
+ "java/lang/Exception", "java/lang/Throwable", "java/io/UnsupportedEncodingException");
+ assertDesugaringBehavior(origCounter, desugaredCounter);
+ }
+
+ @Test
+ public void testCheckSuppressedExceptionsReturningEmptySuppressedExceptions() {
+ {
+ Throwable[] suppressed = ClassUsingTryWithResources.checkSuppressedExceptions(false);
+ assertThat(suppressed).isEmpty();
+ }
+ try {
+ Throwable[] suppressed =
+ (Throwable[])
+ desugaredClass
+ .getMethod("checkSuppressedExceptions", boolean.class)
+ .invoke(null, Boolean.FALSE);
+ assertThat(suppressed).isEmpty();
+ } catch (Exception e) {
+ e.printStackTrace();
+ throw new AssertionError(e);
+ }
+ }
+
+ @Test
+ public void testPrintStackTraceOfCaughtException() {
+ {
+ String trace = ClassUsingTryWithResources.printStackTraceOfCaughtException();
+ assertThat(trace.toLowerCase()).contains("suppressed");
+ }
+ try {
+ String trace =
+ (String) desugaredClass.getMethod("printStackTraceOfCaughtException").invoke(null);
+
+ if (isMimicStrategy()) {
+ assertThat(trace.toLowerCase()).contains("suppressed");
+ } else if (isReuseStrategy()) {
+ assertThat(trace.toLowerCase()).contains("suppressed");
+ } else if (isNullStrategy()) {
+ assertThat(trace.toLowerCase()).doesNotContain("suppressed");
+ } else {
+ fail("unexpected desugaring strategy " + ThrowableExtension.getStrategy());
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ throw new AssertionError(e);
+ }
+ }
+
+ @Test
+ public void testCheckSuppressedExceptionReturningOneSuppressedException() {
+ {
+ Throwable[] suppressed = ClassUsingTryWithResources.checkSuppressedExceptions(true);
+ assertThat(suppressed).hasLength(1);
+ }
+ try {
+ Throwable[] suppressed =
+ (Throwable[])
+ desugaredClass
+ .getMethod("checkSuppressedExceptions", boolean.class)
+ .invoke(null, Boolean.TRUE);
+
+ if (isMimicStrategy()) {
+ assertThat(suppressed).hasLength(1);
+ } else if (isReuseStrategy()) {
+ assertThat(suppressed).hasLength(1);
+ } else if (isNullStrategy()) {
+ assertThat(suppressed).isEmpty();
+ } else {
+ fail("unexpected desugaring strategy " + ThrowableExtension.getStrategy());
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ throw new AssertionError(e);
+ }
+ }
+
+ @Test
+ public void testSimpleTryWithResources() throws Throwable {
+ {
+ try {
+ ClassUsingTryWithResources.simpleTryWithResources();
+ fail("Expected RuntimeException");
+ } catch (RuntimeException expected) {
+ assertThat(expected.getClass()).isEqualTo(RuntimeException.class);
+ assertThat(expected.getSuppressed()).hasLength(1);
+ assertThat(expected.getSuppressed()[0].getClass()).isEqualTo(IOException.class);
+ }
+ }
+
+ try {
+ try {
+ desugaredClass.getMethod("simpleTryWithResources").invoke(null);
+ fail("Expected RuntimeException");
+ } catch (InvocationTargetException e) {
+ throw e.getCause();
+ }
+ } catch (RuntimeException expected) {
+ 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 " + ThrowableExtension.getStrategy());
+ }
+ }
+ }
+
+ private static void assertDesugaringBehavior(
+ DesugaredThrowableMethodCallCounter orig, DesugaredThrowableMethodCallCounter desugared) {
+ assertThat(desugared.countThrowableGetSuppressed()).isEqualTo(orig.countExtGetSuppressed());
+ assertThat(desugared.countThrowableAddSuppressed()).isEqualTo(orig.countExtAddSuppressed());
+ assertThat(desugared.countThrowablePrintStackTrace()).isEqualTo(orig.countExtPrintStackTrace());
+ assertThat(desugared.countThrowablePrintStackTracePrintStream())
+ .isEqualTo(orig.countExtPrintStackTracePrintStream());
+ assertThat(desugared.countThrowablePrintStackTracePrintWriter())
+ .isEqualTo(orig.countExtPrintStackTracePrintWriter());
+
+ assertThat(orig.countThrowableGetSuppressed()).isEqualTo(desugared.countExtGetSuppressed());
+ // $closeResource is rewritten to ThrowableExtension.closeResource, so addSuppressed() is called
+ // in the runtime library now.
+ assertThat(orig.countThrowableAddSuppressed())
+ .isAtLeast(desugared.countThrowableAddSuppressed());
+ assertThat(orig.countThrowablePrintStackTrace()).isEqualTo(desugared.countExtPrintStackTrace());
+ assertThat(orig.countThrowablePrintStackTracePrintStream())
+ .isEqualTo(desugared.countExtPrintStackTracePrintStream());
+ assertThat(orig.countThrowablePrintStackTracePrintWriter())
+ .isEqualTo(desugared.countExtPrintStackTracePrintWriter());
+
+ assertThat(desugared.countThrowablePrintStackTracePrintStream()).isEqualTo(0);
+ assertThat(desugared.countThrowablePrintStackTracePrintStream()).isEqualTo(0);
+ assertThat(desugared.countThrowablePrintStackTracePrintWriter()).isEqualTo(0);
+ assertThat(desugared.countThrowableAddSuppressed()).isEqualTo(0);
+ assertThat(desugared.countThrowableGetSuppressed()).isEqualTo(0);
+ }
+
+ private static DesugaredThrowableMethodCallCounter countDesugaredThrowableMethodCalls(
+ Class<?> klass) {
+ try {
+ ClassReader reader = new ClassReader(klass.getName());
+ DesugaredThrowableMethodCallCounter counter =
+ new DesugaredThrowableMethodCallCounter(klass.getClassLoader());
+ reader.accept(counter, 0);
+ return counter;
+ } catch (IOException e) {
+ e.printStackTrace();
+ fail(e.toString());
+ return null;
+ }
+ }
+
+ private static DesugaredThrowableMethodCallCounter countDesugaredThrowableMethodCalls(
+ byte[] content, ClassLoader loader) {
+ ClassReader reader = new ClassReader(content);
+ DesugaredThrowableMethodCallCounter counter = new DesugaredThrowableMethodCallCounter(loader);
+ reader.accept(counter, 0);
+ return counter;
+ }
+
+ /** Check whether java.lang.AutoCloseable is used as arguments of any method. */
+ private static boolean hasAutoCloseable(byte[] classContent) {
+ ClassReader reader = new ClassReader(classContent);
+ final AtomicInteger counter = new AtomicInteger();
+ ClassVisitor visitor =
+ new ClassVisitor(Opcodes.ASM5) {
+ @Override
+ public MethodVisitor visitMethod(
+ int access, String name, String desc, String signature, String[] exceptions) {
+ for (Type argumentType : Type.getArgumentTypes(desc)) {
+ if ("Ljava/lang/AutoCloseable;".equals(argumentType.getDescriptor())) {
+ counter.incrementAndGet();
+ }
+ }
+ return null;
+ }
+ };
+ reader.accept(visitor, 0);
+ return counter.get() > 0;
+ }
+
+ private static class DesugaredThrowableMethodCallCounter extends ClassVisitor {
+ private final ClassLoader classLoader;
+ private final Map<String, AtomicInteger> counterMap;
+
+ public DesugaredThrowableMethodCallCounter(ClassLoader loader) {
+ super(ASM5);
+ classLoader = loader;
+ counterMap = new HashMap<>();
+ TryWithResourcesRewriter.TARGET_METHODS
+ .entries()
+ .forEach(entry -> counterMap.put(entry.getKey() + entry.getValue(), new AtomicInteger()));
+ TryWithResourcesRewriter.TARGET_METHODS
+ .entries()
+ .forEach(
+ entry ->
+ counterMap.put(
+ entry.getKey()
+ + TryWithResourcesRewriter.METHOD_DESC_MAP.get(entry.getValue()),
+ new AtomicInteger()));
+ }
+
+ @Override
+ public MethodVisitor visitMethod(
+ int access, String name, String desc, String signature, String[] exceptions) {
+ return new InvokeCounter();
+ }
+
+ private class InvokeCounter extends MethodVisitor {
+
+ public InvokeCounter() {
+ super(ASM5);
+ }
+
+ private boolean isAssignableToThrowable(String owner) {
+ try {
+ Class<?> ownerClass = classLoader.loadClass(owner.replace('/', '.'));
+ return Throwable.class.isAssignableFrom(ownerClass);
+ } catch (ClassNotFoundException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ @Override
+ public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
+ String signature = name + desc;
+ if ((opcode == INVOKEVIRTUAL && isAssignableToThrowable(owner))
+ || (opcode == INVOKESTATIC
+ && Type.getInternalName(ThrowableExtension.class).equals(owner))) {
+ AtomicInteger counter = counterMap.get(signature);
+ if (counter == null) {
+ return;
+ }
+ counter.incrementAndGet();
+ }
+ }
+ }
+
+ public int countThrowableAddSuppressed() {
+ return counterMap.get("addSuppressed(Ljava/lang/Throwable;)V").get();
+ }
+
+ public int countThrowableGetSuppressed() {
+ return counterMap.get("getSuppressed()[Ljava/lang/Throwable;").get();
+ }
+
+ public int countThrowablePrintStackTrace() {
+ return counterMap.get("printStackTrace()V").get();
+ }
+
+ public int countThrowablePrintStackTracePrintStream() {
+ return counterMap.get("printStackTrace(Ljava/io/PrintStream;)V").get();
+ }
+
+ public int countThrowablePrintStackTracePrintWriter() {
+ return counterMap.get("printStackTrace(Ljava/io/PrintWriter;)V").get();
+ }
+
+ public int countExtAddSuppressed() {
+ return counterMap.get("addSuppressed(Ljava/lang/Throwable;Ljava/lang/Throwable;)V").get();
+ }
+
+ public int countExtGetSuppressed() {
+ return counterMap.get("getSuppressed(Ljava/lang/Throwable;)[Ljava/lang/Throwable;").get();
+ }
+
+ public int countExtPrintStackTrace() {
+ return counterMap.get("printStackTrace(Ljava/lang/Throwable;)V").get();
+ }
+
+ public int countExtPrintStackTracePrintStream() {
+ return counterMap.get("printStackTrace(Ljava/lang/Throwable;Ljava/io/PrintStream;)V").get();
+ }
+
+ public int countExtPrintStackTracePrintWriter() {
+ return counterMap.get("printStackTrace(Ljava/lang/Throwable;Ljava/io/PrintWriter;)V").get();
+ }
+ }
+
+ private static class DesugaringClassLoader extends ClassLoader {
+
+ private final String targetedClassName;
+ private Class<?> klass;
+ private byte[] classContent;
+ private final Set<String> visitedExceptionTypes = new HashSet<>();
+ private final AtomicInteger numOfTryWithResourcesInvoked = new AtomicInteger();
+
+ public DesugaringClassLoader(String targetedClassName) {
+ super(DesugaringClassLoader.class.getClassLoader());
+ this.targetedClassName = targetedClassName;
+ }
+
+ @Override
+ protected Class<?> findClass(String name) throws ClassNotFoundException {
+ if (name.equals(targetedClassName)) {
+ if (klass != null) {
+ return klass;
+ }
+ // desugar the class, and return the desugared one.
+ classContent = desugarTryWithResources(name);
+ klass = defineClass(name, classContent, 0, classContent.length);
+ return klass;
+ } else {
+ return super.findClass(name);
+ }
+ }
+
+ private byte[] desugarTryWithResources(String className) {
+ try {
+ ClassReader reader = new ClassReader(className);
+ ClassWriter writer = new ClassWriter(reader, COMPUTE_MAXS);
+ TryWithResourcesRewriter rewriter =
+ new TryWithResourcesRewriter(
+ writer,
+ TryWithResourcesRewriterTest.class.getClassLoader(),
+ visitedExceptionTypes,
+ numOfTryWithResourcesInvoked);
+ reader.accept(rewriter, 0);
+ return writer.toByteArray();
+ } catch (IOException e) {
+ fail(e.toString());
+ return null; // suppress compiler error.
+ }
+ }
+ }
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/capture_lambda_disassembled_golden.txt b/test/java/com/google/devtools/build/android/desugar/capture_lambda_disassembled_golden.txt
new file mode 100644
index 0000000..752dad6
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/capture_lambda_disassembled_golden.txt
@@ -0,0 +1,21 @@
+final class com.google.devtools.build.android.desugar.testdata.CaptureLambda$$Lambda$0 implements java.util.function.Predicate {
+ private final java.lang.String arg$1;
+
+ com.google.devtools.build.android.desugar.testdata.CaptureLambda$$Lambda$0(java.lang.String);
+ Code:
+ 0: aload_0
+ 1: invokespecial #13 // Method java/lang/Object."<init>":()V
+ 4: aload_0
+ 5: aload_1
+ 6: putfield #15 // Field arg$1:Ljava/lang/String;
+ 9: return
+
+ public boolean test(java.lang.Object);
+ Code:
+ 0: aload_0
+ 1: getfield #15 // Field arg$1:Ljava/lang/String;
+ 4: aload_1
+ 5: checkcast #19 // class java/lang/String
+ 8: invokestatic #25 // Method com/google/devtools/build/android/desugar/testdata/CaptureLambda.lambda$prefixed$0$CaptureLambda:(Ljava/lang/String;Ljava/lang/String;)Z
+ 11: ireturn
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/class_with_inherited_abstract_method_disassembled_golden.txt b/test/java/com/google/devtools/build/android/desugar/class_with_inherited_abstract_method_disassembled_golden.txt
new file mode 100644
index 0000000..9ab9eb7
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/class_with_inherited_abstract_method_disassembled_golden.txt
@@ -0,0 +1,4 @@
+Compiled from "Named.java"
+public abstract class com.google.devtools.build.android.desugar.testdata.java8.Named$AbstractName extends com.google.devtools.build.android.desugar.testdata.java8.Named$AbstractNameBase implements com.google.devtools.build.android.desugar.testdata.java8.Named {
+ public com.google.devtools.build.android.desugar.testdata.java8.Named$AbstractName();
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/class_with_lambdas_in_implemented_interface_disassembled_golden.txt b/test/java/com/google/devtools/build/android/desugar/class_with_lambdas_in_implemented_interface_disassembled_golden.txt
new file mode 100644
index 0000000..48a8632
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/class_with_lambdas_in_implemented_interface_disassembled_golden.txt
@@ -0,0 +1,8 @@
+Compiled from "InterfaceMethod.java"
+public class com.google.devtools.build.android.desugar.testdata.java8.InterfaceMethod$Concrete implements com.google.devtools.build.android.desugar.testdata.java8.InterfaceMethod {
+ public com.google.devtools.build.android.desugar.testdata.java8.InterfaceMethod$Concrete();
+ public java.util.List defaultMethodReference(java.util.List);
+ public java.util.List staticMethodReference(java.util.List);
+ public java.util.List lambdaCallsDefaultMethod(java.util.List);
+ public boolean startsWithS(java.lang.String);
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/diff.sh b/test/java/com/google/devtools/build/android/desugar/diff.sh
new file mode 100755
index 0000000..afe2d96
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/diff.sh
@@ -0,0 +1,16 @@
+#!/bin/bash
+#
+# 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.
+diff "$1" "$2"
diff --git a/test/java/com/google/devtools/build/android/desugar/interface_with_desugared_method_bodies_disassembled_golden.txt b/test/java/com/google/devtools/build/android/desugar/interface_with_desugared_method_bodies_disassembled_golden.txt
new file mode 100644
index 0000000..828cee4
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/interface_with_desugared_method_bodies_disassembled_golden.txt
@@ -0,0 +1,7 @@
+Compiled from "InterfaceMethod.java"
+public interface com.google.devtools.build.android.desugar.testdata.java8.InterfaceMethod {
+ public abstract java.util.List<java.lang.String> defaultMethodReference(java.util.List<java.lang.String>);
+ public abstract java.util.List<java.lang.String> staticMethodReference(java.util.List<java.lang.String>);
+ public abstract java.util.List<java.lang.String> lambdaCallsDefaultMethod(java.util.List<java.lang.String>);
+ public abstract boolean startsWithS(java.lang.String);
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/jacoco_0_7_5_default_method.jar b/test/java/com/google/devtools/build/android/desugar/jacoco_0_7_5_default_method.jar
new file mode 100644
index 0000000..1e0deca
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/jacoco_0_7_5_default_method.jar
Binary files differ
diff --git a/test/java/com/google/devtools/build/android/desugar/jacoco_legacy_default_method_companion_disassembled_golden.txt b/test/java/com/google/devtools/build/android/desugar/jacoco_legacy_default_method_companion_disassembled_golden.txt
new file mode 100644
index 0000000..0eb51c1
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/jacoco_legacy_default_method_companion_disassembled_golden.txt
@@ -0,0 +1,37 @@
+public abstract class com.example.gavra.java8coverage.Defaults$$CC {
+ public static boolean[] $jacocoData;
+
+ public static void foo(com.example.gavra.java8coverage.Defaults);
+ Code:
+ 0: invokestatic #12 // Method $jacocoInit$$STATIC$$:()[Z
+ 3: astore_1
+ 4: aload_1
+ 5: iconst_0
+ 6: iconst_1
+ 7: bastore
+ 8: return
+
+ public static void baz$$STATIC$$();
+ Code:
+ 0: invokestatic #12 // Method $jacocoInit$$STATIC$$:()[Z
+ 3: astore_0
+ 4: aload_0
+ 5: iconst_1
+ 6: iconst_1
+ 7: bastore
+ 8: return
+
+ static boolean[] $jacocoInit$$STATIC$$();
+ Code:
+ 0: getstatic #18 // Field $jacocoData:[Z
+ 3: dup
+ 4: ifnonnull 21
+ 7: pop
+ 8: ldc2_w #19 // long -7447229029980688604l
+ 11: ldc #22 // String com/example/gavra/java8coverage/Defaults
+ 13: iconst_2
+ 14: invokestatic #28 // Method org/jacoco/agent/rt/internal_773e439/Offline.getProbes:(JLjava/lang/String;I)[Z
+ 17: dup
+ 18: putstatic #18 // Field $jacocoData:[Z
+ 21: areturn
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/mocked_android_framework/android/os/Build.java b/test/java/com/google/devtools/build/android/desugar/mocked_android_framework/android/os/Build.java
new file mode 100644
index 0000000..43d2f7d
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/mocked_android_framework/android/os/Build.java
@@ -0,0 +1,31 @@
+// Copyright 2017 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 android.os;
+
+/** This class is a standin for android.os.Build for tests running in a JVM */
+public final class Build {
+
+ public static final String SYSTEM_PROPERTY_NAME = "fortest.simulated.android.sdk_int";
+
+ /** A simple mock for the real android.os.Build.VERSION */
+ public static final class VERSION {
+
+ public static final int SDK_INT;
+
+ static {
+ String sdkInt = System.getProperty(SYSTEM_PROPERTY_NAME, "0");
+ SDK_INT = Integer.parseInt(sdkInt);
+ }
+ }
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/runtime/ConcurrentWeakIdentityHashMapTest.java b/test/java/com/google/devtools/build/android/desugar/runtime/ConcurrentWeakIdentityHashMapTest.java
new file mode 100644
index 0000000..2fde7f4
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/runtime/ConcurrentWeakIdentityHashMapTest.java
@@ -0,0 +1,262 @@
+// Copyright 2017 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.runtime;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.testing.GcFinalization;
+import com.google.devtools.build.android.desugar.runtime.ThrowableExtension.ConcurrentWeakIdentityHashMap;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Test for {@link ConcurrentWeakIdentityHashMap}. This test uses multi-threading, and needs GC
+ * sometime to assert weak references, so it could take long.
+ */
+@RunWith(JUnit4.class)
+public class ConcurrentWeakIdentityHashMapTest {
+
+ private final Random random = new Random();
+
+ /**
+ * This method makes sure that after return, all the exceptions in the map should be garbage
+ * collected. .
+ */
+ private static ConcurrentWeakIdentityHashMap
+ testConcurrentWeakIdentityHashMapSingleThreadedHelper(CountDownLatch latch)
+ throws InterruptedException {
+ ConcurrentWeakIdentityHashMap map = new ConcurrentWeakIdentityHashMap();
+ Exception e1 = new ExceptionWithLatch("e1", latch);
+ assertThat(map.get(e1, false)).isNull();
+ assertThat(map.get(e1, true)).isNotNull();
+ assertThat(map.get(e1, true)).isEmpty();
+ assertThat(map.get(e1, false)).isNotNull();
+
+ Exception suppressed1 = new ExceptionWithLatch("suppressed1", latch);
+ map.get(e1, true).add(suppressed1);
+ assertThat(map.get(e1, true)).containsExactly(suppressed1);
+
+ Exception suppressed2 = new ExceptionWithLatch("suppressed2", latch);
+ map.get(e1, true).add(suppressed2);
+ assertThat(map.get(e1, true)).containsExactly(suppressed1, suppressed2);
+
+ assertThat(map.get(suppressed1, false)).isNull();
+ assertThat(map.get(suppressed2, false)).isNull();
+ assertThat(map.size()).isEqualTo(1);
+ assertThat(map.get(suppressed1, true)).isNotNull();
+ assertThat(map.size()).isEqualTo(2);
+ assertThat(map.get(suppressed1, true)).isNotNull();
+ assertThat(map.size()).isEqualTo(2);
+
+ Exception e2 = new ExceptionWithLatch("e2", latch);
+ assertThat(map.get(e2, true)).isNotNull();
+ Exception e3 = new ExceptionWithLatch("e3", latch);
+ assertThat(map.get(e3, true)).isNotNull();
+ assertThat(map.size()).isEqualTo(4);
+ return map;
+ }
+
+ @Test
+ public void testSingleThreadedUse() throws InterruptedException {
+ CountDownLatch latch = new CountDownLatch(5);
+ ConcurrentWeakIdentityHashMap map =
+ testConcurrentWeakIdentityHashMapSingleThreadedHelper(latch);
+ for (int i = 0; i < 5; i++) {
+ map.deleteEmptyKeys();
+ GcFinalization.awaitFullGc();
+ }
+ latch.await(); // wait for e1 to be garbage collected.
+ map.deleteEmptyKeys();
+ assertThat(map.size()).isEqualTo(0);
+ }
+
+ private static Map<Throwable, List<Throwable>> createExceptionWithSuppressed(
+ int numMainExceptions, int numSuppressedPerMain, CountDownLatch latch) {
+ Map<Throwable, List<Throwable>> map = new HashMap<>();
+ for (int i = 0; i < numMainExceptions; ++i) {
+ Exception main = new ExceptionWithLatch("main-" + i, latch);
+ List<Throwable> suppressedList = new ArrayList<>();
+ assertThat(map).doesNotContainKey(main);
+ map.put(main, suppressedList);
+ for (int j = 0; j < numSuppressedPerMain; ++j) {
+ Exception suppressed = new ExceptionWithLatch("suppressed-" + j + "-main-" + i, latch);
+ suppressedList.add(suppressed);
+ }
+ }
+ return map;
+ }
+
+ private ConcurrentWeakIdentityHashMap testFunctionalCorrectnessForMultiThreadedUse(
+ int numMainExceptions, int numSuppressedPerMain, CountDownLatch latch)
+ throws InterruptedException {
+ Map<Throwable, List<Throwable>> exceptionWithSuppressed =
+ createExceptionWithSuppressed(numMainExceptions, numSuppressedPerMain, latch);
+ assertThat(exceptionWithSuppressed).hasSize(numMainExceptions);
+ List<Pair> allPairs =
+ exceptionWithSuppressed
+ .entrySet()
+ .stream()
+ .flatMap(
+ entry -> entry.getValue().stream().map(value -> new Pair(entry.getKey(), value)))
+ .collect(Collectors.toList());
+ Collections.shuffle(allPairs);
+ ConcurrentWeakIdentityHashMap map = new ConcurrentWeakIdentityHashMap();
+ List<Worker> workers =
+ IntStream.range(1, 11) // ten threads.
+ .mapToObj(i -> new Worker("worker-" + i, map))
+ .collect(Collectors.toList());
+
+ // Assign tasks to workers.
+ Iterator<Worker> workIterator = workers.iterator();
+ for (Pair pair : allPairs) {
+ if (!workIterator.hasNext()) {
+ workIterator = workers.iterator();
+ }
+ assertThat(workIterator.hasNext()).isTrue();
+ workIterator.next().exceptionList.add(pair);
+ }
+
+ // Execute all the workers.
+ ExecutorService executorService = Executors.newFixedThreadPool(workers.size());
+ workers.forEach(executorService::execute);
+ executorService.shutdown();
+ executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.DAYS); // wait for completion.
+ exceptionWithSuppressed
+ .entrySet()
+ .forEach(
+ entry -> {
+ assertThat(map.get(entry.getKey(), false)).isNotNull();
+ assertThat(map.get(entry.getKey(), false))
+ .containsExactlyElementsIn(exceptionWithSuppressed.get(entry.getKey()));
+ });
+ return map;
+ }
+
+ private void testMultiThreadedUse(int numMainExceptions, int numSuppressedPerMain)
+ throws InterruptedException {
+ CountDownLatch latch = new CountDownLatch(numMainExceptions * numSuppressedPerMain);
+ ConcurrentWeakIdentityHashMap map =
+ testFunctionalCorrectnessForMultiThreadedUse(
+ numMainExceptions, numSuppressedPerMain, latch);
+ /*
+ * Calling the following methods multiple times to make sure the keys are garbage collected,
+ * and their corresponding entries are removed from the map.
+ */
+ map.deleteEmptyKeys();
+ GcFinalization.awaitFullGc();
+ map.deleteEmptyKeys();
+ GcFinalization.awaitFullGc();
+ map.deleteEmptyKeys();
+
+ assertThat(map.size()).isEqualTo(0);
+ }
+
+ @Test
+ public void testMultiThreadedUseMedium() throws InterruptedException {
+ for (int i = 0; i < 10; ++i) {
+ testMultiThreadedUse(50, 100);
+ }
+ }
+
+ @Test
+ public void testMultiThreadedUseLarge() throws InterruptedException {
+ for (int i = 0; i < 5; ++i) {
+ testMultiThreadedUse(100, 100);
+ }
+ }
+
+ @Test
+ public void testMultiThreadedUseSmall() throws InterruptedException {
+ for (int i = 0; i < 10; ++i) {
+ testMultiThreadedUse(20, 100);
+ }
+ }
+
+ private static class ExceptionWithLatch extends Exception {
+ private final CountDownLatch latch;
+
+ private ExceptionWithLatch(String message, CountDownLatch latch) {
+ super(message);
+ this.latch = latch;
+ }
+
+ @Override
+ public String toString() {
+ return this.getMessage();
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ latch.countDown();
+ }
+ }
+
+ private static class Pair {
+ final Throwable throwable;
+ final Throwable suppressed;
+
+ public Pair(Throwable throwable, Throwable suppressed) {
+ this.throwable = throwable;
+ this.suppressed = suppressed;
+ }
+ }
+
+ private class Worker implements Runnable {
+ private final ConcurrentWeakIdentityHashMap map;
+ private final List<Pair> exceptionList = new ArrayList<>();
+ private final String name;
+
+ private Worker(String name, ConcurrentWeakIdentityHashMap map) {
+ this.name = name;
+ this.map = map;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public void run() {
+ Iterator<Pair> iterator = exceptionList.iterator();
+ while (iterator.hasNext()) {
+ int timeToSleep = random.nextInt(3);
+ if (random.nextBoolean() && timeToSleep > 0) {
+ try {
+ Thread.sleep(timeToSleep); // add randomness to the scheduler.
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ Pair pair = iterator.next();
+ List<Throwable> suppressed = map.get(pair.throwable, true);
+ System.out.printf("add suppressed %s to %s\n", pair.suppressed, pair.throwable);
+ suppressed.add(pair.suppressed);
+ }
+ }
+ }
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/runtime/ThrowableExtensionTest.java b/test/java/com/google/devtools/build/android/desugar/runtime/ThrowableExtensionTest.java
new file mode 100644
index 0000000..5ac88b8
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/runtime/ThrowableExtensionTest.java
@@ -0,0 +1,453 @@
+// Copyright 2017 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.runtime;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.devtools.build.android.desugar.runtime.ThrowableExtension.MimicDesugaringStrategy.SUPPRESSED_PREFIX;
+import static com.google.devtools.build.android.desugar.runtime.ThrowableExtensionTestUtility.getTwrStrategyClassNameSpecifiedInSystemProperty;
+import static com.google.devtools.build.android.desugar.runtime.ThrowableExtensionTestUtility.isNullStrategy;
+import static com.google.devtools.build.lib.testutil.MoreAsserts.assertThrows;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.fail;
+
+import com.google.devtools.build.android.desugar.runtime.ThrowableExtension.MimicDesugaringStrategy;
+import com.google.devtools.build.android.desugar.runtime.ThrowableExtension.NullDesugaringStrategy;
+import com.google.devtools.build.android.desugar.runtime.ThrowableExtension.ReuseDesugaringStrategy;
+import java.io.BufferedWriter;
+import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.PrintStream;
+import java.io.PrintWriter;
+import java.util.function.Consumer;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Test case for {@link ThrowableExtension} */
+@RunWith(JUnit4.class)
+public class ThrowableExtensionTest {
+
+ /**
+ * This test tests the behavior of closing resources via reflection. This is only enabled below
+ * API 19. So, if the API level is 19 or above, this test will simply skip.
+ */
+ @Test
+ public void testCloseResourceViaReflection() throws Throwable {
+ class Resource extends AbstractResource {
+ protected Resource(boolean exceptionOnClose) {
+ super(exceptionOnClose);
+ }
+
+ public void close() throws Exception {
+ super.internalClose();
+ }
+ }
+ if (ThrowableExtension.API_LEVEL >= 19) {
+ return;
+ }
+ {
+ Resource r = new Resource(false);
+ assertThat(r.isClosed()).isFalse();
+ ThrowableExtension.closeResource(null, r);
+ assertThat(r.isClosed()).isTrue();
+ }
+ {
+ Resource r = new Resource(true);
+ assertThat(r.isClosed()).isFalse();
+ assertThrows(IOException.class, () -> ThrowableExtension.closeResource(null, r));
+ }
+ {
+ Resource r = new Resource(false);
+ assertThat(r.isClosed()).isFalse();
+ ThrowableExtension.closeResource(new Exception(), r);
+ assertThat(r.isClosed()).isTrue();
+ }
+ {
+ Resource r = new Resource(true);
+ assertThat(r.isClosed()).isFalse();
+ assertThrows(Exception.class, () -> ThrowableExtension.closeResource(new Exception(), r));
+ }
+ }
+
+ /**
+ * Test the new method closeResources() in the runtime library.
+ *
+ * <p>The method is introduced to fix b/37167433.
+ */
+ @Test
+ public void testCloseResource() throws Throwable {
+
+ /**
+ * A resource implementing the interface AutoCloseable. This interface is only available since
+ * API 19.
+ */
+ class AutoCloseableResource extends AbstractResource implements AutoCloseable {
+
+ protected AutoCloseableResource(boolean exceptionOnClose) {
+ super(exceptionOnClose);
+ }
+
+ @Override
+ public void close() throws Exception {
+ internalClose();
+ }
+ }
+
+ /** A resource implementing the interface Closeable. */
+ class CloseableResource extends AbstractResource implements Closeable {
+
+ protected CloseableResource(boolean exceptionOnClose) {
+ super(exceptionOnClose);
+ }
+
+ @Override
+ public void close() throws IOException {
+ internalClose();
+ }
+ }
+
+ {
+ CloseableResource r = new CloseableResource(false);
+ assertThat(r.isClosed()).isFalse();
+ ThrowableExtension.closeResource(null, r);
+ assertThat(r.isClosed()).isTrue();
+ }
+ {
+ CloseableResource r = new CloseableResource(false);
+ assertThat(r.isClosed()).isFalse();
+ Exception suppressor = new Exception();
+ ThrowableExtension.closeResource(suppressor, r);
+ assertThat(r.isClosed()).isTrue();
+ assertThat(ThrowableExtension.getSuppressed(suppressor)).isEmpty();
+ }
+ {
+ CloseableResource r = new CloseableResource(true);
+ assertThat(r.isClosed()).isFalse();
+ assertThrows(IOException.class, () -> ThrowableExtension.closeResource(null, r));
+ assertThat(r.isClosed()).isFalse();
+ }
+ {
+ CloseableResource r = new CloseableResource(true);
+ assertThat(r.isClosed()).isFalse();
+ Exception suppressor = new Exception();
+ assertThrows(Exception.class, () -> ThrowableExtension.closeResource(suppressor, r));
+ assertThat(r.isClosed()).isFalse(); // Failed to close.
+ if (!isNullStrategy()) {
+ assertThat(ThrowableExtension.getSuppressed(suppressor)).hasLength(1);
+ assertThat(ThrowableExtension.getSuppressed(suppressor)[0].getClass())
+ .isEqualTo(IOException.class);
+ }
+ }
+ {
+ AutoCloseableResource r = new AutoCloseableResource(false);
+ assertThat(r.isClosed()).isFalse();
+ ThrowableExtension.closeResource(null, r);
+ assertThat(r.isClosed()).isTrue();
+ }
+ {
+ AutoCloseableResource r = new AutoCloseableResource(false);
+ assertThat(r.isClosed()).isFalse();
+ Exception suppressor = new Exception();
+ ThrowableExtension.closeResource(suppressor, r);
+ assertThat(r.isClosed()).isTrue();
+ assertThat(ThrowableExtension.getSuppressed(suppressor)).isEmpty();
+ }
+ {
+ AutoCloseableResource r = new AutoCloseableResource(true);
+ assertThat(r.isClosed()).isFalse();
+ assertThrows(IOException.class, () -> ThrowableExtension.closeResource(null, r));
+ assertThat(r.isClosed()).isFalse();
+ }
+ {
+ AutoCloseableResource r = new AutoCloseableResource(true);
+ assertThat(r.isClosed()).isFalse();
+ Exception suppressor = new Exception();
+ assertThrows(Exception.class, () -> ThrowableExtension.closeResource(suppressor, r));
+ assertThat(r.isClosed()).isFalse(); // Failed to close.
+ if (!isNullStrategy()) {
+ assertThat(ThrowableExtension.getSuppressed(suppressor)).hasLength(1);
+ assertThat(ThrowableExtension.getSuppressed(suppressor)[0].getClass())
+ .isEqualTo(IOException.class);
+ }
+ assertThat(r.isClosed()).isFalse();
+ }
+ }
+
+ /**
+ * LightweightStackTraceRecorder tracks the calls of various printStackTrace(*), and ensures that
+ *
+ * <p>suppressed exceptions are printed only once.
+ */
+ @Test
+ public void testLightweightStackTraceRecorder() throws IOException {
+ MimicDesugaringStrategy strategy = new MimicDesugaringStrategy();
+ ExceptionForTest receiver = new ExceptionForTest(strategy);
+ FileNotFoundException suppressed = new FileNotFoundException();
+ strategy.addSuppressed(receiver, suppressed);
+
+ String trace = printStackTraceStderrToString(() -> strategy.printStackTrace(receiver));
+ assertThat(trace).contains(SUPPRESSED_PREFIX);
+ assertThat(countOccurrences(trace, SUPPRESSED_PREFIX)).isEqualTo(1);
+ }
+
+ @Test
+ public void testMimicDesugaringStrategy() throws IOException {
+ MimicDesugaringStrategy strategy = new MimicDesugaringStrategy();
+ IOException receiver = new IOException();
+ FileNotFoundException suppressed = new FileNotFoundException();
+ strategy.addSuppressed(receiver, suppressed);
+
+ assertThat(
+ printStackTracePrintStreamToString(
+ stream -> strategy.printStackTrace(receiver, stream)))
+ .contains(SUPPRESSED_PREFIX);
+
+ assertThat(
+ printStackTracePrintWriterToString(
+ writer -> strategy.printStackTrace(receiver, writer)))
+ .contains(SUPPRESSED_PREFIX);
+
+ assertThat(printStackTraceStderrToString(() -> strategy.printStackTrace(receiver)))
+ .contains(SUPPRESSED_PREFIX);
+ }
+
+ private void testThrowableExtensionWithMimicDesugaringStrategy() throws IOException {
+ IOException receiver = new IOException();
+ FileNotFoundException suppressed = new FileNotFoundException();
+ ThrowableExtension.addSuppressed(receiver, suppressed);
+
+ assertThat(
+ printStackTracePrintStreamToString(
+ stream -> ThrowableExtension.printStackTrace(receiver, stream)))
+ .contains(SUPPRESSED_PREFIX);
+ assertThat(
+ printStackTracePrintWriterToString(
+ writer -> ThrowableExtension.printStackTrace(receiver, writer)))
+ .contains(SUPPRESSED_PREFIX);
+ assertThat(printStackTraceStderrToString(() -> ThrowableExtension.printStackTrace(receiver)))
+ .contains(SUPPRESSED_PREFIX);
+ }
+
+ private interface PrintStackTraceCaller {
+ void printStackTrace();
+ }
+
+ private static String printStackTraceStderrToString(PrintStackTraceCaller caller)
+ throws IOException {
+ PrintStream err = System.err;
+ try (ByteArrayOutputStream stream = new ByteArrayOutputStream()) {
+ PrintStream newErr = new PrintStream(stream);
+ System.setErr(newErr);
+ caller.printStackTrace();
+ newErr.flush();
+ return stream.toString();
+ } finally {
+ System.setErr(err);
+ }
+ }
+
+ private static String printStackTracePrintStreamToString(Consumer<PrintStream> caller)
+ throws IOException {
+ try (ByteArrayOutputStream stream = new ByteArrayOutputStream()) {
+ PrintStream printStream = new PrintStream(stream);
+ caller.accept(printStream);
+ printStream.flush();
+ return stream.toString();
+ }
+ }
+
+ private static String printStackTracePrintWriterToString(Consumer<PrintWriter> caller)
+ throws IOException {
+ try (ByteArrayOutputStream stream = new ByteArrayOutputStream()) {
+ PrintWriter printWriter =
+ new PrintWriter(new BufferedWriter(new OutputStreamWriter(stream, UTF_8)));
+ caller.accept(printWriter);
+ printWriter.flush();
+ return stream.toString();
+ }
+ }
+
+ @Test
+ public void testNullDesugaringStrategy() throws IOException {
+ NullDesugaringStrategy strategy = new NullDesugaringStrategy();
+ IOException receiver = new IOException();
+ FileNotFoundException suppressed = new FileNotFoundException();
+ strategy.addSuppressed(receiver, suppressed);
+ assertThat(strategy.getSuppressed(receiver)).isEmpty();
+
+ strategy.addSuppressed(receiver, suppressed);
+ assertThat(strategy.getSuppressed(receiver)).isEmpty();
+
+ assertThat(printStackTracePrintStreamToString(stream -> receiver.printStackTrace(stream)))
+ .isEqualTo(
+ printStackTracePrintStreamToString(
+ stream -> strategy.printStackTrace(receiver, stream)));
+
+ assertThat(printStackTracePrintWriterToString(receiver::printStackTrace))
+ .isEqualTo(
+ printStackTracePrintWriterToString(
+ writer -> strategy.printStackTrace(receiver, writer)));
+
+ assertThat(printStackTraceStderrToString(receiver::printStackTrace))
+ .isEqualTo(printStackTraceStderrToString(() -> strategy.printStackTrace(receiver)));
+ }
+
+ private void testThrowableExtensionWithNullDesugaringStrategy() throws IOException {
+ IOException receiver = new IOException();
+ FileNotFoundException suppressed = new FileNotFoundException();
+ ThrowableExtension.addSuppressed(receiver, suppressed);
+ assertThat(ThrowableExtension.getSuppressed(receiver)).isEmpty();
+
+ ThrowableExtension.addSuppressed(receiver, suppressed);
+ assertThat(ThrowableExtension.getSuppressed(receiver)).isEmpty();
+
+ assertThat(printStackTracePrintStreamToString(stream -> receiver.printStackTrace(stream)))
+ .isEqualTo(
+ printStackTracePrintStreamToString(
+ stream -> ThrowableExtension.printStackTrace(receiver, stream)));
+ assertThat(printStackTracePrintWriterToString(receiver::printStackTrace))
+ .isEqualTo(
+ printStackTracePrintWriterToString(
+ writer -> ThrowableExtension.printStackTrace(receiver, writer)));
+
+ assertThat(printStackTraceStderrToString(receiver::printStackTrace))
+ .isEqualTo(
+ printStackTraceStderrToString(() -> ThrowableExtension.printStackTrace(receiver)));
+ }
+
+ @Test
+ public void testReuseDesugaringStrategy() throws IOException {
+ ReuseDesugaringStrategy strategy = new ReuseDesugaringStrategy();
+ IOException receiver = new IOException();
+ FileNotFoundException suppressed = new FileNotFoundException();
+ strategy.addSuppressed(receiver, suppressed);
+ assertThat(strategy.getSuppressed(receiver))
+ .asList()
+ .containsExactly((Object[]) receiver.getSuppressed());
+
+ assertThat(printStackTracePrintStreamToString(stream -> receiver.printStackTrace(stream)))
+ .isEqualTo(
+ printStackTracePrintStreamToString(
+ stream -> strategy.printStackTrace(receiver, stream)));
+
+ assertThat(printStackTracePrintWriterToString(receiver::printStackTrace))
+ .isEqualTo(
+ printStackTracePrintWriterToString(
+ writer -> strategy.printStackTrace(receiver, writer)));
+ assertThat(printStackTraceStderrToString(receiver::printStackTrace))
+ .isEqualTo(printStackTraceStderrToString(() -> strategy.printStackTrace(receiver)));
+ }
+
+ private void testThrowableExtensionWithReuseDesugaringStrategy() throws IOException {
+ IOException receiver = new IOException();
+ FileNotFoundException suppressed = new FileNotFoundException();
+ ThrowableExtension.addSuppressed(receiver, suppressed);
+ assertThat(ThrowableExtension.getSuppressed(receiver))
+ .asList()
+ .containsExactly((Object[]) receiver.getSuppressed());
+
+ assertThat(printStackTracePrintStreamToString(receiver::printStackTrace))
+ .isEqualTo(
+ printStackTracePrintStreamToString(
+ stream -> ThrowableExtension.printStackTrace(receiver, stream)));
+
+ assertThat(printStackTracePrintWriterToString(receiver::printStackTrace))
+ .isEqualTo(
+ printStackTracePrintWriterToString(
+ writer -> ThrowableExtension.printStackTrace(receiver, writer)));
+
+ assertThat(printStackTraceStderrToString(receiver::printStackTrace))
+ .isEqualTo(
+ printStackTraceStderrToString(() -> ThrowableExtension.printStackTrace(receiver)));
+ }
+
+ /** This class */
+ private static class ExceptionForTest extends Exception {
+
+ private final MimicDesugaringStrategy strategy;
+
+ public ExceptionForTest(MimicDesugaringStrategy strategy) {
+ this.strategy = strategy;
+ }
+
+ @Override
+ public void printStackTrace() {
+ this.printStackTrace(System.err);
+ }
+
+ /**
+ * This method should call this.printStackTrace(PrintWriter) directly. I deliberately change it
+ * to strategy.printStackTrace(Throwable, PrintWriter) to simulate the behavior of Desguar, that
+ * is, the direct call is intercepted and redirected to ThrowableExtension.
+ */
+ @Override
+ public void printStackTrace(PrintStream s) {
+ this.strategy.printStackTrace(
+ this, new PrintWriter(new BufferedWriter(new OutputStreamWriter(s, UTF_8))));
+ }
+ }
+
+ @Test
+ public void testStrategySelection() throws ClassNotFoundException, IOException {
+ String expectedStrategyClassName = getTwrStrategyClassNameSpecifiedInSystemProperty();
+ assertThat(expectedStrategyClassName).isNotEmpty();
+ assertThat(expectedStrategyClassName)
+ .isEqualTo(ThrowableExtension.STRATEGY.getClass().getName());
+
+ Class<?> expectedStrategyClass = Class.forName(expectedStrategyClassName);
+ if (expectedStrategyClass.equals(ReuseDesugaringStrategy.class)) {
+ testThrowableExtensionWithReuseDesugaringStrategy();
+ } else if (expectedStrategyClass.equals(MimicDesugaringStrategy.class)) {
+ testThrowableExtensionWithMimicDesugaringStrategy();
+ } else if (expectedStrategyClass.equals(NullDesugaringStrategy.class)) {
+ testThrowableExtensionWithNullDesugaringStrategy();
+ } else {
+ fail("unrecognized expected strategy class " + expectedStrategyClassName);
+ }
+ }
+
+ private static int countOccurrences(String string, String substring) {
+ int i = 0;
+ int count = 0;
+ while ((i = string.indexOf(substring, i)) >= 0) {
+ ++count;
+ i = i + string.length();
+ }
+ return count;
+ }
+
+ /** A mocked closeable class, which we can query the closedness. */
+ private abstract static class AbstractResource {
+ private final boolean exceptionOnClose;
+ private boolean closed;
+
+ protected AbstractResource(boolean exceptionOnClose) {
+ this.exceptionOnClose = exceptionOnClose;
+ }
+
+ boolean isClosed() {
+ return closed;
+ }
+
+ void internalClose() throws IOException {
+ if (exceptionOnClose) {
+ throw new IOException("intended exception");
+ }
+ closed = true;
+ }
+ }
+}
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
new file mode 100644
index 0000000..b65b8bd
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/runtime/ThrowableExtensionTestUtility.java
@@ -0,0 +1,64 @@
+// Copyright 2017 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.runtime;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.lang.reflect.Method;
+
+/**
+ * A utility class for testing ThrowableExtension. It uses reflection to get the strategy name, so
+ * as to avoid dependency on the runtime library. This is beneficial, because we can test whether
+ * the runtime library is on the classpath.
+ */
+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);
+ assertThat(className).isNotEmpty();
+ return className;
+ }
+
+ private static final String THROWABLE_EXTENSION_CLASS_NAME =
+ "com.google.devtools.build.android.desugar.runtime.ThrowableExtension";
+
+ private static boolean isStrategyOfClass(String className) {
+ return getStrategyClassName().equals(className);
+ }
+
+ public static String getStrategyClassName() {
+ try {
+ Class<?> klass = Class.forName(THROWABLE_EXTENSION_CLASS_NAME);
+ Method method = klass.getMethod("getStrategy");
+ Object strategy = method.invoke(null);
+ return strategy.getClass().getName();
+ } catch (Throwable e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ public static boolean isMimicStrategy() {
+ return isStrategyOfClass(THROWABLE_EXTENSION_CLASS_NAME + "$MimicDesugaringStrategy");
+ }
+
+ public static boolean isNullStrategy() {
+ return isStrategyOfClass(THROWABLE_EXTENSION_CLASS_NAME + "$NullDesugaringStrategy");
+ }
+
+ public static boolean isReuseStrategy() {
+ return isStrategyOfClass(THROWABLE_EXTENSION_CLASS_NAME + "$ReuseDesugaringStrategy");
+ }
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/simple_instance_method_reference_disassembled_golden.txt b/test/java/com/google/devtools/build/android/desugar/simple_instance_method_reference_disassembled_golden.txt
new file mode 100644
index 0000000..7f26e7e
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/simple_instance_method_reference_disassembled_golden.txt
@@ -0,0 +1,21 @@
+final class com.google.devtools.build.android.desugar.testdata.MethodReferenceSuperclass$$Lambda$0 implements java.util.function.Predicate {
+ private final com.google.devtools.build.android.desugar.testdata.MethodReferenceSuperclass arg$1;
+
+ com.google.devtools.build.android.desugar.testdata.MethodReferenceSuperclass$$Lambda$0(com.google.devtools.build.android.desugar.testdata.MethodReferenceSuperclass);
+ Code:
+ 0: aload_0
+ 1: invokespecial #13 // Method java/lang/Object."<init>":()V
+ 4: aload_0
+ 5: aload_1
+ 6: putfield #15 // Field arg$1:Lcom/google/devtools/build/android/desugar/testdata/MethodReferenceSuperclass;
+ 9: return
+
+ public boolean test(java.lang.Object);
+ Code:
+ 0: aload_0
+ 1: getfield #15 // Field arg$1:Lcom/google/devtools/build/android/desugar/testdata/MethodReferenceSuperclass;
+ 4: aload_1
+ 5: checkcast #19 // class java/lang/String
+ 8: invokevirtual #25 // Method com/google/devtools/build/android/desugar/testdata/MethodReferenceSuperclass.bridge$lambda$0$MethodReferenceSuperclass:(Ljava/lang/String;)Z
+ 11: ireturn
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/stateless_lambda_disassembled_golden.txt b/test/java/com/google/devtools/build/android/desugar/stateless_lambda_disassembled_golden.txt
new file mode 100644
index 0000000..c297c04
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/stateless_lambda_disassembled_golden.txt
@@ -0,0 +1,24 @@
+final class com.google.devtools.build.android.desugar.testdata.Lambda$$Lambda$0 implements java.util.function.Predicate {
+ static final java.util.function.Predicate $instance;
+
+ private com.google.devtools.build.android.desugar.testdata.Lambda$$Lambda$0();
+ Code:
+ 0: aload_0
+ 1: invokespecial #10 // Method java/lang/Object."<init>":()V
+ 4: return
+
+ public boolean test(java.lang.Object);
+ Code:
+ 0: aload_1
+ 1: checkcast #14 // class java/lang/String
+ 4: invokestatic #20 // Method com/google/devtools/build/android/desugar/testdata/Lambda.lambda$as$0$Lambda:(Ljava/lang/String;)Z
+ 7: ireturn
+
+ static {};
+ Code:
+ 0: new #2 // class com/google/devtools/build/android/desugar/testdata/Lambda$$Lambda$0
+ 3: dup
+ 4: invokespecial #24 // Method "<init>":()V
+ 7: putstatic #26 // Field $instance:Ljava/util/function/Predicate;
+ 10: return
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/static_initializer_of_functional_interface_should_not_execute.sh b/test/java/com/google/devtools/build/android/desugar/static_initializer_of_functional_interface_should_not_execute.sh
new file mode 100755
index 0000000..32b4c5a
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/static_initializer_of_functional_interface_should_not_execute.sh
@@ -0,0 +1,22 @@
+#!/bin/bash -e
+#
+# 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.
+
+# Test whether Desugar runs static initializers of interfaces.
+if grep "THIS STRING IS NOT EXPECTED TO APPEAR IN THE OUTPUT OF DESUGAR!!!" "${1}" ; then
+ exit 1
+else
+ exit 0
+fi
diff --git a/test/java/com/google/devtools/build/android/desugar/testdata/CaptureLambda.java b/test/java/com/google/devtools/build/android/desugar/testdata/CaptureLambda.java
new file mode 100644
index 0000000..6c4b6f5
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/testdata/CaptureLambda.java
@@ -0,0 +1,33 @@
+// 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.testdata;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class CaptureLambda {
+
+ private final List<String> names;
+
+ public CaptureLambda(List<String> names) {
+ this.names = names;
+ }
+
+ public List<String> prefixed(String prefix) {
+ return names
+ .stream()
+ .filter(n -> n.startsWith(prefix))
+ .collect(Collectors.toList());
+ }
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/testdata/ClassCallingLongCompare.java b/test/java/com/google/devtools/build/android/desugar/testdata/ClassCallingLongCompare.java
new file mode 100644
index 0000000..1026437
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/testdata/ClassCallingLongCompare.java
@@ -0,0 +1,51 @@
+// 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.testdata;
+
+/** This class calls Long.compare(long, long) */
+public class ClassCallingLongCompare {
+
+ public static int compareLongByCallingLong_compare(long a, long b) {
+ return Long.compare(a, b);
+ }
+
+ public static String compareLongByCallingLong_compare2(long a, long b) {
+ if (Long.compare(a, b) == 0) {
+ return "e";
+ }
+ if (Long.compare(a, b) > 0) {
+ return "g";
+ }
+ if (Long.compare(a, b) < 0) {
+ return "l";
+ }
+ throw new AssertionError("unreachable");
+ }
+
+ public static int compareLongWithLambda(long a, long b) {
+ return internalCompare(a, b, (long l1, long l2) -> Long.compare(l1, l2));
+ }
+
+ public static int compareLongWithMethodReference(long a, long b) {
+ return internalCompare(a, b, Long::compare);
+ }
+
+ private static interface LongCmpFunc {
+ int compare(long a, long b);
+ }
+
+ private static int internalCompare(long a, long b, LongCmpFunc func) {
+ return func.compare(a, b);
+ }
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/testdata/ClassCallingRequireNonNull.java b/test/java/com/google/devtools/build/android/desugar/testdata/ClassCallingRequireNonNull.java
new file mode 100644
index 0000000..3823f1d
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/testdata/ClassCallingRequireNonNull.java
@@ -0,0 +1,53 @@
+// 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.testdata;
+
+import java.util.Objects;
+import java.util.function.IntSupplier;
+
+/** This class is for the testing of desugaring calls to Objects.requireNonNull(Object o...) */
+public class ClassCallingRequireNonNull {
+
+ public static int getStringLengthWithMethodReference(String s) {
+ return toInt(s::length);
+ }
+
+ public static int toInt(IntSupplier function) {
+ return function.getAsInt();
+ }
+
+ public static int getStringLengthWithLambdaAndExplicitCallToRequireNonNull(final String s) {
+ return toInt(() -> Objects.requireNonNull(s).length());
+ }
+
+ public static char getFirstCharVersionOne(String string) {
+ Objects.requireNonNull(string);
+ return string.charAt(0);
+ }
+
+ public static char getFirstCharVersionTwo(String string) {
+ string = Objects.requireNonNull(string);
+ return string.charAt(0);
+ }
+
+ public static char callRequireNonNullWithArgumentString(String string) {
+ string = Objects.requireNonNull(string, "the string should not be null");
+ return string.charAt(0);
+ }
+
+ public static char callRequireNonNullWithArgumentSupplier(String string) {
+ string = Objects.requireNonNull(string, () -> "the string should not be null");
+ return string.charAt(0);
+ }
+}
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
new file mode 100644
index 0000000..c340c84
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/testdata/ClassUsingTryWithResources.java
@@ -0,0 +1,88 @@
+// Copyright 2017 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;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.io.UnsupportedEncodingException;
+
+/**
+ * This is a test subject for {@link
+ * com.google.devtools.build.android.desugar.TryWithResourcesRewriter}
+ */
+public class ClassUsingTryWithResources {
+
+ /**
+ * A simple resource, which always throws an exception when being closed.
+ *
+ * <p>Note that we need to implement java.io.Closeable instead of java.lang.AutoCloseable, because
+ * AutoCloseable is not available below API 19
+ *
+ * <p>java9 will emit $closeResource(Throwable, AutoCloseable) for the following class.
+ */
+ public static class SimpleResource implements Closeable {
+
+ public void call(boolean throwException) {
+ if (throwException) {
+ throw new RuntimeException("exception in call()");
+ }
+ }
+
+ @Override
+ public void close() throws IOException {
+ throw new IOException("exception in close().");
+ }
+ }
+
+ /** 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.
+ try (SimpleResource resource = new SimpleResource()) {
+ resource.call(true);
+ }
+ }
+
+ public static Throwable[] checkSuppressedExceptions(boolean throwException) {
+ // Throwable.addSuppressed(Throwable) should be called in the following block.
+ try (SimpleResource resource = new SimpleResource()) {
+ resource.call(throwException);
+ } catch (Exception e) {
+ return e.getSuppressed(); // getSuppressed() is called.
+ }
+ return new Throwable[0];
+ }
+
+ public static String printStackTraceOfCaughtException() {
+ try {
+ simpleTryWithResources();
+ } catch (Exception e) {
+ PrintStream err = System.err;
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
+ try {
+ System.setErr(new PrintStream(stream, true, "utf-8"));
+ e.printStackTrace();
+ } catch (UnsupportedEncodingException e1) {
+ throw new AssertionError(e1);
+ } finally {
+ System.setErr(err);
+ }
+ return new String(stream.toByteArray(), UTF_8);
+ }
+ return "";
+ }
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/testdata/ConcreteFunction.java b/test/java/com/google/devtools/build/android/desugar/testdata/ConcreteFunction.java
new file mode 100644
index 0000000..69bacdc
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/testdata/ConcreteFunction.java
@@ -0,0 +1,54 @@
+// 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.testdata;
+
+import com.google.devtools.build.android.desugar.testdata.separate.SeparateInterface;
+import java.util.List;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+public class ConcreteFunction implements SpecializedFunction<String, Long> {
+ @Override
+ public Long apply(String input) {
+ return Long.valueOf(input);
+ }
+
+ // SpecializedParser makes it so we have to search multiple extended interfaces for bridge methods
+ // when desugaring the lambda returned by this method.
+ public static SpecializedParser<Integer> toInt() {
+ return (s -> Integer.valueOf(s));
+ }
+
+ public static SeparateInterface<Long> isInt() {
+ return (l -> Integer.MIN_VALUE <= l && l <= Integer.MAX_VALUE);
+ }
+
+ public static <T extends Number> List<T> parseAll(List<String> in,
+ SpecializedFunction<String, T> parser) {
+ return in.stream().map(parser).collect(Collectors.toList());
+ }
+
+ public static <T extends Number> List<T> doFilter(List<T> in, SeparateInterface<T> filter) {
+ return in.stream().filter(filter).collect(Collectors.toList());
+ }
+
+ interface Parser<T> extends Function<String, T> {
+ @Override public T apply(String in);
+ }
+
+ public interface SpecializedParser<T extends Number>
+ extends SpecializedFunction<String, T>, Parser<T> {
+ @Override public T apply(String in);
+ }
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/testdata/ConstructorReference.java b/test/java/com/google/devtools/build/android/desugar/testdata/ConstructorReference.java
new file mode 100644
index 0000000..03af2a3
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/testdata/ConstructorReference.java
@@ -0,0 +1,56 @@
+// 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.testdata;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
+public class ConstructorReference {
+
+ private final List<String> names;
+
+ private ConstructorReference(String name) {
+ names = new ArrayList<>(1);
+ names.add(name);
+ }
+
+ public ConstructorReference(List<String> names) {
+ this.names = names;
+ }
+
+ public List<Integer> toInt() {
+ return names.stream().map(Integer::new).collect(Collectors.toList());
+ }
+
+ public static Function<String, ConstructorReference> singleton() {
+ return ConstructorReference::new;
+ }
+
+ public static Supplier<ConstructorReference> emptyThroughJavacGeneratedBridge() {
+ // Because Empty is private in another (inner) class, Javac seems to generate a lambda body
+ // method in this case that calls the Empty(SentinalType) bridge constructor Javac generates.
+ return Empty::new;
+ }
+
+ private static class Empty extends ConstructorReference {
+
+ private Empty() {
+ super(new ArrayList<String>(0));
+ throw new RuntimeException("got it!");
+ }
+ }
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/testdata/GuavaLambda.java b/test/java/com/google/devtools/build/android/desugar/testdata/GuavaLambda.java
new file mode 100644
index 0000000..4974d25
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/testdata/GuavaLambda.java
@@ -0,0 +1,31 @@
+// 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.testdata;
+
+import static com.google.common.collect.Iterables.filter;
+
+import java.util.List;
+
+public class GuavaLambda {
+
+ private final List<String> names;
+
+ public GuavaLambda(List<String> names) {
+ this.names = names;
+ }
+
+ public Iterable<String> as() {
+ return filter(names, n -> n.startsWith("A"));
+ }
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/testdata/InnerClassLambda.java b/test/java/com/google/devtools/build/android/desugar/testdata/InnerClassLambda.java
new file mode 100644
index 0000000..0fe12b9
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/testdata/InnerClassLambda.java
@@ -0,0 +1,51 @@
+// 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.testdata;
+
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+public class InnerClassLambda {
+
+ protected final List<String> reference;
+
+ public InnerClassLambda(List<String> names) {
+ this.reference = names;
+ }
+
+ /**
+ * Uses a lambda that refers to a method parameter across 2 nested anonymous inner classes as well
+ * as a field in the outer scope, the former being relatively unusual as it causes javac to emit
+ * 2 getfields to pass the captured parameter directly to the generated lambda class, covering
+ * an unusual branch in how we rewrite invokedynamics.
+ */
+ public Function<List<String>, Callable<List<String>>> prefixFilter(String prefix) {
+ return new Function<List<String>, Callable<List<String>>>() {
+ @Override
+ public Callable<List<String>> apply(List<String> input) {
+ return new Callable<List<String>>() {
+ @Override
+ public List<String> call() throws Exception {
+ return input
+ .stream()
+ .filter(n -> n.startsWith(prefix) && reference.contains(n))
+ .collect(Collectors.toList());
+ }
+ };
+ }
+ };
+ }
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/testdata/InterfaceWithLambda.java b/test/java/com/google/devtools/build/android/desugar/testdata/InterfaceWithLambda.java
new file mode 100644
index 0000000..47d8ab6
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/testdata/InterfaceWithLambda.java
@@ -0,0 +1,25 @@
+// 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.testdata;
+
+import com.google.common.collect.ImmutableList;
+
+public interface InterfaceWithLambda {
+ String ZERO = String.valueOf(0);
+ ImmutableList<String> DIGITS =
+ ImmutableList.of(0, 1)
+ .stream()
+ .map(i -> i == 0 ? ZERO : String.valueOf(i))
+ .collect(ImmutableList.toImmutableList());
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/testdata/Lambda.java b/test/java/com/google/devtools/build/android/desugar/testdata/Lambda.java
new file mode 100644
index 0000000..48c81c8
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/testdata/Lambda.java
@@ -0,0 +1,60 @@
+// 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.testdata;
+
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+public class Lambda {
+
+ private final List<String> names;
+
+ public Lambda(List<String> names) {
+ this.names = names;
+ }
+
+ public List<String> as() {
+ return names
+ .stream()
+ .filter(n -> n.startsWith("A"))
+ .collect(Collectors.toList());
+ }
+
+ public static Callable<String> hello() {
+ return (Callable<String> & java.util.RandomAccess) () -> "hello";
+ }
+
+ public static Function<Integer, Callable<Long>> mult(int x) {
+ return new Function<Integer, Callable<Long>>() {
+ @Override
+ public Callable<Long> apply(Integer y) {
+ return () -> (long) x * (long) y;
+ }
+ };
+ }
+
+ /**
+ * Test method for b/62456849. This method will first be converted to a synthetic method by {@link
+ * com.google.devtools.build.android.desugar.Bug62456849TestDataGenerator}, and then Desugar
+ * should keep it in this class without desugaring it (such as renaming).
+ *
+ * <p>Please ignore the lint error on the method name. The method name is intentionally chosen to
+ * trigger a bug in Desugar.
+ */
+ public static int lambda$mult$0() {
+ return 0;
+ }
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/testdata/LambdaInOverride.java b/test/java/com/google/devtools/build/android/desugar/testdata/LambdaInOverride.java
new file mode 100644
index 0000000..1f683f8
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/testdata/LambdaInOverride.java
@@ -0,0 +1,35 @@
+// 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.testdata;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Test class carefully constructed so javac emits a lambda body method called lambda$filter$0,
+ * which is exactly the name used for the lambda body method generated by javac for the superclass.
+ */
+public class LambdaInOverride extends OuterReferenceLambda {
+ public LambdaInOverride(List<String> names) {
+ super(names);
+ }
+
+ public List<String> filter(List<String> names) {
+ return super
+ .filter(names)
+ .stream()
+ .filter(n -> !reference.contains(n))
+ .collect(Collectors.toList());
+ }
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/testdata/MethodReference.java b/test/java/com/google/devtools/build/android/desugar/testdata/MethodReference.java
new file mode 100644
index 0000000..a293cea
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/testdata/MethodReference.java
@@ -0,0 +1,88 @@
+// 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.testdata;
+
+import com.google.devtools.build.android.desugar.testdata.separate.SeparateBaseClass;
+import java.io.IOException;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+public class MethodReference extends SeparateBaseClass<String> {
+
+ private final List<String> names;
+
+ public MethodReference(List<String> names) {
+ super(names);
+ this.names = names;
+ }
+
+ // Class method reference
+ public void appendAll(StringBuilder dest) {
+ names.stream().forEach(dest::append);
+ }
+
+ // Interface method reference (regression test for b/33304582)
+ public List<String> transform(Transformer<String> transformer) {
+ return names.stream().map(transformer::transform).collect(Collectors.toList());
+ }
+
+ // Private method reference (regression test for b/33378312)
+ public List<String> some() {
+ return names.stream().filter(MethodReference::startsWithS).collect(Collectors.toList());
+ }
+
+ // Protected method reference in a base class of another package (regression test for b/33378312)
+ public List<String> intersect(List<String> other) {
+ return other.stream().filter(this::contains).collect(Collectors.toList());
+ }
+
+ // Contains the same method reference as intersect
+ public List<String> onlyIn(List<String> other) {
+ Predicate<String> p = this::contains;
+ return other.stream().filter(p.negate()).collect(Collectors.toList());
+ }
+
+ // Private method reference to an instance method that throws (regression test for b/33378312)
+ public Callable<String> stringer() {
+ return this::throwing;
+ }
+
+ /** Returns a method reference derived from an expression (object.toString()). */
+ public static Function<Integer, Character> stringChars(Object object) {
+ return (object == null ? "" : object.toString())::charAt;
+ }
+
+ /** Returns a method reference derived from a field */
+ public Predicate<String> toPredicate() {
+ return names::contains;
+ }
+
+ private static boolean startsWithS(String input) {
+ return input.startsWith("S");
+ }
+
+ private String throwing() throws Exception {
+ StringBuilder msg = new StringBuilder();
+ appendAll(msg);
+ throw new IOException(msg.toString());
+ }
+
+ /** Interface to create a method reference for in {@link #transform}. */
+ public interface Transformer<T> {
+ T transform(T input);
+ }
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/testdata/MethodReferenceInSubclass.java b/test/java/com/google/devtools/build/android/desugar/testdata/MethodReferenceInSubclass.java
new file mode 100644
index 0000000..fb1c49e
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/testdata/MethodReferenceInSubclass.java
@@ -0,0 +1,37 @@
+// 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.testdata;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class MethodReferenceInSubclass extends MethodReferenceSuperclass {
+
+ public MethodReferenceInSubclass(List<String> names) {
+ super(names);
+ }
+
+ // Private method reference in subclass that causes a bridge method with the same signature as in
+ // a superclass in the same package (regression test for b/36201257). Both superclass and this
+ // class need a method reference to a private *instance* method with the same signature, and they
+ // should each only one method reference and no lambdas so any class-local counter matches, for
+ // this class to serve as a repro for b/36201257.
+ public List<String> containsE() {
+ return names.stream().filter(this::containsE).collect(Collectors.toList());
+ }
+
+ private boolean containsE(String input) {
+ return input.contains("e");
+ }
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/testdata/MethodReferenceSuperclass.java b/test/java/com/google/devtools/build/android/desugar/testdata/MethodReferenceSuperclass.java
new file mode 100644
index 0000000..c24cf55
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/testdata/MethodReferenceSuperclass.java
@@ -0,0 +1,39 @@
+// 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.testdata;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class MethodReferenceSuperclass {
+
+ protected final List<String> names;
+
+ public MethodReferenceSuperclass(List<String> names) {
+ this.names = names;
+ }
+
+ // Method reference that causes a simple bridge method because the referenced method is private.
+ // We want to make sure that bridge methods generated in subclasses don't clobber this one.
+ public List<String> startsWithL() {
+ return names
+ .stream()
+ .filter(this::startsWithL)
+ .collect(Collectors.toList());
+ }
+
+ private boolean startsWithL(String input) {
+ return input.startsWith("L");
+ }
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/testdata/OuterReferenceLambda.java b/test/java/com/google/devtools/build/android/desugar/testdata/OuterReferenceLambda.java
new file mode 100644
index 0000000..d3cf829
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/testdata/OuterReferenceLambda.java
@@ -0,0 +1,33 @@
+// 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.testdata;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class OuterReferenceLambda {
+
+ protected final List<String> reference;
+
+ public OuterReferenceLambda(List<String> names) {
+ this.reference = names;
+ }
+
+ public List<String> filter(List<String> names) {
+ return names
+ .stream()
+ .filter(n -> reference.contains(n))
+ .collect(Collectors.toList());
+ }
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/testdata/SpecializedFunction.java b/test/java/com/google/devtools/build/android/desugar/testdata/SpecializedFunction.java
new file mode 100644
index 0000000..b7616a0
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/testdata/SpecializedFunction.java
@@ -0,0 +1,22 @@
+// 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.testdata;
+
+import java.util.function.Function;
+
+public interface SpecializedFunction<S, T extends Number> extends Function<S, T> {
+ Integer DO_NOT_COPY_INTO_LAMBDA_CLASSES = Integer.valueOf(42);
+ @Override
+ public T apply(S in);
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/testdata/core_library/java/lang/AutoboxedTypes.java b/test/java/com/google/devtools/build/android/desugar/testdata/core_library/java/lang/AutoboxedTypes.java
new file mode 100644
index 0000000..bdf298c
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/testdata/core_library/java/lang/AutoboxedTypes.java
@@ -0,0 +1,36 @@
+// Copyright 2017 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.
+
+// This test class is in the java.lang namespace to trigger the hardcoded JVM restrictions that
+// desugar --core_library works around
+package java.lang;
+
+/**
+ * This class will be desugared with --core_library and then functionally tested by {@code
+ * DesugarCoreLibraryFunctionalTest}
+ */
+public class AutoboxedTypes {
+ /**
+ * Dummy functional interface for autoboxedTypeLambda to return without introducing a dependency
+ * on any other java.* classes.
+ */
+ @FunctionalInterface
+ public interface Lambda {
+ String charAt(String s);
+ }
+
+ public static Lambda autoboxedTypeLambda(Integer i) {
+ return n -> n.substring(i, i + 1);
+ }
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/testdata/core_library/test/util/TestClassForStackMapFrame.java b/test/java/com/google/devtools/build/android/desugar/testdata/core_library/test/util/TestClassForStackMapFrame.java
new file mode 100644
index 0000000..c39d7bc
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/testdata/core_library/test/util/TestClassForStackMapFrame.java
@@ -0,0 +1,56 @@
+// Copyright 2017 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 test.util;
+
+/** Test input for b/36654936 */
+public class TestClassForStackMapFrame {
+
+ /**
+ * This method caused cl/152199391 to fail due to stack map frame corruption. So it is to make
+ * sure the desugared version of this class still has correct stack map frames.
+ */
+ public String joinIntegers(int integers) {
+ StringBuilder builder = new StringBuilder();
+ for (int i = 0; i < integers; i++) {
+ if (i > 0) {
+ builder.append(",");
+ }
+ builder.append(i);
+ builder.append('=');
+ Object value = i % 2 == 0 ? "Even" : "Odd";
+ if (i % 2 == 0) {
+ builder.append(value);
+ } else {
+ builder.append(value);
+ }
+ }
+ return builder.toString();
+ }
+
+ /**
+ * This method triggers ASM bug 317785 .
+ *
+ * @return 20
+ */
+ public static int testInputForAsmBug317785() {
+ Integer x = 0;
+ for (int i = 0; i < 10; ++i) {
+ x++;
+ }
+ for (int i = 0; i < 10; ++i) {
+ x++;
+ }
+ return x;
+ }
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/testdata/java8/AnnotationsOfDefaultMethodsShouldBeKept.java b/test/java/com/google/devtools/build/android/desugar/testdata/java8/AnnotationsOfDefaultMethodsShouldBeKept.java
new file mode 100644
index 0000000..eb79488
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/testdata/java8/AnnotationsOfDefaultMethodsShouldBeKept.java
@@ -0,0 +1,43 @@
+// Copyright 2017 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;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** Test for b/38302860. The annotations of default methods should be kept after desugaring. */
+public class AnnotationsOfDefaultMethodsShouldBeKept {
+
+ /**
+ * An interface, that has annotation, annotated abstract methods, and annotated default methods.
+ * After desugaring, all these annotations should remain in the interface.
+ */
+ @SomeAnnotation(1)
+ public interface AnnotatedInterface {
+
+ @SomeAnnotation(2)
+ void annotatedAbstractMethod();
+
+ @SomeAnnotation(3)
+ default void annotatedDefaultMethod() {}
+ }
+
+ /**
+ * A simple annotation, used for testing.
+ */
+ @Retention(value = RetentionPolicy.RUNTIME)
+ public @interface SomeAnnotation {
+ int value();
+ }
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/testdata/java8/ConcreteDefaultInterfaceWithLambda.java b/test/java/com/google/devtools/build/android/desugar/testdata/java8/ConcreteDefaultInterfaceWithLambda.java
new file mode 100644
index 0000000..0d9f70a
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/testdata/java8/ConcreteDefaultInterfaceWithLambda.java
@@ -0,0 +1,28 @@
+// Copyright 2017 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;
+
+import com.google.common.collect.ImmutableList;
+
+public class ConcreteDefaultInterfaceWithLambda implements DefaultInterfaceWithLambda {
+ static final String ONE = String.valueOf(1);
+
+ @Override
+ public ImmutableList<String> digits() {
+ return ImmutableList.of(0, 2)
+ .stream()
+ .map(i -> i == 0 ? ONE : String.valueOf(i))
+ .collect(ImmutableList.toImmutableList());
+ }
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/testdata/java8/ConcreteOverridesDefaultWithLambda.java b/test/java/com/google/devtools/build/android/desugar/testdata/java8/ConcreteOverridesDefaultWithLambda.java
new file mode 100644
index 0000000..cdcc5e9
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/testdata/java8/ConcreteOverridesDefaultWithLambda.java
@@ -0,0 +1,37 @@
+// Copyright 2017 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;
+
+import com.google.common.collect.ImmutableList;
+
+public class ConcreteOverridesDefaultWithLambda implements DefaultInterfaceWithLambda {
+ static final String TWO = String.valueOf(2);
+ static final String THREE = String.valueOf(3);
+
+ @Override
+ public ImmutableList<String> defaultWithLambda() {
+ return ImmutableList.of(0, 3)
+ .stream()
+ .map(i -> i == 0 ? TWO : String.valueOf(i))
+ .collect(ImmutableList.toImmutableList());
+ }
+
+ @Override
+ public ImmutableList<String> digits() {
+ return ImmutableList.of(0, 4)
+ .stream()
+ .map(i -> i == 0 ? THREE : String.valueOf(i))
+ .collect(ImmutableList.toImmutableList());
+ }
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/testdata/java8/DefaultInterfaceMethodWithStaticInitializer.java b/test/java/com/google/devtools/build/android/desugar/testdata/java8/DefaultInterfaceMethodWithStaticInitializer.java
new file mode 100644
index 0000000..dbbf555
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/testdata/java8/DefaultInterfaceMethodWithStaticInitializer.java
@@ -0,0 +1,172 @@
+// Copyright 2017 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;
+
+import com.google.common.collect.ImmutableList;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Interfaces with default methods are intialized differently from those without default methods.
+ * When we load such an interface, its static intializer will be executed.
+ *
+ * <p>However, interfaces without default methods are only initialized when their non-primitive
+ * fields are accessed.
+ *
+ * <p>Test data for b/38255926
+ */
+public class DefaultInterfaceMethodWithStaticInitializer {
+
+ final List<String> initializationOrder = new ArrayList<>();
+
+ DefaultInterfaceMethodWithStaticInitializer register(Class<?> enclosingInterfaceClass) {
+ initializationOrder.add(enclosingInterfaceClass.getSimpleName());
+ return this;
+ }
+
+ private static long getTime() {
+ return 0;
+ }
+
+ /** The simplest case: direct implementation. */
+ public static class TestInterfaceSetOne {
+
+ /**
+ * A writable field so that other interfaces can set it in their static initializers.
+ * (b/64290760)
+ */
+ static long writableStaticField;
+
+ static final DefaultInterfaceMethodWithStaticInitializer RECORDER =
+ new DefaultInterfaceMethodWithStaticInitializer();
+
+ /** With a default method, this interface should run clinit. */
+ interface I1 {
+ long NOW = TestInterfaceSetOne.writableStaticField = getTime();
+ DefaultInterfaceMethodWithStaticInitializer C = RECORDER.register(I1.class);
+
+ default int defaultM1() {
+ return 1;
+ }
+ }
+
+ /** With a default method, this interface should run clinit. */
+ interface I2 {
+ long NOW = TestInterfaceSetOne.writableStaticField = getTime();
+ DefaultInterfaceMethodWithStaticInitializer D = RECORDER.register(I2.class);
+
+ default int defaultM2() {
+ return 10;
+ }
+ }
+
+ /** Class to trigger the clinit. */
+ public static class C implements I1, I2 {
+ public int sum() {
+ return defaultM1() + defaultM2();
+ }
+ }
+
+ public static ImmutableList<String> getExpectedInitializationOrder() {
+ return ImmutableList.of(I1.class.getSimpleName(), I2.class.getSimpleName());
+ }
+
+ public static ImmutableList<String> getRealInitializationOrder() {
+ return ImmutableList.copyOf(RECORDER.initializationOrder);
+ }
+ }
+
+ /** Test for initializer execution order. */
+ public static class TestInterfaceSetTwo {
+
+ static final DefaultInterfaceMethodWithStaticInitializer RECORDER =
+ new DefaultInterfaceMethodWithStaticInitializer();
+
+ interface I1 {
+ DefaultInterfaceMethodWithStaticInitializer C = RECORDER.register(I1.class);
+
+ default int defaultM1() {
+ return 1;
+ }
+ }
+
+ interface I2 extends I1 {
+ DefaultInterfaceMethodWithStaticInitializer D = RECORDER.register(I2.class);
+
+ default int defaultM2() {
+ return 2;
+ }
+ }
+
+ /**
+ * Loading this class will trigger the execution of the static initializers of I2 and I1.
+ * However, I1 will be loaded first, as I2 extends I1.
+ */
+ public static class C implements I2, I1 {
+ protected static final Integer INT_VALUE = Integer.valueOf(1); // To create a <clinit>
+
+ public int sum() {
+ return defaultM1() + defaultM2();
+ }
+ }
+
+ public static ImmutableList<String> getExpectedInitializationOrder() {
+ return ImmutableList.of(I1.class.getSimpleName(), I2.class.getSimpleName());
+ }
+
+ public static ImmutableList<String> getRealInitializationOrder() {
+ return ImmutableList.copyOf(RECORDER.initializationOrder);
+ }
+ }
+
+ /** Test: I2's <clinit> should not be executed. */
+ public static class TestInterfaceSetThree {
+ static final DefaultInterfaceMethodWithStaticInitializer RECORDER =
+ new DefaultInterfaceMethodWithStaticInitializer();
+
+ interface I1 {
+ DefaultInterfaceMethodWithStaticInitializer C = RECORDER.register(I1.class);
+
+ default int defaultM1() {
+ return 6;
+ }
+ }
+
+ interface I2 extends I1 {
+ default int defaultM2() {
+ return 5;
+ }
+ }
+
+ /**
+ * Loading this class will trigger the execution of the static initializers of I1. I2's will not
+ * execute.
+ */
+ public static class C implements I2, I1 {
+ protected static final Integer INT_VALUE = Integer.valueOf(1); // To create a <clinit>
+
+ public int sum() {
+ return defaultM1() + defaultM2();
+ }
+ }
+
+ public static ImmutableList<String> getExpectedInitializationOrder() {
+ return ImmutableList.of(I1.class.getSimpleName());
+ }
+
+ public static ImmutableList<String> getRealInitializationOrder() {
+ return ImmutableList.copyOf(RECORDER.initializationOrder);
+ }
+ }
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/testdata/java8/DefaultInterfaceWithBridges.java b/test/java/com/google/devtools/build/android/desugar/testdata/java8/DefaultInterfaceWithBridges.java
new file mode 100644
index 0000000..c5fb2d3
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/testdata/java8/DefaultInterfaceWithBridges.java
@@ -0,0 +1,66 @@
+// Copyright 2017 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;
+
+/**
+ * The base interface, which is generic, and has two default methods. These two default methods will
+ * introduce bridge methods in the child-interfaces
+ */
+interface GenericInterfaceWithDefaultMethod<T extends Number> {
+ default T copy(T t) {
+ return t;
+ }
+
+ default Number getNumber() {
+ return 1;
+ }
+}
+
+/** This interface generate two additional bridge methods */
+interface InterfaceWithDefaultAndBridgeMethods extends GenericInterfaceWithDefaultMethod<Integer> {
+ @Override
+ default Integer copy(Integer t) {
+ return GenericInterfaceWithDefaultMethod.super.copy(t);
+ }
+
+ @Override
+ default Double getNumber() {
+ return 2.3d;
+ }
+}
+
+/** A class implementing the interface. */
+class ClassWithDefaultAndBridgeMethods implements InterfaceWithDefaultAndBridgeMethods {}
+
+/** The client class that uses the interfaces and the class that implements the interfaces. */
+public class DefaultInterfaceWithBridges {
+ private final ClassWithDefaultAndBridgeMethods c = new ClassWithDefaultAndBridgeMethods();
+
+ public Integer copy(Integer i) {
+ return c.copy(i);
+ }
+
+ public Number getNumber() {
+ return ((GenericInterfaceWithDefaultMethod) c).getNumber();
+ }
+
+ public Double getDouble() {
+ return c.getNumber();
+ }
+
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ public Number copy(Number n) {
+ return ((GenericInterfaceWithDefaultMethod) c).copy(n);
+ }
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/testdata/java8/DefaultInterfaceWithLambda.java b/test/java/com/google/devtools/build/android/desugar/testdata/java8/DefaultInterfaceWithLambda.java
new file mode 100644
index 0000000..e97cae9
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/testdata/java8/DefaultInterfaceWithLambda.java
@@ -0,0 +1,33 @@
+// Copyright 2017 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;
+
+import com.google.common.collect.ImmutableList;
+
+public interface DefaultInterfaceWithLambda {
+ String ZERO = String.valueOf(0);
+
+ public default ImmutableList<String> defaultWithLambda() {
+ return ImmutableList.of(0, 1)
+ .stream()
+ .map(i -> i == 0 ? ZERO : String.valueOf(i))
+ .collect(ImmutableList.toImmutableList());
+ }
+
+ public default ImmutableList<String> defaultCallsInterfaceMethod() {
+ return digits();
+ }
+
+ public ImmutableList<String> digits();
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/testdata/java8/FunctionWithDefaultMethod.java b/test/java/com/google/devtools/build/android/desugar/testdata/java8/FunctionWithDefaultMethod.java
new file mode 100644
index 0000000..176eace
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/testdata/java8/FunctionWithDefaultMethod.java
@@ -0,0 +1,78 @@
+// Copyright 2017 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;
+
+import java.util.List;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/** Desugaring test input interface that includes a default method and a static method. */
+public interface FunctionWithDefaultMethod<T extends Number> extends Function<T, T> {
+
+ @Override
+ T apply(T input);
+
+ static <T extends Number> Function<T, Long> toLong() {
+ return input -> input.longValue();
+ }
+
+ default T twice(T input) {
+ return apply(apply(input));
+ }
+
+ /** Don't call this method from tests, it won't work since Desugar moves it! */
+ static FunctionWithDefaultMethod<Integer> inc(int add) {
+ return input -> input + add;
+ }
+
+ /**
+ * Implementation of {@link FunctionWithDefaultMethod} that overrides the default method.
+ * Also declares static methods the test uses to exercise the code in this file.
+ */
+ public static class DoubleInts implements FunctionWithDefaultMethod<Integer> {
+ @Override
+ public Integer apply(Integer input) {
+ return 2 * input;
+ }
+
+ @Override
+ public Integer twice(Integer input) {
+ return 5 * input; // deliberately wrong :)
+ }
+
+ public static List<Long> add(List<Integer> ints, int add) {
+ return ints.stream().map(inc(add)).map(toLong()).collect(Collectors.toList());
+ }
+
+ public static FunctionWithDefaultMethod<Integer> doubleLambda() {
+ return input -> 2 * input;
+ }
+
+ public static FunctionWithDefaultMethod<Integer> incTwice(int add) {
+ return inc(add)::twice;
+ }
+
+ public static FunctionWithDefaultMethod<Integer> times5() {
+ return new DoubleInts2()::twice;
+ }
+
+ public static Function<Integer, FunctionWithDefaultMethod<Integer>> incFactory() {
+ return FunctionWithDefaultMethod::inc;
+ }
+ }
+
+ /** Empty subclass that explicitly implements the interface the superclass already implements. */
+ public static class DoubleInts2 extends DoubleInts implements FunctionWithDefaultMethod<Integer> {
+ }
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/testdata/java8/FunctionalInterfaceWithInitializerAndDefaultMethods.java b/test/java/com/google/devtools/build/android/desugar/testdata/java8/FunctionalInterfaceWithInitializerAndDefaultMethods.java
new file mode 100644
index 0000000..cde6c7b
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/testdata/java8/FunctionalInterfaceWithInitializerAndDefaultMethods.java
@@ -0,0 +1,60 @@
+// Copyright 2017 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;
+
+/**
+ * An interface that has a default method, and a non-empty static initializer. The initializer is
+ * NOT expected to run during desugaring.
+ */
+public interface FunctionalInterfaceWithInitializerAndDefaultMethods {
+
+ ClassWithInitializer CONSTANT = new ClassWithInitializer();
+ boolean BOOLEAN = getFalse();
+ char CHAR = "hello".charAt(0);
+ byte BYTE = Byte.parseByte("0");
+ short SHORT = Short.parseShort("0");
+ int INT = Integer.parseInt("0");
+ float FLOAT = Float.parseFloat("0");
+ long LONG = Long.parseLong("0");
+ double DOUBLE = Double.parseDouble("0");
+
+ int convert();
+
+ /**
+ * The default method ensures that the static initializer of this interface will be executed when
+ * the interface is loaded.
+ */
+ default void defaultMethod() {}
+
+ static boolean getFalse() {
+ return false;
+ }
+
+ /**
+ * A class with a static initializer that has side effects (In this class, the printing to stdout)
+ */
+ class ClassWithInitializer {
+ static {
+ System.out.println("THIS STRING IS NOT EXPECTED TO APPEAR IN THE OUTPUT OF DESUGAR!!!");
+ }
+
+ /**
+ * A lambda to trigger Desugar to load the interface {@link
+ * FunctionalInterfaceWithInitializerAndDefaultMethods}
+ */
+ public FunctionalInterfaceWithInitializerAndDefaultMethods length(String s) {
+ return s::length;
+ }
+ }
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/testdata/java8/GenericDefaultInterfaceWithLambda.java b/test/java/com/google/devtools/build/android/desugar/testdata/java8/GenericDefaultInterfaceWithLambda.java
new file mode 100644
index 0000000..5839679
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/testdata/java8/GenericDefaultInterfaceWithLambda.java
@@ -0,0 +1,97 @@
+// Copyright 2017 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;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/** An interface with default methods, lambdas, and generics */
+public interface GenericDefaultInterfaceWithLambda<T> {
+
+ T getBaseValue();
+
+ T increment(T value);
+
+ String toString(T value);
+
+ public default ArrayList<T> toList(int bound) {
+ ArrayList<T> result = new ArrayList<>();
+ if (bound == 0) {
+ return result;
+ }
+ result.add(getBaseValue());
+ for (int i = 1; i < bound; ++i) {
+ result.add(increment(result.get(i - 1)));
+ }
+ return result;
+ }
+
+ public default List<String> convertToStringList(List<T> list) {
+ return list.stream().map(this::toString).collect(Collectors.toList());
+ }
+
+ public default Function<Integer, ArrayList<T>> toListSupplier() {
+ return this::toList;
+ }
+
+ /** The type parameter is concretized to {@link Number} */
+ interface LevelOne<T extends Number> extends GenericDefaultInterfaceWithLambda<T> {}
+
+ /** The type parameter is instantiated to {@link Integer} */
+ interface LevelTwo extends LevelOne<Integer> {
+
+ @Override
+ default Integer getBaseValue() {
+ return 0;
+ }
+ }
+
+ /** An abstract class with no implementing methods. */
+ abstract static class ClassOne implements LevelTwo {}
+
+ /** A class for {@link Integer} */
+ class ClassTwo extends ClassOne {
+
+ @Override
+ public Integer increment(Integer value) {
+ return value + 1;
+ }
+
+ @Override
+ public String toString(Integer value) {
+ return value.toString();
+ }
+ }
+
+ /** A class fo {@link Long} */
+ class ClassThree implements LevelOne<Long> {
+
+ @Override
+ public Long getBaseValue() {
+ return Long.valueOf(0);
+ }
+
+ @Override
+ public Long increment(Long value) {
+ return value + 1;
+ }
+
+ @Override
+ public String toString(Long value) {
+ return value.toString();
+ }
+ }
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/testdata/java8/InterfaceMethod.java b/test/java/com/google/devtools/build/android/desugar/testdata/java8/InterfaceMethod.java
new file mode 100644
index 0000000..622e6e5
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/testdata/java8/InterfaceMethod.java
@@ -0,0 +1,49 @@
+// Copyright 2017 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;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Desugar test input interface that declares lambdas and method references in default and static
+ * interface methods.
+ */
+public interface InterfaceMethod {
+ public default List<String> defaultMethodReference(List<String> names) {
+ return names.stream().filter(this::startsWithS).collect(Collectors.toList());
+ }
+
+ public default List<String> staticMethodReference(List<String> names) {
+ return names.stream().filter(InterfaceMethod::startsWithA).collect(Collectors.toList());
+ }
+
+ public default List<String> lambdaCallsDefaultMethod(List<String> names) {
+ return names.stream().filter(s -> startsWithS(s)).collect(Collectors.toList());
+ }
+
+ public static boolean startsWithA(String input) {
+ return input.startsWith("A");
+ }
+
+ public default boolean startsWithS(String input) {
+ return input.startsWith("S");
+ }
+
+ /**
+ * Empty class implementing {@link InterfaceMethod} so the test can instantiate and call default
+ * methods.
+ */
+ public static class Concrete implements InterfaceMethod {}
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/testdata/java8/InterfaceWithDefaultMethod.java b/test/java/com/google/devtools/build/android/desugar/testdata/java8/InterfaceWithDefaultMethod.java
new file mode 100644
index 0000000..b2d1beb
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/testdata/java8/InterfaceWithDefaultMethod.java
@@ -0,0 +1,41 @@
+// Copyright 2017 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;
+
+/** Interface for testing default methods overridden by extending interfaces. */
+public interface InterfaceWithDefaultMethod {
+ default int version() {
+ return 1;
+ }
+
+ /** Interface that overrides {@link #version}. */
+ public interface Redefine extends InterfaceWithDefaultMethod {
+ @Override
+ default int version() {
+ return 2;
+ }
+ }
+
+ /** Class that implements both interfaces, supertype before subtype. */
+ public static class Version2 implements InterfaceWithDefaultMethod, Redefine {}
+
+ /** Base class that just implements {@link Redefine}. */
+ static class Version2Base implements Redefine {}
+
+ /**
+ * Subclass that implements an interface explicitly that the superclass also implements,
+ * but the superclass implements a more specific interface that overrides a defautl method.
+ */
+ public static class AlsoVersion2 extends Version2Base implements InterfaceWithDefaultMethod {}
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/testdata/java8/InterfaceWithDuplicateMethods.java b/test/java/com/google/devtools/build/android/desugar/testdata/java8/InterfaceWithDuplicateMethods.java
new file mode 100644
index 0000000..56217b8
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/testdata/java8/InterfaceWithDuplicateMethods.java
@@ -0,0 +1,45 @@
+// Copyright 2017 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;
+
+/**
+ * Test for b/38308515. This interface has one instance method {@code m()} and one static method
+ * {@code m(InterfaceWithDuplicateMethods)}, which may cause Desugar to dump a companion class with
+ * duplicate method signatures.
+ */
+public interface InterfaceWithDuplicateMethods {
+
+ /**
+ * In the companion class, this default method will be transformed to {@code int
+ * getZero(InterfaceWithDuplicateMethods)}, which has the same signature as the static interface
+ * method below.
+ */
+ @SuppressWarnings("AmbiguousMethodReference")
+ default int getZero() {
+ return 0;
+ }
+
+ /** Should not be called. Should only be called by the class {@link ClassWithDuplicateMethods} */
+ @SuppressWarnings("AmbiguousMethodReference")
+ static int getZero(InterfaceWithDuplicateMethods i) {
+ return 1;
+ }
+
+ /** This class implements the interface, and calls the static interface method. */
+ class ClassWithDuplicateMethods implements InterfaceWithDuplicateMethods {
+ public int getZeroFromStaticInterfaceMethod() {
+ return InterfaceWithDuplicateMethods.getZero(this);
+ }
+ }
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/testdata/java8/Java7InterfaceWithBridges.java b/test/java/com/google/devtools/build/android/desugar/testdata/java8/Java7InterfaceWithBridges.java
new file mode 100644
index 0000000..ae9ef4f
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/testdata/java8/Java7InterfaceWithBridges.java
@@ -0,0 +1,56 @@
+// Copyright 2017 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;
+
+/**
+ * The test classes for desugaring default methods. Desugar is not expected to generate companion
+ * classes for interfaces without default methods. The bridge methods are automatically generated by
+ * javac and put in the implementing classes.
+ *
+ * <p>NOTE: There should be NO companion class generated for this class.
+ */
+public interface Java7InterfaceWithBridges<T> {
+ T add(T value);
+
+ /** Concretize T to {@link Number)} */
+ interface LevelOne<T extends Number> extends Java7InterfaceWithBridges<T> {
+ @Override
+ T add(T value);
+ }
+
+ /** Concretize to {@link Integer} */
+ interface LevelTwo extends LevelOne<Integer> {
+ @Override
+ Integer add(Integer value);
+ }
+
+ /** Empty abstract class. This class should have no bridge methods */
+ abstract static class AbstractClassOne implements LevelTwo {}
+
+ /** Implementing class. */
+ static class ClassAddOne extends AbstractClassOne {
+ @Override
+ public Integer add(Integer value) {
+ return value + 1;
+ }
+ }
+
+ /** Implementing class. */
+ static class ClassAddTwo extends AbstractClassOne implements LevelTwo {
+ @Override
+ public Integer add(Integer value) {
+ return value + 2;
+ }
+ }
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/testdata/java8/Named.java b/test/java/com/google/devtools/build/android/desugar/testdata/java8/Named.java
new file mode 100644
index 0000000..4c44ffd
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/testdata/java8/Named.java
@@ -0,0 +1,72 @@
+// Copyright 2017 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;
+
+/** Desugar test interface to test precedence of inherited methods over default methods. */
+public interface Named {
+ default String name() {
+ return getClass().getSimpleName();
+ }
+
+ /** Base class defining {@link #name} without implementing {@link Named}. */
+ static class ExplicitNameBase {
+ private final String name;
+
+ public ExplicitNameBase(String name) {
+ this.name = name;
+ }
+
+ public String name() {
+ return name;
+ }
+ }
+
+ /** Class whose base class implementes {@link #name}. */
+ public static class ExplicitName extends ExplicitNameBase implements Named {
+ public ExplicitName(String name) {
+ super(name);
+ }
+ }
+
+ /** Class that explicitly defers to the default method in {@link Named}. */
+ public static class DefaultName extends ExplicitNameBase implements Named {
+ public DefaultName() {
+ super(null);
+ }
+
+ @Override
+ public String name() {
+ return Named.super.name() + "-once";
+ }
+ }
+
+ /** Subclass of {@link DefaultName} that uses {@code super} as well. */
+ public static class DefaultNameSubclass extends DefaultName {
+ @Override
+ public String name() {
+ return super.name() + "-twice";
+ }
+ }
+
+ /** Base class that declares {@link #name} abstract. */
+ abstract static class AbstractNameBase {
+ public abstract String name();
+ }
+
+ /**
+ * Class that inherits {@link #name} abstract so subclasses must implement it despite default
+ * method in implemented interface.
+ */
+ public abstract static class AbstractName extends AbstractNameBase implements Named {}
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/testdata/java8/TwoInheritedDefaultMethods.java b/test/java/com/google/devtools/build/android/desugar/testdata/java8/TwoInheritedDefaultMethods.java
new file mode 100644
index 0000000..e9456ea
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/testdata/java8/TwoInheritedDefaultMethods.java
@@ -0,0 +1,36 @@
+// Copyright 2017 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;
+
+/** Desugar test class that explicitly calls default methods from two implemented interfaces. */
+public class TwoInheritedDefaultMethods implements Name1, Name2 {
+ @Override
+ public String name() {
+ return Name1.super.name() + ":" + Name2.super.name();
+ }
+}
+
+/** Test interface for {@link TwoInheritedDefaultMethods}. */
+interface Name1 {
+ default String name() {
+ return "One";
+ }
+}
+
+/** Test interface for {@link TwoInheritedDefaultMethods}. */
+interface Name2 {
+ default String name() {
+ return "Two";
+ }
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/testdata/java8/VisibilityTestClass.java b/test/java/com/google/devtools/build/android/desugar/testdata/java8/VisibilityTestClass.java
new file mode 100644
index 0000000..b6bd379
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/testdata/java8/VisibilityTestClass.java
@@ -0,0 +1,23 @@
+// Copyright 2017 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;
+
+import com.google.devtools.build.android.desugar.testdata.java8.subpackage.PublicInterface;
+
+/**
+ * Class that transitively implements a package-private interface in another package. Default
+ * method desugaring will need to make the default method defined in that interface publicly
+ * accessible.
+ */
+public class VisibilityTestClass implements PublicInterface {}
diff --git a/test/java/com/google/devtools/build/android/desugar/testdata/java8/subpackage/PackagePrivateInterface.java b/test/java/com/google/devtools/build/android/desugar/testdata/java8/subpackage/PackagePrivateInterface.java
new file mode 100644
index 0000000..9153c12
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/testdata/java8/subpackage/PackagePrivateInterface.java
@@ -0,0 +1,33 @@
+// Copyright 2017 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.subpackage;
+
+/** Package-private interface with default method. */
+interface PackagePrivateInterface {
+
+ /**
+ * This field makes this interface need to be initialized. With the default methods, when this
+ * interface is loaded, its initializer should also be run.
+ *
+ * <p>However, this test interface is different, as it is package-private. We need to to make sure
+ * the desugared code does not trigger IllegalAccessError.
+ *
+ * <p>See b/38255926.
+ */
+ Integer VERSION = Integer.valueOf(0);
+
+ default int m() {
+ return 42;
+ }
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/testdata/java8/subpackage/PublicInterface.java b/test/java/com/google/devtools/build/android/desugar/testdata/java8/subpackage/PublicInterface.java
new file mode 100644
index 0000000..c3d51db
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/testdata/java8/subpackage/PublicInterface.java
@@ -0,0 +1,22 @@
+// Copyright 2017 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.subpackage;
+
+/**
+ * Public interface extending a package-private interface so classes in other packages can
+ * transitively implement a package-private interface.
+ *
+ * @see com.google.devtools.build.android.desugar.testdata.java8.VisibilityTestClass
+ */
+public interface PublicInterface extends PackagePrivateInterface {}
diff --git a/test/java/com/google/devtools/build/android/desugar/testdata/separate/SeparateBaseClass.java b/test/java/com/google/devtools/build/android/desugar/testdata/separate/SeparateBaseClass.java
new file mode 100644
index 0000000..a182c53
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/testdata/separate/SeparateBaseClass.java
@@ -0,0 +1,32 @@
+// 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.testdata.separate;
+
+import java.util.List;
+
+/**
+ * Test base class for testing method references to protected methods in another compilation.
+ */
+public class SeparateBaseClass<T> {
+
+ private final List<T> list;
+
+ protected SeparateBaseClass(List<T> list) {
+ this.list = list;
+ }
+
+ protected boolean contains(T elem) {
+ return list.contains(elem);
+ }
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/testdata/separate/SeparateInterface.java b/test/java/com/google/devtools/build/android/desugar/testdata/separate/SeparateInterface.java
new file mode 100644
index 0000000..ce4b058
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/testdata/separate/SeparateInterface.java
@@ -0,0 +1,21 @@
+// 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.testdata.separate;
+
+import java.util.function.Predicate;
+
+public interface SeparateInterface<T extends Number> extends Predicate<T> {
+ @Override
+ boolean test(T input);
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/testdata/testresource.txt b/test/java/com/google/devtools/build/android/desugar/testdata/testresource.txt
new file mode 100644
index 0000000..30d74d2
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/testdata/testresource.txt
@@ -0,0 +1 @@
+test \ No newline at end of file
diff --git a/test/java/com/google/devtools/build/android/desugar/testdata_desugared_core_library_jar_toc_golden.txt b/test/java/com/google/devtools/build/android/desugar/testdata_desugared_core_library_jar_toc_golden.txt
new file mode 100644
index 0000000..cb0d40c
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/testdata_desugared_core_library_jar_toc_golden.txt
@@ -0,0 +1,18 @@
+META-INF/
+META-INF/MANIFEST.MF
+com/
+com/google/
+com/google/devtools/
+com/google/devtools/build/
+com/google/devtools/build/android/
+com/google/devtools/build/android/desugar/
+com/google/devtools/build/android/desugar/testdata/
+com/google/devtools/build/android/desugar/testdata/testresource.txt
+java/
+java/lang/
+java/lang/AutoboxedTypes$Lambda.class
+java/lang/AutoboxedTypes.class
+test/
+test/util/
+test/util/TestClassForStackMapFrame.class
+java/lang/AutoboxedTypes$$Lambda$0.class \ No newline at end of file
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
new file mode 100644
index 0000000..b7c3c25
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/testdata_desugared_for_disabling_twr_with_large_minsdkversion_jar_toc_golden.txt
@@ -0,0 +1,65 @@
+META-INF/
+META-INF/MANIFEST.MF
+com/
+com/google/
+com/google/devtools/
+com/google/devtools/build/
+com/google/devtools/build/android/
+com/google/devtools/build/android/desugar/
+com/google/devtools/build/android/desugar/testdata/
+com/google/devtools/build/android/desugar/testdata/testresource.txt
+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$SimpleResource.class
+com/google/devtools/build/android/desugar/testdata/ClassUsingTryWithResources.class
+com/google/devtools/build/android/desugar/testdata/ConcreteFunction$Parser.class
+com/google/devtools/build/android/desugar/testdata/ConcreteFunction$SpecializedParser.class
+com/google/devtools/build/android/desugar/testdata/ConcreteFunction.class
+com/google/devtools/build/android/desugar/testdata/ConstructorReference$1.class
+com/google/devtools/build/android/desugar/testdata/ConstructorReference$Empty.class
+com/google/devtools/build/android/desugar/testdata/ConstructorReference.class
+com/google/devtools/build/android/desugar/testdata/GuavaLambda.class
+com/google/devtools/build/android/desugar/testdata/InnerClassLambda$1$1.class
+com/google/devtools/build/android/desugar/testdata/InnerClassLambda$1.class
+com/google/devtools/build/android/desugar/testdata/InnerClassLambda.class
+com/google/devtools/build/android/desugar/testdata/InterfaceWithLambda.class
+com/google/devtools/build/android/desugar/testdata/Lambda$1.class
+com/google/devtools/build/android/desugar/testdata/Lambda.class
+com/google/devtools/build/android/desugar/testdata/LambdaInOverride.class
+com/google/devtools/build/android/desugar/testdata/MethodReference$Transformer.class
+com/google/devtools/build/android/desugar/testdata/MethodReference.class
+com/google/devtools/build/android/desugar/testdata/MethodReferenceInSubclass.class
+com/google/devtools/build/android/desugar/testdata/MethodReferenceSuperclass.class
+com/google/devtools/build/android/desugar/testdata/OuterReferenceLambda.class
+com/google/devtools/build/android/desugar/testdata/SpecializedFunction.class
+com/google/devtools/build/android/desugar/testdata/CaptureLambda$$Lambda$0.class
+com/google/devtools/build/android/desugar/testdata/ClassCallingLongCompare$$Lambda$0.class
+com/google/devtools/build/android/desugar/testdata/ClassCallingLongCompare$$Lambda$1.class
+com/google/devtools/build/android/desugar/testdata/ClassCallingRequireNonNull$$Lambda$0.class
+com/google/devtools/build/android/desugar/testdata/ClassCallingRequireNonNull$$Lambda$1.class
+com/google/devtools/build/android/desugar/testdata/ClassCallingRequireNonNull$$Lambda$2.class
+com/google/devtools/build/android/desugar/testdata/ConcreteFunction$$Lambda$0.class
+com/google/devtools/build/android/desugar/testdata/ConcreteFunction$$Lambda$1.class
+com/google/devtools/build/android/desugar/testdata/ConstructorReference$$Lambda$0.class
+com/google/devtools/build/android/desugar/testdata/ConstructorReference$$Lambda$1.class
+com/google/devtools/build/android/desugar/testdata/ConstructorReference$$Lambda$2.class
+com/google/devtools/build/android/desugar/testdata/GuavaLambda$$Lambda$0.class
+com/google/devtools/build/android/desugar/testdata/InnerClassLambda$1$1$$Lambda$0.class
+com/google/devtools/build/android/desugar/testdata/InterfaceWithLambda$$Lambda$0.class
+com/google/devtools/build/android/desugar/testdata/Lambda$1$$Lambda$0.class
+com/google/devtools/build/android/desugar/testdata/Lambda$$Lambda$0.class
+com/google/devtools/build/android/desugar/testdata/Lambda$$Lambda$1.class
+com/google/devtools/build/android/desugar/testdata/LambdaInOverride$$Lambda$0.class
+com/google/devtools/build/android/desugar/testdata/MethodReference$$Lambda$0.class
+com/google/devtools/build/android/desugar/testdata/MethodReference$$Lambda$1.class
+com/google/devtools/build/android/desugar/testdata/MethodReference$$Lambda$2.class
+com/google/devtools/build/android/desugar/testdata/MethodReference$$Lambda$3.class
+com/google/devtools/build/android/desugar/testdata/MethodReference$$Lambda$4.class
+com/google/devtools/build/android/desugar/testdata/MethodReference$$Lambda$5.class
+com/google/devtools/build/android/desugar/testdata/MethodReference$$Lambda$6.class
+com/google/devtools/build/android/desugar/testdata/MethodReference$$Lambda$7.class
+com/google/devtools/build/android/desugar/testdata/MethodReferenceInSubclass$$Lambda$0.class
+com/google/devtools/build/android/desugar/testdata/MethodReferenceSuperclass$$Lambda$0.class
+com/google/devtools/build/android/desugar/testdata/OuterReferenceLambda$$Lambda$0.class \ No newline at end of file
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
new file mode 100644
index 0000000..d03b121
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/testdata_desugared_for_try_with_resources_jar_toc_golden.txt
@@ -0,0 +1,72 @@
+META-INF/
+META-INF/MANIFEST.MF
+com/
+com/google/
+com/google/devtools/
+com/google/devtools/build/
+com/google/devtools/build/android/
+com/google/devtools/build/android/desugar/
+com/google/devtools/build/android/desugar/testdata/
+com/google/devtools/build/android/desugar/testdata/testresource.txt
+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$SimpleResource.class
+com/google/devtools/build/android/desugar/testdata/ClassUsingTryWithResources.class
+com/google/devtools/build/android/desugar/testdata/ConcreteFunction$Parser.class
+com/google/devtools/build/android/desugar/testdata/ConcreteFunction$SpecializedParser.class
+com/google/devtools/build/android/desugar/testdata/ConcreteFunction.class
+com/google/devtools/build/android/desugar/testdata/ConstructorReference$1.class
+com/google/devtools/build/android/desugar/testdata/ConstructorReference$Empty.class
+com/google/devtools/build/android/desugar/testdata/ConstructorReference.class
+com/google/devtools/build/android/desugar/testdata/GuavaLambda.class
+com/google/devtools/build/android/desugar/testdata/InnerClassLambda$1$1.class
+com/google/devtools/build/android/desugar/testdata/InnerClassLambda$1.class
+com/google/devtools/build/android/desugar/testdata/InnerClassLambda.class
+com/google/devtools/build/android/desugar/testdata/InterfaceWithLambda.class
+com/google/devtools/build/android/desugar/testdata/Lambda$1.class
+com/google/devtools/build/android/desugar/testdata/Lambda.class
+com/google/devtools/build/android/desugar/testdata/LambdaInOverride.class
+com/google/devtools/build/android/desugar/testdata/MethodReference$Transformer.class
+com/google/devtools/build/android/desugar/testdata/MethodReference.class
+com/google/devtools/build/android/desugar/testdata/MethodReferenceInSubclass.class
+com/google/devtools/build/android/desugar/testdata/MethodReferenceSuperclass.class
+com/google/devtools/build/android/desugar/testdata/OuterReferenceLambda.class
+com/google/devtools/build/android/desugar/testdata/SpecializedFunction.class
+com/google/devtools/build/android/desugar/testdata/CaptureLambda$$Lambda$0.class
+com/google/devtools/build/android/desugar/testdata/ClassCallingLongCompare$$Lambda$0.class
+com/google/devtools/build/android/desugar/testdata/ClassCallingLongCompare$$Lambda$1.class
+com/google/devtools/build/android/desugar/testdata/ClassCallingRequireNonNull$$Lambda$0.class
+com/google/devtools/build/android/desugar/testdata/ClassCallingRequireNonNull$$Lambda$1.class
+com/google/devtools/build/android/desugar/testdata/ClassCallingRequireNonNull$$Lambda$2.class
+com/google/devtools/build/android/desugar/testdata/ConcreteFunction$$Lambda$0.class
+com/google/devtools/build/android/desugar/testdata/ConcreteFunction$$Lambda$1.class
+com/google/devtools/build/android/desugar/testdata/ConstructorReference$$Lambda$0.class
+com/google/devtools/build/android/desugar/testdata/ConstructorReference$$Lambda$1.class
+com/google/devtools/build/android/desugar/testdata/ConstructorReference$$Lambda$2.class
+com/google/devtools/build/android/desugar/testdata/GuavaLambda$$Lambda$0.class
+com/google/devtools/build/android/desugar/testdata/InnerClassLambda$1$1$$Lambda$0.class
+com/google/devtools/build/android/desugar/testdata/InterfaceWithLambda$$Lambda$0.class
+com/google/devtools/build/android/desugar/testdata/Lambda$1$$Lambda$0.class
+com/google/devtools/build/android/desugar/testdata/Lambda$$Lambda$0.class
+com/google/devtools/build/android/desugar/testdata/Lambda$$Lambda$1.class
+com/google/devtools/build/android/desugar/testdata/LambdaInOverride$$Lambda$0.class
+com/google/devtools/build/android/desugar/testdata/MethodReference$$Lambda$0.class
+com/google/devtools/build/android/desugar/testdata/MethodReference$$Lambda$1.class
+com/google/devtools/build/android/desugar/testdata/MethodReference$$Lambda$2.class
+com/google/devtools/build/android/desugar/testdata/MethodReference$$Lambda$3.class
+com/google/devtools/build/android/desugar/testdata/MethodReference$$Lambda$4.class
+com/google/devtools/build/android/desugar/testdata/MethodReference$$Lambda$5.class
+com/google/devtools/build/android/desugar/testdata/MethodReference$$Lambda$6.class
+com/google/devtools/build/android/desugar/testdata/MethodReference$$Lambda$7.class
+com/google/devtools/build/android/desugar/testdata/MethodReferenceInSubclass$$Lambda$0.class
+com/google/devtools/build/android/desugar/testdata/MethodReferenceSuperclass$$Lambda$0.class
+com/google/devtools/build/android/desugar/testdata/OuterReferenceLambda$$Lambda$0.class
+com/google/devtools/build/android/desugar/runtime/ThrowableExtension.class
+com/google/devtools/build/android/desugar/runtime/ThrowableExtension$AbstractDesugaringStrategy.class
+com/google/devtools/build/android/desugar/runtime/ThrowableExtension$ConcurrentWeakIdentityHashMap.class
+com/google/devtools/build/android/desugar/runtime/ThrowableExtension$ConcurrentWeakIdentityHashMap$WeakKey.class
+com/google/devtools/build/android/desugar/runtime/ThrowableExtension$MimicDesugaringStrategy.class
+com/google/devtools/build/android/desugar/runtime/ThrowableExtension$NullDesugaringStrategy.class
+com/google/devtools/build/android/desugar/runtime/ThrowableExtension$ReuseDesugaringStrategy.class \ No newline at end of file
diff --git a/test/java/com/google/devtools/build/android/desugar/testdata_desugared_jar_test.sh b/test/java/com/google/devtools/build/android/desugar/testdata_desugared_jar_test.sh
new file mode 100755
index 0000000..7aa7d19
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/testdata_desugared_jar_test.sh
@@ -0,0 +1,44 @@
+#!/bin/bash -e
+#
+# 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.
+
+# Test that lists the content of the desugared Jar and compares it to a golden
+# file. This makes sure that output is deterministic and the resulting Jar
+# doesn't contain any unwanted files, such as lambdas generated as part of
+# running the desugaring tool.
+
+progdir="$(dirname "$0")"
+
+if [ -d "$TEST_TMPDIR" ]; then
+ # Running as part of blaze test
+ tmpdir="$TEST_TMPDIR"
+else
+ # Manual run from command line
+ tmpdir="/tmp/test-$$"
+ mkdir "${tmpdir}"
+fi
+
+if [ -d "$TEST_UNDECLARED_OUTPUTS_DIR" ]; then
+ # Running as part of blaze test: capture test output
+ output="$TEST_UNDECLARED_OUTPUTS_DIR"
+else
+ # Manual run from command line: just write into temp dir
+ output="${tmpdir}"
+fi
+
+JAVABASE=$3
+$JAVABASE/bin/jar tf "$1" >"${output}/actual_toc.txt"
+# sorting can be removed when cl/145334839 is released
+diff <(sort "$2") <(sort "${output}/actual_toc.txt")
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
new file mode 100644
index 0000000..91fc415
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/testdata_desugared_jar_toc_golden.txt
@@ -0,0 +1,72 @@
+META-INF/
+META-INF/MANIFEST.MF
+com/
+com/google/
+com/google/devtools/
+com/google/devtools/build/
+com/google/devtools/build/android/
+com/google/devtools/build/android/desugar/
+com/google/devtools/build/android/desugar/testdata/
+com/google/devtools/build/android/desugar/testdata/testresource.txt
+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$SimpleResource.class
+com/google/devtools/build/android/desugar/testdata/ClassUsingTryWithResources.class
+com/google/devtools/build/android/desugar/testdata/ConcreteFunction$Parser.class
+com/google/devtools/build/android/desugar/testdata/ConcreteFunction$SpecializedParser.class
+com/google/devtools/build/android/desugar/testdata/ConcreteFunction.class
+com/google/devtools/build/android/desugar/testdata/ConstructorReference$1.class
+com/google/devtools/build/android/desugar/testdata/ConstructorReference$Empty.class
+com/google/devtools/build/android/desugar/testdata/ConstructorReference.class
+com/google/devtools/build/android/desugar/testdata/GuavaLambda.class
+com/google/devtools/build/android/desugar/testdata/InnerClassLambda$1$1.class
+com/google/devtools/build/android/desugar/testdata/InnerClassLambda$1.class
+com/google/devtools/build/android/desugar/testdata/InnerClassLambda.class
+com/google/devtools/build/android/desugar/testdata/InterfaceWithLambda.class
+com/google/devtools/build/android/desugar/testdata/Lambda$1.class
+com/google/devtools/build/android/desugar/testdata/Lambda.class
+com/google/devtools/build/android/desugar/testdata/LambdaInOverride.class
+com/google/devtools/build/android/desugar/testdata/MethodReference$Transformer.class
+com/google/devtools/build/android/desugar/testdata/MethodReference.class
+com/google/devtools/build/android/desugar/testdata/MethodReferenceInSubclass.class
+com/google/devtools/build/android/desugar/testdata/MethodReferenceSuperclass.class
+com/google/devtools/build/android/desugar/testdata/OuterReferenceLambda.class
+com/google/devtools/build/android/desugar/testdata/SpecializedFunction.class
+com/google/devtools/build/android/desugar/testdata/CaptureLambda$$Lambda$0.class
+com/google/devtools/build/android/desugar/testdata/ClassCallingLongCompare$$Lambda$0.class
+com/google/devtools/build/android/desugar/testdata/ClassCallingLongCompare$$Lambda$1.class
+com/google/devtools/build/android/desugar/testdata/ClassCallingRequireNonNull$$Lambda$0.class
+com/google/devtools/build/android/desugar/testdata/ClassCallingRequireNonNull$$Lambda$1.class
+com/google/devtools/build/android/desugar/testdata/ClassCallingRequireNonNull$$Lambda$2.class
+com/google/devtools/build/android/desugar/testdata/ConcreteFunction$$Lambda$0.class
+com/google/devtools/build/android/desugar/testdata/ConcreteFunction$$Lambda$1.class
+com/google/devtools/build/android/desugar/testdata/ConstructorReference$$Lambda$0.class
+com/google/devtools/build/android/desugar/testdata/ConstructorReference$$Lambda$1.class
+com/google/devtools/build/android/desugar/testdata/ConstructorReference$$Lambda$2.class
+com/google/devtools/build/android/desugar/testdata/GuavaLambda$$Lambda$0.class
+com/google/devtools/build/android/desugar/testdata/InnerClassLambda$1$1$$Lambda$0.class
+com/google/devtools/build/android/desugar/testdata/InterfaceWithLambda$$Lambda$0.class
+com/google/devtools/build/android/desugar/testdata/Lambda$1$$Lambda$0.class
+com/google/devtools/build/android/desugar/testdata/Lambda$$Lambda$0.class
+com/google/devtools/build/android/desugar/testdata/Lambda$$Lambda$1.class
+com/google/devtools/build/android/desugar/testdata/LambdaInOverride$$Lambda$0.class
+com/google/devtools/build/android/desugar/testdata/MethodReference$$Lambda$0.class
+com/google/devtools/build/android/desugar/testdata/MethodReference$$Lambda$1.class
+com/google/devtools/build/android/desugar/testdata/MethodReference$$Lambda$2.class
+com/google/devtools/build/android/desugar/testdata/MethodReference$$Lambda$3.class
+com/google/devtools/build/android/desugar/testdata/MethodReference$$Lambda$4.class
+com/google/devtools/build/android/desugar/testdata/MethodReference$$Lambda$5.class
+com/google/devtools/build/android/desugar/testdata/MethodReference$$Lambda$6.class
+com/google/devtools/build/android/desugar/testdata/MethodReference$$Lambda$7.class
+com/google/devtools/build/android/desugar/testdata/MethodReferenceInSubclass$$Lambda$0.class
+com/google/devtools/build/android/desugar/testdata/MethodReferenceSuperclass$$Lambda$0.class
+com/google/devtools/build/android/desugar/testdata/OuterReferenceLambda$$Lambda$0.class
+com/google/devtools/build/android/desugar/runtime/ThrowableExtension.class
+com/google/devtools/build/android/desugar/runtime/ThrowableExtension$AbstractDesugaringStrategy.class
+com/google/devtools/build/android/desugar/runtime/ThrowableExtension$ConcurrentWeakIdentityHashMap.class
+com/google/devtools/build/android/desugar/runtime/ThrowableExtension$ConcurrentWeakIdentityHashMap$WeakKey.class
+com/google/devtools/build/android/desugar/runtime/ThrowableExtension$MimicDesugaringStrategy.class
+com/google/devtools/build/android/desugar/runtime/ThrowableExtension$NullDesugaringStrategy.class
+com/google/devtools/build/android/desugar/runtime/ThrowableExtension$ReuseDesugaringStrategy.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
new file mode 100644
index 0000000..8664932
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/testdata_desugared_java8_jar_toc_golden.txt
@@ -0,0 +1,145 @@
+META-INF/
+META-INF/MANIFEST.MF
+com/
+com/google/
+com/google/devtools/
+com/google/devtools/build/
+com/google/devtools/build/android/
+com/google/devtools/build/android/desugar/
+com/google/devtools/build/android/desugar/testdata/
+com/google/devtools/build/android/desugar/testdata/testresource.txt
+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$SimpleResource.class
+com/google/devtools/build/android/desugar/testdata/ClassUsingTryWithResources.class
+com/google/devtools/build/android/desugar/testdata/ConcreteFunction$Parser.class
+com/google/devtools/build/android/desugar/testdata/ConcreteFunction$SpecializedParser.class
+com/google/devtools/build/android/desugar/testdata/ConcreteFunction.class
+com/google/devtools/build/android/desugar/testdata/ConstructorReference$1.class
+com/google/devtools/build/android/desugar/testdata/ConstructorReference$Empty.class
+com/google/devtools/build/android/desugar/testdata/ConstructorReference.class
+com/google/devtools/build/android/desugar/testdata/GuavaLambda.class
+com/google/devtools/build/android/desugar/testdata/InnerClassLambda$1$1.class
+com/google/devtools/build/android/desugar/testdata/InnerClassLambda$1.class
+com/google/devtools/build/android/desugar/testdata/InnerClassLambda.class
+com/google/devtools/build/android/desugar/testdata/InterfaceWithLambda.class
+com/google/devtools/build/android/desugar/testdata/Lambda$1.class
+com/google/devtools/build/android/desugar/testdata/Lambda.class
+com/google/devtools/build/android/desugar/testdata/LambdaInOverride.class
+com/google/devtools/build/android/desugar/testdata/MethodReference$Transformer.class
+com/google/devtools/build/android/desugar/testdata/MethodReference.class
+com/google/devtools/build/android/desugar/testdata/MethodReferenceInSubclass.class
+com/google/devtools/build/android/desugar/testdata/MethodReferenceSuperclass.class
+com/google/devtools/build/android/desugar/testdata/OuterReferenceLambda.class
+com/google/devtools/build/android/desugar/testdata/SpecializedFunction.class
+com/google/devtools/build/android/desugar/testdata/java8/
+com/google/devtools/build/android/desugar/testdata/java8/AnnotationsOfDefaultMethodsShouldBeKept$AnnotatedInterface.class
+com/google/devtools/build/android/desugar/testdata/java8/AnnotationsOfDefaultMethodsShouldBeKept$SomeAnnotation.class
+com/google/devtools/build/android/desugar/testdata/java8/AnnotationsOfDefaultMethodsShouldBeKept.class
+com/google/devtools/build/android/desugar/testdata/java8/ClassWithDefaultAndBridgeMethods.class
+com/google/devtools/build/android/desugar/testdata/java8/ConcreteDefaultInterfaceWithLambda.class
+com/google/devtools/build/android/desugar/testdata/java8/ConcreteOverridesDefaultWithLambda.class
+com/google/devtools/build/android/desugar/testdata/java8/DefaultInterfaceMethodWithStaticInitializer$TestInterfaceSetOne$C.class
+com/google/devtools/build/android/desugar/testdata/java8/DefaultInterfaceMethodWithStaticInitializer$TestInterfaceSetOne$I1.class
+com/google/devtools/build/android/desugar/testdata/java8/DefaultInterfaceMethodWithStaticInitializer$TestInterfaceSetOne$I2.class
+com/google/devtools/build/android/desugar/testdata/java8/DefaultInterfaceMethodWithStaticInitializer$TestInterfaceSetOne.class
+com/google/devtools/build/android/desugar/testdata/java8/DefaultInterfaceMethodWithStaticInitializer$TestInterfaceSetThree$C.class
+com/google/devtools/build/android/desugar/testdata/java8/DefaultInterfaceMethodWithStaticInitializer$TestInterfaceSetThree$I1.class
+com/google/devtools/build/android/desugar/testdata/java8/DefaultInterfaceMethodWithStaticInitializer$TestInterfaceSetThree$I2.class
+com/google/devtools/build/android/desugar/testdata/java8/DefaultInterfaceMethodWithStaticInitializer$TestInterfaceSetThree.class
+com/google/devtools/build/android/desugar/testdata/java8/DefaultInterfaceMethodWithStaticInitializer$TestInterfaceSetTwo$C.class
+com/google/devtools/build/android/desugar/testdata/java8/DefaultInterfaceMethodWithStaticInitializer$TestInterfaceSetTwo$I1.class
+com/google/devtools/build/android/desugar/testdata/java8/DefaultInterfaceMethodWithStaticInitializer$TestInterfaceSetTwo$I2.class
+com/google/devtools/build/android/desugar/testdata/java8/DefaultInterfaceMethodWithStaticInitializer$TestInterfaceSetTwo.class
+com/google/devtools/build/android/desugar/testdata/java8/DefaultInterfaceMethodWithStaticInitializer.class
+com/google/devtools/build/android/desugar/testdata/java8/DefaultInterfaceWithBridges.class
+com/google/devtools/build/android/desugar/testdata/java8/DefaultInterfaceWithLambda.class
+com/google/devtools/build/android/desugar/testdata/java8/FunctionWithDefaultMethod$DoubleInts.class
+com/google/devtools/build/android/desugar/testdata/java8/FunctionWithDefaultMethod$DoubleInts2.class
+com/google/devtools/build/android/desugar/testdata/java8/FunctionWithDefaultMethod.class
+com/google/devtools/build/android/desugar/testdata/java8/FunctionalInterfaceWithInitializerAndDefaultMethods$ClassWithInitializer.class
+com/google/devtools/build/android/desugar/testdata/java8/FunctionalInterfaceWithInitializerAndDefaultMethods.class
+com/google/devtools/build/android/desugar/testdata/java8/GenericDefaultInterfaceWithLambda$ClassOne.class
+com/google/devtools/build/android/desugar/testdata/java8/GenericDefaultInterfaceWithLambda$ClassThree.class
+com/google/devtools/build/android/desugar/testdata/java8/GenericDefaultInterfaceWithLambda$ClassTwo.class
+com/google/devtools/build/android/desugar/testdata/java8/GenericDefaultInterfaceWithLambda$LevelOne.class
+com/google/devtools/build/android/desugar/testdata/java8/GenericDefaultInterfaceWithLambda$LevelTwo.class
+com/google/devtools/build/android/desugar/testdata/java8/GenericDefaultInterfaceWithLambda.class
+com/google/devtools/build/android/desugar/testdata/java8/GenericInterfaceWithDefaultMethod.class
+com/google/devtools/build/android/desugar/testdata/java8/InterfaceMethod$Concrete.class
+com/google/devtools/build/android/desugar/testdata/java8/InterfaceMethod.class
+com/google/devtools/build/android/desugar/testdata/java8/InterfaceWithDefaultAndBridgeMethods.class
+com/google/devtools/build/android/desugar/testdata/java8/InterfaceWithDefaultMethod$AlsoVersion2.class
+com/google/devtools/build/android/desugar/testdata/java8/InterfaceWithDefaultMethod$Redefine.class
+com/google/devtools/build/android/desugar/testdata/java8/InterfaceWithDefaultMethod$Version2.class
+com/google/devtools/build/android/desugar/testdata/java8/InterfaceWithDefaultMethod$Version2Base.class
+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/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
+com/google/devtools/build/android/desugar/testdata/java8/Java7InterfaceWithBridges$LevelOne.class
+com/google/devtools/build/android/desugar/testdata/java8/Java7InterfaceWithBridges$LevelTwo.class
+com/google/devtools/build/android/desugar/testdata/java8/Java7InterfaceWithBridges.class
+com/google/devtools/build/android/desugar/testdata/java8/Name1.class
+com/google/devtools/build/android/desugar/testdata/java8/Name2.class
+com/google/devtools/build/android/desugar/testdata/java8/Named$AbstractName.class
+com/google/devtools/build/android/desugar/testdata/java8/Named$AbstractNameBase.class
+com/google/devtools/build/android/desugar/testdata/java8/Named$DefaultName.class
+com/google/devtools/build/android/desugar/testdata/java8/Named$DefaultNameSubclass.class
+com/google/devtools/build/android/desugar/testdata/java8/Named$ExplicitName.class
+com/google/devtools/build/android/desugar/testdata/java8/Named$ExplicitNameBase.class
+com/google/devtools/build/android/desugar/testdata/java8/Named.class
+com/google/devtools/build/android/desugar/testdata/java8/TwoInheritedDefaultMethods.class
+com/google/devtools/build/android/desugar/testdata/java8/VisibilityTestClass.class
+com/google/devtools/build/android/desugar/testdata/java8/subpackage/
+com/google/devtools/build/android/desugar/testdata/java8/subpackage/PackagePrivateInterface.class
+com/google/devtools/build/android/desugar/testdata/java8/subpackage/PublicInterface.class
+com/google/devtools/build/android/desugar/testdata/CaptureLambda$$Lambda$0.class
+com/google/devtools/build/android/desugar/testdata/ClassCallingLongCompare$$Lambda$0.class
+com/google/devtools/build/android/desugar/testdata/ClassCallingLongCompare$$Lambda$1.class
+com/google/devtools/build/android/desugar/testdata/ClassCallingRequireNonNull$$Lambda$0.class
+com/google/devtools/build/android/desugar/testdata/ClassCallingRequireNonNull$$Lambda$1.class
+com/google/devtools/build/android/desugar/testdata/ClassCallingRequireNonNull$$Lambda$2.class
+com/google/devtools/build/android/desugar/testdata/ConcreteFunction$$Lambda$0.class
+com/google/devtools/build/android/desugar/testdata/ConcreteFunction$$Lambda$1.class
+com/google/devtools/build/android/desugar/testdata/ConstructorReference$$Lambda$0.class
+com/google/devtools/build/android/desugar/testdata/ConstructorReference$$Lambda$1.class
+com/google/devtools/build/android/desugar/testdata/ConstructorReference$$Lambda$2.class
+com/google/devtools/build/android/desugar/testdata/GuavaLambda$$Lambda$0.class
+com/google/devtools/build/android/desugar/testdata/InnerClassLambda$1$1$$Lambda$0.class
+com/google/devtools/build/android/desugar/testdata/InterfaceWithLambda$$Lambda$0.class
+com/google/devtools/build/android/desugar/testdata/Lambda$1$$Lambda$0.class
+com/google/devtools/build/android/desugar/testdata/Lambda$$Lambda$0.class
+com/google/devtools/build/android/desugar/testdata/Lambda$$Lambda$1.class
+com/google/devtools/build/android/desugar/testdata/LambdaInOverride$$Lambda$0.class
+com/google/devtools/build/android/desugar/testdata/MethodReference$$Lambda$0.class
+com/google/devtools/build/android/desugar/testdata/MethodReference$$Lambda$1.class
+com/google/devtools/build/android/desugar/testdata/MethodReference$$Lambda$2.class
+com/google/devtools/build/android/desugar/testdata/MethodReference$$Lambda$3.class
+com/google/devtools/build/android/desugar/testdata/MethodReference$$Lambda$4.class
+com/google/devtools/build/android/desugar/testdata/MethodReference$$Lambda$5.class
+com/google/devtools/build/android/desugar/testdata/MethodReference$$Lambda$6.class
+com/google/devtools/build/android/desugar/testdata/MethodReference$$Lambda$7.class
+com/google/devtools/build/android/desugar/testdata/MethodReferenceInSubclass$$Lambda$0.class
+com/google/devtools/build/android/desugar/testdata/MethodReferenceSuperclass$$Lambda$0.class
+com/google/devtools/build/android/desugar/testdata/OuterReferenceLambda$$Lambda$0.class
+com/google/devtools/build/android/desugar/testdata/java8/ConcreteDefaultInterfaceWithLambda$$Lambda$0.class
+com/google/devtools/build/android/desugar/testdata/java8/ConcreteOverridesDefaultWithLambda$$Lambda$0.class
+com/google/devtools/build/android/desugar/testdata/java8/ConcreteOverridesDefaultWithLambda$$Lambda$1.class
+com/google/devtools/build/android/desugar/testdata/java8/DefaultInterfaceWithLambda$$Lambda$0.class
+com/google/devtools/build/android/desugar/testdata/java8/FunctionWithDefaultMethod$DoubleInts$$Lambda$0.class
+com/google/devtools/build/android/desugar/testdata/java8/FunctionWithDefaultMethod$DoubleInts$$Lambda$1.class
+com/google/devtools/build/android/desugar/testdata/java8/FunctionWithDefaultMethod$DoubleInts$$Lambda$2.class
+com/google/devtools/build/android/desugar/testdata/java8/FunctionWithDefaultMethod$DoubleInts$$Lambda$3.class
+com/google/devtools/build/android/desugar/testdata/java8/FunctionWithDefaultMethod$$Lambda$0.class
+com/google/devtools/build/android/desugar/testdata/java8/FunctionWithDefaultMethod$$Lambda$1.class
+com/google/devtools/build/android/desugar/testdata/java8/FunctionalInterfaceWithInitializerAndDefaultMethods$ClassWithInitializer$$Lambda$0.class
+com/google/devtools/build/android/desugar/testdata/java8/GenericDefaultInterfaceWithLambda$$Lambda$0.class
+com/google/devtools/build/android/desugar/testdata/java8/GenericDefaultInterfaceWithLambda$$Lambda$1.class
+com/google/devtools/build/android/desugar/testdata/java8/InterfaceMethod$$Lambda$0.class
+com/google/devtools/build/android/desugar/testdata/java8/InterfaceMethod$$Lambda$1.class
+com/google/devtools/build/android/desugar/testdata/java8/InterfaceMethod$$Lambda$2.class \ No newline at end of file
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
new file mode 100644
index 0000000..256760b
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/testdata_desugared_without_lambda_desugared_jar_toc_golden.txt
@@ -0,0 +1,36 @@
+META-INF/
+META-INF/MANIFEST.MF
+com/
+com/google/
+com/google/devtools/
+com/google/devtools/build/
+com/google/devtools/build/android/
+com/google/devtools/build/android/desugar/
+com/google/devtools/build/android/desugar/testdata/
+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$SimpleResource.class
+com/google/devtools/build/android/desugar/testdata/ClassUsingTryWithResources.class
+com/google/devtools/build/android/desugar/testdata/ConcreteFunction$Parser.class
+com/google/devtools/build/android/desugar/testdata/ConcreteFunction$SpecializedParser.class
+com/google/devtools/build/android/desugar/testdata/ConcreteFunction.class
+com/google/devtools/build/android/desugar/testdata/ConstructorReference$1.class
+com/google/devtools/build/android/desugar/testdata/ConstructorReference$Empty.class
+com/google/devtools/build/android/desugar/testdata/ConstructorReference.class
+com/google/devtools/build/android/desugar/testdata/GuavaLambda.class
+com/google/devtools/build/android/desugar/testdata/InnerClassLambda$1$1.class
+com/google/devtools/build/android/desugar/testdata/InnerClassLambda$1.class
+com/google/devtools/build/android/desugar/testdata/InnerClassLambda.class
+com/google/devtools/build/android/desugar/testdata/InterfaceWithLambda.class
+com/google/devtools/build/android/desugar/testdata/Lambda$1.class
+com/google/devtools/build/android/desugar/testdata/Lambda.class
+com/google/devtools/build/android/desugar/testdata/LambdaInOverride.class
+com/google/devtools/build/android/desugar/testdata/MethodReference$Transformer.class
+com/google/devtools/build/android/desugar/testdata/MethodReference.class
+com/google/devtools/build/android/desugar/testdata/MethodReferenceInSubclass.class
+com/google/devtools/build/android/desugar/testdata/MethodReferenceSuperclass.class
+com/google/devtools/build/android/desugar/testdata/OuterReferenceLambda.class
+com/google/devtools/build/android/desugar/testdata/SpecializedFunction.class
+com/google/devtools/build/android/desugar/testdata/testresource.txt \ No newline at end of file