diff options
author | Ivan Gavrilovic <gavra@google.com> | 2017-09-25 20:18:36 +0000 |
---|---|---|
committer | android-build-merger <android-build-merger@google.com> | 2017-09-25 20:18:36 +0000 |
commit | 34477e8629b7244fbf9da92845834380606c8e1a (patch) | |
tree | d7601367dff9c70fb41ff4f76a5064173374b0c4 | |
parent | 550655cfbe384de9956137538b2b59c2e3208797 (diff) | |
parent | 3a238ab3ddcd05bfd6735ba02317476cfc46829e (diff) | |
download | desugar-34477e8629b7244fbf9da92845834380606c8e1a.tar.gz |
Merge remote-tracking branch 'aosp/upstream-master' am: 14905bc683
am: 3a238ab3dd
Change-Id: I42c5bc3246a1d22ac12ba53425aada744a1544d4
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 @<path> 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 Binary files differnew file mode 100644 index 0000000..1e0deca --- /dev/null +++ b/test/java/com/google/devtools/build/android/desugar/jacoco_0_7_5_default_method.jar 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 |