From c9ab0808f008a976220475fff19aee72d6839292 Mon Sep 17 00:00:00 2001 From: ccalvarin Date: Tue, 19 Dec 2017 12:23:17 -0800 Subject: Remove wrapper options support. RELNOTES: None. PiperOrigin-RevId: 179588174 GitOrigin-RevId: 16f1c19c2c4f68555bb23891e3a4da4d5ac4a68d Change-Id: I089b4b2e4c846682db552aa4c0e0905142e9278b --- .../com/google/devtools/common/options/Option.java | 18 --------- .../devtools/common/options/OptionDefinition.java | 5 --- .../common/options/OptionValueDescription.java | 46 ---------------------- .../devtools/common/options/OptionsParserImpl.java | 26 +++++------- .../common/options/processor/OptionProcessor.java | 29 -------------- 5 files changed, 9 insertions(+), 115 deletions(-) diff --git a/java/com/google/devtools/common/options/Option.java b/java/com/google/devtools/common/options/Option.java index 45f320a..f26a136 100644 --- a/java/com/google/devtools/common/options/Option.java +++ b/java/com/google/devtools/common/options/Option.java @@ -186,22 +186,4 @@ public @interface Option { * that the old name is deprecated and the new name should be used. */ String oldName() default ""; - - /** - * Indicates that this option is a wrapper for other options, and will be unwrapped when parsed. - * For example, if foo is a wrapper option, then "--foo=--bar=baz" will be parsed as the flag - * "--bar=baz" (rather than --foo taking the value "--bar=baz"). A wrapper option should have the - * type {@link Void} (if it is something other than Void, the parser will not assign a value to - * it). The {@link Option#implicitRequirements()}, {@link Option#expansion()}, {@link - * Option#converter()} attributes will not be processed. Wrapper options are implicitly repeatable - * (i.e., as though {@link Option#allowMultiple()} is true regardless of its value in the - * annotation). - * - *

Wrapper options are provided only for transitioning flags which appear as values to other - * flags, to top-level flags. Wrapper options should not be used in Invocation Policy, as - * expansion flags to other flags, or as implicit requirements to other flags. Use the inner flags - * instead. - */ - @Deprecated - boolean wrapperOption() default false; } diff --git a/java/com/google/devtools/common/options/OptionDefinition.java b/java/com/google/devtools/common/options/OptionDefinition.java index 84a9d2d..7b87744 100644 --- a/java/com/google/devtools/common/options/OptionDefinition.java +++ b/java/com/google/devtools/common/options/OptionDefinition.java @@ -156,11 +156,6 @@ public class OptionDefinition implements Comparable { return optionAnnotation.oldName(); } - /** {@link Option#wrapperOption()} ()} ()} */ - public boolean isWrapperOption() { - return optionAnnotation.wrapperOption(); - } - /** Returns whether an option --foo has a negative equivalent --nofoo. */ public boolean hasNegativeOption() { return getType().equals(boolean.class) || getType().equals(TriState.class); diff --git a/java/com/google/devtools/common/options/OptionValueDescription.java b/java/com/google/devtools/common/options/OptionValueDescription.java index 616b3b5..3fde138 100644 --- a/java/com/google/devtools/common/options/OptionValueDescription.java +++ b/java/com/google/devtools/common/options/OptionValueDescription.java @@ -98,8 +98,6 @@ public abstract class OptionValueDescription { return new RepeatableOptionValueDescription(option); } else if (option.hasImplicitRequirements()) { return new OptionWithImplicitRequirementsValueDescription(option); - } else if (option.isWrapperOption()) { - return new WrapperOptionValueDescription(option); } else { return new SingleOptionValueDescription(option); } @@ -430,50 +428,6 @@ public abstract class OptionValueDescription { optionDefinition, parsedOption.getSource())); } } - - /** Form for options that contain other options in the value text to which they expand. */ - private static final class WrapperOptionValueDescription extends OptionValueDescription { - - WrapperOptionValueDescription(OptionDefinition optionDefinition) { - super(optionDefinition); - } - - @Override - public Object getValue() { - return null; - } - - @Override - public String getSourceString() { - return null; - } - - @Override - ExpansionBundle addOptionInstance(ParsedOptionDescription parsedOption, List warnings) - throws OptionsParsingException { - if (!parsedOption.getUnconvertedValue().startsWith("-")) { - throw new OptionsParsingException( - String.format( - "Invalid value format for %s. You may have meant --%s=--%s", - optionDefinition, - optionDefinition.getOptionName(), - parsedOption.getUnconvertedValue())); - } - return new ExpansionBundle( - ImmutableList.of(parsedOption.getUnconvertedValue()), - (parsedOption.getSource() == null) - ? String.format("unwrapped from %s", optionDefinition) - : String.format( - "unwrapped from %s (source %s)", optionDefinition, parsedOption.getSource())); - } - - @Override - public ImmutableList getCanonicalInstances() { - // No wrapper options get listed in the canonical form - the options they are wrapping will - // be in the right place. - return ImmutableList.of(); - } - } } diff --git a/java/com/google/devtools/common/options/OptionsParserImpl.java b/java/com/google/devtools/common/options/OptionsParserImpl.java index 496927b..5ce35da 100644 --- a/java/com/google/devtools/common/options/OptionsParserImpl.java +++ b/java/com/google/devtools/common/options/OptionsParserImpl.java @@ -392,16 +392,15 @@ class OptionsParserImpl { @Nullable String unconvertedValue = parsedOption.getUnconvertedValue(); // There are 3 types of flags that expand to other flag values. Expansion flags are the - // accepted way to do this, but two legacy features remain: implicit requirements and wrapper - // options. We rely on the OptionProcessor compile-time check's guarantee that no option sets - // multiple of these behaviors. (In Bazel, --config is another such flag, but that expansion + // accepted way to do this, but implicit requirements also do this. We rely on the + // OptionProcessor compile-time check's guarantee that no option sets + // both expansion behaviors. (In Bazel, --config is another such flag, but that expansion // is not controlled within the options parser, so we ignore it here) // As much as possible, we want the behaviors of these different types of flags to be // identical, as this minimizes the number of edge cases, but we do not yet track these values - // in the same way. Wrapper options are replaced by their value and implicit requirements are - // hidden from the reported lists of parsed options. - if (parsedOption.getImplicitDependent() == null && !optionDefinition.isWrapperOption()) { + // in the same way. + if (parsedOption.getImplicitDependent() == null) { // Log explicit options and expanded options in the order they are parsed (can be sorted // later). This information is needed to correctly canonicalize flags. parsedOptions.add(parsedOption); @@ -416,13 +415,7 @@ class OptionsParserImpl { optionDefinition.isExpansionOption() ? optionDefinition : null, expansionBundle.expansionArgs); if (!residueAndPriority.residue.isEmpty()) { - if (optionDefinition.isWrapperOption()) { - throw new OptionsParsingException( - "Unparsed options remain after unwrapping " - + unconvertedValue - + ": " - + Joiner.on(' ').join(residueAndPriority.residue)); - } else { + // Throw an assertion here, because this indicates an error in the definition of this // option's expansion or requirements, not with the input as provided by the user. throw new AssertionError( @@ -430,7 +423,7 @@ class OptionsParserImpl { + unconvertedValue + ": " + Joiner.on(' ').join(residueAndPriority.residue)); - } + } } } @@ -506,9 +499,8 @@ class OptionsParserImpl { // Special-case boolean to supply value based on presence of "no" prefix. if (optionDefinition.usesBooleanValueSyntax()) { unconvertedValue = booleanValue ? "1" : "0"; - } else if (optionDefinition.getType().equals(Void.class) - && !optionDefinition.isWrapperOption()) { - // This is expected, Void type options have no args (unless they're wrapper options). + } else if (optionDefinition.getType().equals(Void.class)) { + // This is expected, Void type options have no args. } else if (nextArgs.hasNext()) { // "--flag value" form unconvertedValue = nextArgs.next(); diff --git a/java/com/google/devtools/common/options/processor/OptionProcessor.java b/java/com/google/devtools/common/options/processor/OptionProcessor.java index fd7c023..76b9640 100644 --- a/java/com/google/devtools/common/options/processor/OptionProcessor.java +++ b/java/com/google/devtools/common/options/processor/OptionProcessor.java @@ -476,10 +476,6 @@ public final class OptionProcessor extends AbstractProcessor { } if (isExpansion || hasImplicitRequirements) { - if (annotation.wrapperOption()) { - throw new OptionProcessorException( - optionField, "Wrapper options cannot have expansions or implicit requirements."); - } if (annotation.allowMultiple()) { throw new OptionProcessorException( optionField, @@ -488,30 +484,6 @@ public final class OptionProcessor extends AbstractProcessor { } } - /** - * Some flags wrap other flags. They are objectively useless, as there is no difference between - * passing --wrapper=--foo and --foo other than the "source" information tracked. This - * functionality comes from requiring compatibility at some past point in time, but is actively - * being deprecated. No non-deprecated flag can use this feature. - */ - private void checkWrapperOptions(VariableElement optionField) throws OptionProcessorException { - Option annotation = optionField.getAnnotation(Option.class); - if (annotation.wrapperOption()) { - if (annotation.deprecationWarning().isEmpty()) { - throw new OptionProcessorException( - optionField, - "Can't have non deprecated wrapper options, this feature is deprecated. " - + "Please add a deprecationWarning."); - } - if (!ImmutableList.copyOf(annotation.metadataTags()).contains(OptionMetadataTag.DEPRECATED)) { - throw new OptionProcessorException( - optionField, - "Can't have non deprecated wrapper options, this feature is deprecated. " - + "Please add the metadata tag DEPRECATED."); - } - } - } - @Override public boolean process(Set annotations, RoundEnvironment roundEnv) { for (Element annotatedElement : roundEnv.getElementsAnnotatedWith(Option.class)) { @@ -528,7 +500,6 @@ public final class OptionProcessor extends AbstractProcessor { checkConverter(optionField); checkEffectTagRationality(optionField); checkMetadataTagAndCategoryRationality(optionField); - checkWrapperOptions(optionField); } catch (OptionProcessorException e) { error(e.getElementInError(), e.getMessage()); } -- cgit v1.2.3 From c1733d1549fe715c9915ae7cabebc717058313da Mon Sep 17 00:00:00 2001 From: ccalvarin Date: Thu, 21 Dec 2017 14:17:10 -0800 Subject: Warn about config expansions as we do for other expansions. If an expanded value overrides an explicit value, users who do not know the contents of the expansion may be surprised. We already warned about this for hard-coded expansions, and this is now applicable for --config expansions as well. This will only warn when a single-valued option has its value replaced. Options that accumulate multiple values in a list (e.g., --copt) will silently include both explicit and expanded values. RELNOTES: None. PiperOrigin-RevId: 179857526 GitOrigin-RevId: 0421d7d8566a6fbe35e17a1edc3ab4d622aa6c9e Change-Id: Ie028995d2c4cbb90614ea8094b662d1b6e319241 --- .../common/options/OptionInstanceOrigin.java | 12 ++--- .../common/options/OptionValueDescription.java | 10 ++-- .../devtools/common/options/OptionsParser.java | 23 ++++++++-- .../devtools/common/options/OptionsParserImpl.java | 52 ++++++++++++--------- .../common/options/ParsedOptionDescription.java | 53 ++++++++++++++++------ 5 files changed, 99 insertions(+), 51 deletions(-) diff --git a/java/com/google/devtools/common/options/OptionInstanceOrigin.java b/java/com/google/devtools/common/options/OptionInstanceOrigin.java index 584e75b..b0782f8 100644 --- a/java/com/google/devtools/common/options/OptionInstanceOrigin.java +++ b/java/com/google/devtools/common/options/OptionInstanceOrigin.java @@ -22,14 +22,14 @@ import javax.annotation.Nullable; public class OptionInstanceOrigin { private final OptionPriority priority; @Nullable private final String source; - @Nullable private final OptionDefinition implicitDependent; - @Nullable private final OptionDefinition expandedFrom; + @Nullable private final ParsedOptionDescription implicitDependent; + @Nullable private final ParsedOptionDescription expandedFrom; public OptionInstanceOrigin( OptionPriority priority, String source, - OptionDefinition implicitDependent, - OptionDefinition expandedFrom) { + ParsedOptionDescription implicitDependent, + ParsedOptionDescription expandedFrom) { this.priority = priority; this.source = source; this.implicitDependent = implicitDependent; @@ -46,12 +46,12 @@ public class OptionInstanceOrigin { } @Nullable - public OptionDefinition getImplicitDependent() { + public ParsedOptionDescription getImplicitDependent() { return implicitDependent; } @Nullable - public OptionDefinition getExpandedFrom() { + public ParsedOptionDescription getExpandedFrom() { return expandedFrom; } } diff --git a/java/com/google/devtools/common/options/OptionValueDescription.java b/java/com/google/devtools/common/options/OptionValueDescription.java index 3fde138..f52e8a5 100644 --- a/java/com/google/devtools/common/options/OptionValueDescription.java +++ b/java/com/google/devtools/common/options/OptionValueDescription.java @@ -185,11 +185,11 @@ public abstract class OptionValueDescription { // log warnings describing the change. if (parsedOption.getPriority().compareTo(effectiveOptionInstance.getPriority()) >= 0) { // Identify the option that might have led to the current and new value of this option. - OptionDefinition implicitDependent = parsedOption.getImplicitDependent(); - OptionDefinition expandedFrom = parsedOption.getExpandedFrom(); - OptionDefinition optionThatDependsOnEffectiveValue = + ParsedOptionDescription implicitDependent = parsedOption.getImplicitDependent(); + ParsedOptionDescription expandedFrom = parsedOption.getExpandedFrom(); + ParsedOptionDescription optionThatDependsOnEffectiveValue = effectiveOptionInstance.getImplicitDependent(); - OptionDefinition optionThatExpandedToEffectiveValue = + ParsedOptionDescription optionThatExpandedToEffectiveValue = effectiveOptionInstance.getExpandedFrom(); Object newValue = parsedOption.getConvertedValue(); @@ -225,7 +225,7 @@ public abstract class OptionValueDescription { // Create a warning if an expansion option overrides an explicit option: warnings.add( String.format( - "%s was expanded and now overrides a previous explicitly specified %s with %s", + "%s was expanded and now overrides the explicit option %s with %s", expandedFrom, effectiveOptionInstance.getCommandLineForm(), parsedOption.getCommandLineForm())); diff --git a/java/com/google/devtools/common/options/OptionsParser.java b/java/com/google/devtools/common/options/OptionsParser.java index fb7161c..b7da004 100644 --- a/java/com/google/devtools/common/options/OptionsParser.java +++ b/java/com/google/devtools/common/options/OptionsParser.java @@ -619,13 +619,26 @@ public class OptionsParser implements OptionsProvider { } } - public void parseOptionsFixedAtSpecificPriority( - OptionPriority priority, String source, List args) throws OptionsParsingException { - Preconditions.checkNotNull(priority, "Priority not specified for arglist " + args); + /** + * Parses the args at the priority of the provided option. This is useful for after-the-fact + * expansion. + * + * @param optionToExpand the option that is being "expanded" after the fact. The provided args + * will have the same priority as this option. + * @param source a description of where the expansion arguments came from. + * @param args the arguments to parse as the expansion. Order matters, as the value of a flag may + * be in the following argument. + */ + public void parseArgsFixedAsExpansionOfOption( + ParsedOptionDescription optionToExpand, String source, List args) + throws OptionsParsingException { + Preconditions.checkNotNull( + optionToExpand, "Option for expansion not specified for arglist " + args); Preconditions.checkArgument( - priority.getPriorityCategory() != OptionPriority.PriorityCategory.DEFAULT, + optionToExpand.getPriority().getPriorityCategory() + != OptionPriority.PriorityCategory.DEFAULT, "Priority cannot be default, which was specified for arglist " + args); - residue.addAll(impl.parseOptionsFixedAtSpecificPriority(priority, o -> source, args)); + residue.addAll(impl.parseArgsFixedAsExpansionOfOption(optionToExpand, o -> source, args)); if (!allowResidue && !residue.isEmpty()) { String errorMsg = "Unrecognized arguments: " + Joiner.on(' ').join(residue); throw new OptionsParsingException(errorMsg); diff --git a/java/com/google/devtools/common/options/OptionsParserImpl.java b/java/com/google/devtools/common/options/OptionsParserImpl.java index 5ce35da..bc66cc3 100644 --- a/java/com/google/devtools/common/options/OptionsParserImpl.java +++ b/java/com/google/devtools/common/options/OptionsParserImpl.java @@ -204,30 +204,33 @@ class OptionsParserImpl { * OptionInstanceOrigin)} */ ImmutableList getExpansionValueDescriptions( - OptionDefinition expansionFlag, OptionInstanceOrigin originOfExpansionFlag) + OptionDefinition expansionFlagDef, OptionInstanceOrigin originOfExpansionFlag) throws OptionsParsingException { ImmutableList.Builder builder = ImmutableList.builder(); OptionInstanceOrigin originOfSubflags; ImmutableList options; - if (expansionFlag.hasImplicitRequirements()) { - options = ImmutableList.copyOf(expansionFlag.getImplicitRequirements()); + ParsedOptionDescription expansionFlagParsedDummy = + ParsedOptionDescription.newDummyInstance(expansionFlagDef, originOfExpansionFlag); + if (expansionFlagDef.hasImplicitRequirements()) { + options = ImmutableList.copyOf(expansionFlagDef.getImplicitRequirements()); originOfSubflags = new OptionInstanceOrigin( originOfExpansionFlag.getPriority(), String.format( "implicitly required by %s (source: %s)", - expansionFlag, originOfExpansionFlag.getSource()), - expansionFlag, + expansionFlagDef, originOfExpansionFlag.getSource()), + expansionFlagParsedDummy, null); - } else if (expansionFlag.isExpansionOption()) { - options = optionsData.getEvaluatedExpansion(expansionFlag); + } else if (expansionFlagDef.isExpansionOption()) { + options = optionsData.getEvaluatedExpansion(expansionFlagDef); originOfSubflags = new OptionInstanceOrigin( originOfExpansionFlag.getPriority(), String.format( - "expanded by %s (source: %s)", expansionFlag, originOfExpansionFlag.getSource()), + "expanded by %s (source: %s)", + expansionFlagDef, originOfExpansionFlag.getSource()), null, - expansionFlag); + expansionFlagParsedDummy); } else { return ImmutableList.of(); } @@ -284,12 +287,19 @@ class OptionsParserImpl { } } - /** Parses the args at the fixed priority. */ - List parseOptionsFixedAtSpecificPriority( - OptionPriority priority, Function sourceFunction, List args) + /** Implements {@link OptionsParser#parseArgsFixedAsExpansionOfOption} */ + List parseArgsFixedAsExpansionOfOption( + ParsedOptionDescription optionToExpand, + Function sourceFunction, + List args) throws OptionsParsingException { ResidueAndPriority residueAndPriority = - parse(OptionPriority.getLockedPriority(priority), sourceFunction, null, null, args); + parse( + OptionPriority.getLockedPriority(optionToExpand.getPriority()), + sourceFunction, + null, + optionToExpand, + args); return residueAndPriority.residue; } @@ -304,8 +314,8 @@ class OptionsParserImpl { private ResidueAndPriority parse( OptionPriority priority, Function sourceFunction, - OptionDefinition implicitDependent, - OptionDefinition expandedFrom, + ParsedOptionDescription implicitDependent, + ParsedOptionDescription expandedFrom, List args) throws OptionsParsingException { List unparsedArgs = new ArrayList<>(); @@ -369,7 +379,7 @@ class OptionsParserImpl { priorityCategory); handleNewParsedOption( - new ParsedOptionDescription( + ParsedOptionDescription.newParsedOptionDescription( option, String.format("--%s=%s", option.getOptionName(), unconvertedValue), unconvertedValue, @@ -411,8 +421,8 @@ class OptionsParserImpl { parse( OptionPriority.getLockedPriority(parsedOption.getPriority()), o -> expansionBundle.sourceOfExpansionArgs, - optionDefinition.hasImplicitRequirements() ? optionDefinition : null, - optionDefinition.isExpansionOption() ? optionDefinition : null, + optionDefinition.hasImplicitRequirements() ? parsedOption : null, + optionDefinition.isExpansionOption() ? parsedOption : null, expansionBundle.expansionArgs); if (!residueAndPriority.residue.isEmpty()) { @@ -433,8 +443,8 @@ class OptionsParserImpl { Iterator nextArgs, OptionPriority priority, Function sourceFunction, - OptionDefinition implicitDependent, - OptionDefinition expandedFrom) + ParsedOptionDescription implicitDependent, + ParsedOptionDescription expandedFrom) throws OptionsParsingException { // Store the way this option was parsed on the command line. @@ -510,7 +520,7 @@ class OptionsParserImpl { } } - return new ParsedOptionDescription( + return ParsedOptionDescription.newParsedOptionDescription( optionDefinition, commandLineForm.toString(), unconvertedValue, diff --git a/java/com/google/devtools/common/options/ParsedOptionDescription.java b/java/com/google/devtools/common/options/ParsedOptionDescription.java index f55f8ad..5088153 100644 --- a/java/com/google/devtools/common/options/ParsedOptionDescription.java +++ b/java/com/google/devtools/common/options/ParsedOptionDescription.java @@ -14,6 +14,7 @@ package com.google.devtools.common.options; +import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import java.util.function.Function; import javax.annotation.Nullable; @@ -27,25 +28,49 @@ import javax.annotation.Nullable; public final class ParsedOptionDescription { private final OptionDefinition optionDefinition; - private final String commandLineForm; + @Nullable private final String commandLineForm; @Nullable private final String unconvertedValue; private final OptionInstanceOrigin origin; - public ParsedOptionDescription( + private ParsedOptionDescription( OptionDefinition optionDefinition, - String commandLineForm, + @Nullable String commandLineForm, @Nullable String unconvertedValue, OptionInstanceOrigin origin) { - this.optionDefinition = optionDefinition; + this.optionDefinition = Preconditions.checkNotNull(optionDefinition); this.commandLineForm = commandLineForm; this.unconvertedValue = unconvertedValue; - this.origin = origin; + this.origin = Preconditions.checkNotNull(origin); + } + + static ParsedOptionDescription newParsedOptionDescription( + OptionDefinition optionDefinition, + String commandLineForm, + @Nullable String unconvertedValue, + OptionInstanceOrigin origin) { + // An actual ParsedOptionDescription should always have a form in which it was parsed, but some + // options, such as expansion options, legitimately have no value. + return new ParsedOptionDescription( + optionDefinition, + Preconditions.checkNotNull(commandLineForm), + unconvertedValue, + origin); + } + + /** + * This factory should be used when there is no actual parsed option, since in those cases we do + * not have an original value or form that the option took. + */ + static ParsedOptionDescription newDummyInstance( + OptionDefinition optionDefinition, OptionInstanceOrigin origin) { + return new ParsedOptionDescription(optionDefinition, null, null, origin); } public OptionDefinition getOptionDefinition() { return optionDefinition; } + @Nullable public String getCommandLineForm() { return commandLineForm; } @@ -127,11 +152,11 @@ public final class ParsedOptionDescription { return origin.getSource(); } - OptionDefinition getImplicitDependent() { + ParsedOptionDescription getImplicitDependent() { return origin.getImplicitDependent(); } - OptionDefinition getExpandedFrom() { + ParsedOptionDescription getExpandedFrom() { return origin.getExpandedFrom(); } @@ -152,14 +177,14 @@ public final class ParsedOptionDescription { @Override public String toString() { - StringBuilder result = new StringBuilder(); - result.append(optionDefinition); - result.append("set to '").append(unconvertedValue).append("' "); - result.append("with priority ").append(origin.getPriority()); - if (origin.getSource() != null) { - result.append(" and source '").append(origin.getSource()).append("'"); + // Check that a dummy value-less option instance does not output all the default information. + if (commandLineForm == null) { + return optionDefinition.toString(); } - return result.toString(); + String source = origin.getSource(); + return String.format( + "option '%s'%s", + commandLineForm, source == null ? "" : String.format(" (source %s)", source)); } } -- cgit v1.2.3 From 45383a982f296015a233fdb5ffa46fad9e0ae245 Mon Sep 17 00:00:00 2001 From: cnsun Date: Fri, 5 Jan 2018 11:10:25 -0800 Subject: Relax the assertion on the inferred resource type. Now we only require that the resource type should have a (public) close() method. The old version requires the resource type implements AutoCloseable. When the classpath provided to Desugar has some problems, the resource type may not implement AutoCloseable, though it has the close() method. RELNOTES:n/a. PiperOrigin-RevId: 180950815 GitOrigin-RevId: 7bde688a21b781caa666fe2bebe4482cf987270b Change-Id: Id0a03911e12f903ce62fec72317a7dbc8d311287 --- .../android/desugar/TryWithResourcesRewriter.java | 46 ++++++++++++++++------ .../DesugarTryWithResourcesFunctionalTest.java | 30 ++++++++++++++ .../testdata/ClassUsingTryWithResources.java | 14 +++++++ ...twr_with_large_minsdkversion_jar_toc_golden.txt | 1 + ...gared_for_try_with_resources_jar_toc_golden.txt | 1 + .../desugar/testdata_desugared_jar_toc_golden.txt | 1 + .../testdata_desugared_java8_jar_toc_golden.txt | 1 + ...red_without_lambda_desugared_jar_toc_golden.txt | 1 + 8 files changed, 83 insertions(+), 12 deletions(-) diff --git a/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriter.java b/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriter.java index 8e6d6d5..e8509e7 100644 --- a/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriter.java +++ b/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriter.java @@ -311,18 +311,20 @@ public class TryWithResourcesRewriter extends ClassVisitor { InferredType resourceType = typeInference.getTypeOfOperandFromTop(0); Optional resourceClassInternalName = resourceType.getInternalName(); - checkState( - resourceClassInternalName.isPresent(), - "The resource class %s is not a reference type in %s.%s", - resourceType, - internalName, - methodSignature); - checkState( - isAssignableFrom( - "java.lang.AutoCloseable", resourceClassInternalName.get().replace('/', '.')), - "The resource type should be a subclass of java.lang.AutoCloseable: %s", - resourceClassInternalName); - + { + // Check the resource type. + checkState( + resourceClassInternalName.isPresent(), + "The resource class %s is not a reference type in %s.%s", + resourceType, + internalName, + methodSignature); + String resourceClassName = resourceClassInternalName.get().replace('/', '.'); + checkState( + hasCloseMethod(resourceClassName), + "The resource class %s should have a close() method.", + resourceClassName); + } resourceTypeInternalNames.add(resourceClassInternalName.get()); super.visitMethodInsn( opcode, @@ -356,6 +358,26 @@ public class TryWithResourcesRewriter extends ClassVisitor { return isAssignableFrom("java.lang.Throwable", owner.replace('/', '.')); } + private boolean hasCloseMethod(String resourceClassName) { + try { + Class klass = classLoader.loadClass(resourceClassName); + klass.getMethod("close"); + return true; + } catch (ClassNotFoundException e) { + throw new AssertionError( + "Failed to load class " + + resourceClassName + + " when desugaring method " + + internalName + + "." + + methodSignature, + e); + } catch (NoSuchMethodException e) { + // There is no close() method in the class, so return false. + return false; + } + } + private boolean isAssignableFrom(String baseClassName, String subClassName) { try { Class baseClass = classLoader.loadClass(baseClassName); diff --git a/test/java/com/google/devtools/build/android/desugar/DesugarTryWithResourcesFunctionalTest.java b/test/java/com/google/devtools/build/android/desugar/DesugarTryWithResourcesFunctionalTest.java index 38df9e3..2d567d3 100644 --- a/test/java/com/google/devtools/build/android/desugar/DesugarTryWithResourcesFunctionalTest.java +++ b/test/java/com/google/devtools/build/android/desugar/DesugarTryWithResourcesFunctionalTest.java @@ -96,4 +96,34 @@ public class DesugarTryWithResourcesFunctionalTest { } } } + + @Test + public void testInheritanceTryWithResources() { + + try { + ClassUsingTryWithResources.inheritanceTryWithResources(); + fail("Expected RuntimeException"); + } catch (Exception expected) { + assertThat(expected.getClass()).isEqualTo(RuntimeException.class); + + String expectedStrategyName = getTwrStrategyClassNameSpecifiedInSystemProperty(); + assertThat(getStrategyClassName()).isEqualTo(expectedStrategyName); + if (isMimicStrategy()) { + assertThat(expected.getSuppressed()).isEmpty(); + assertThat(ThrowableExtension.getSuppressed(expected)).hasLength(1); + assertThat(ThrowableExtension.getSuppressed(expected)[0].getClass()) + .isEqualTo(IOException.class); + } else if (isReuseStrategy()) { + assertThat(expected.getSuppressed()).hasLength(1); + assertThat(expected.getSuppressed()[0].getClass()).isEqualTo(IOException.class); + assertThat(ThrowableExtension.getSuppressed(expected)[0].getClass()) + .isEqualTo(IOException.class); + } else if (isNullStrategy()) { + assertThat(expected.getSuppressed()).isEmpty(); + assertThat(ThrowableExtension.getSuppressed(expected)).isEmpty(); + } else { + fail("unexpected desugaring strategy " + getStrategyClassName()); + } + } + } } diff --git a/test/java/com/google/devtools/build/android/desugar/testdata/ClassUsingTryWithResources.java b/test/java/com/google/devtools/build/android/desugar/testdata/ClassUsingTryWithResources.java index c340c84..e4f7f18 100644 --- a/test/java/com/google/devtools/build/android/desugar/testdata/ClassUsingTryWithResources.java +++ b/test/java/com/google/devtools/build/android/desugar/testdata/ClassUsingTryWithResources.java @@ -49,6 +49,9 @@ public class ClassUsingTryWithResources { } } + /** A resource inheriting the close() method from its parent. */ + public static class InheritanceResource extends SimpleResource {} + /** This method will always throw {@link java.lang.Exception}. */ public static void simpleTryWithResources() throws Exception { // Throwable.addSuppressed(Throwable) should be called in the following block. @@ -57,6 +60,17 @@ public class ClassUsingTryWithResources { } } + /** + * This method useds {@link InheritanceResource}, which inherits all methods from {@link + * SimpleResource}. + */ + public static void inheritanceTryWithResources() throws Exception { + // Throwable.addSuppressed(Throwable) should be called in the following block. + try (InheritanceResource resource = new InheritanceResource()) { + resource.call(true); + } + } + public static Throwable[] checkSuppressedExceptions(boolean throwException) { // Throwable.addSuppressed(Throwable) should be called in the following block. try (SimpleResource resource = new SimpleResource()) { diff --git a/test/java/com/google/devtools/build/android/desugar/testdata_desugared_for_disabling_twr_with_large_minsdkversion_jar_toc_golden.txt b/test/java/com/google/devtools/build/android/desugar/testdata_desugared_for_disabling_twr_with_large_minsdkversion_jar_toc_golden.txt index b7c3c25..8e396f5 100644 --- a/test/java/com/google/devtools/build/android/desugar/testdata_desugared_for_disabling_twr_with_large_minsdkversion_jar_toc_golden.txt +++ b/test/java/com/google/devtools/build/android/desugar/testdata_desugared_for_disabling_twr_with_large_minsdkversion_jar_toc_golden.txt @@ -12,6 +12,7 @@ com/google/devtools/build/android/desugar/testdata/CaptureLambda.class com/google/devtools/build/android/desugar/testdata/ClassCallingLongCompare$LongCmpFunc.class com/google/devtools/build/android/desugar/testdata/ClassCallingLongCompare.class com/google/devtools/build/android/desugar/testdata/ClassCallingRequireNonNull.class +com/google/devtools/build/android/desugar/testdata/ClassUsingTryWithResources$InheritanceResource.class com/google/devtools/build/android/desugar/testdata/ClassUsingTryWithResources$SimpleResource.class com/google/devtools/build/android/desugar/testdata/ClassUsingTryWithResources.class com/google/devtools/build/android/desugar/testdata/ConcreteFunction$Parser.class diff --git a/test/java/com/google/devtools/build/android/desugar/testdata_desugared_for_try_with_resources_jar_toc_golden.txt b/test/java/com/google/devtools/build/android/desugar/testdata_desugared_for_try_with_resources_jar_toc_golden.txt index d03b121..41e8d1c 100644 --- a/test/java/com/google/devtools/build/android/desugar/testdata_desugared_for_try_with_resources_jar_toc_golden.txt +++ b/test/java/com/google/devtools/build/android/desugar/testdata_desugared_for_try_with_resources_jar_toc_golden.txt @@ -12,6 +12,7 @@ com/google/devtools/build/android/desugar/testdata/CaptureLambda.class com/google/devtools/build/android/desugar/testdata/ClassCallingLongCompare$LongCmpFunc.class com/google/devtools/build/android/desugar/testdata/ClassCallingLongCompare.class com/google/devtools/build/android/desugar/testdata/ClassCallingRequireNonNull.class +com/google/devtools/build/android/desugar/testdata/ClassUsingTryWithResources$InheritanceResource.class com/google/devtools/build/android/desugar/testdata/ClassUsingTryWithResources$SimpleResource.class com/google/devtools/build/android/desugar/testdata/ClassUsingTryWithResources.class com/google/devtools/build/android/desugar/testdata/ConcreteFunction$Parser.class diff --git a/test/java/com/google/devtools/build/android/desugar/testdata_desugared_jar_toc_golden.txt b/test/java/com/google/devtools/build/android/desugar/testdata_desugared_jar_toc_golden.txt index 91fc415..b79961e 100644 --- a/test/java/com/google/devtools/build/android/desugar/testdata_desugared_jar_toc_golden.txt +++ b/test/java/com/google/devtools/build/android/desugar/testdata_desugared_jar_toc_golden.txt @@ -12,6 +12,7 @@ com/google/devtools/build/android/desugar/testdata/CaptureLambda.class com/google/devtools/build/android/desugar/testdata/ClassCallingLongCompare$LongCmpFunc.class com/google/devtools/build/android/desugar/testdata/ClassCallingLongCompare.class com/google/devtools/build/android/desugar/testdata/ClassCallingRequireNonNull.class +com/google/devtools/build/android/desugar/testdata/ClassUsingTryWithResources$InheritanceResource.class com/google/devtools/build/android/desugar/testdata/ClassUsingTryWithResources$SimpleResource.class com/google/devtools/build/android/desugar/testdata/ClassUsingTryWithResources.class com/google/devtools/build/android/desugar/testdata/ConcreteFunction$Parser.class diff --git a/test/java/com/google/devtools/build/android/desugar/testdata_desugared_java8_jar_toc_golden.txt b/test/java/com/google/devtools/build/android/desugar/testdata_desugared_java8_jar_toc_golden.txt index 907edd0..67a46c3 100644 --- a/test/java/com/google/devtools/build/android/desugar/testdata_desugared_java8_jar_toc_golden.txt +++ b/test/java/com/google/devtools/build/android/desugar/testdata_desugared_java8_jar_toc_golden.txt @@ -12,6 +12,7 @@ com/google/devtools/build/android/desugar/testdata/CaptureLambda.class com/google/devtools/build/android/desugar/testdata/ClassCallingLongCompare$LongCmpFunc.class com/google/devtools/build/android/desugar/testdata/ClassCallingLongCompare.class com/google/devtools/build/android/desugar/testdata/ClassCallingRequireNonNull.class +com/google/devtools/build/android/desugar/testdata/ClassUsingTryWithResources$InheritanceResource.class com/google/devtools/build/android/desugar/testdata/ClassUsingTryWithResources$SimpleResource.class com/google/devtools/build/android/desugar/testdata/ClassUsingTryWithResources.class com/google/devtools/build/android/desugar/testdata/ConcreteFunction$Parser.class diff --git a/test/java/com/google/devtools/build/android/desugar/testdata_desugared_without_lambda_desugared_jar_toc_golden.txt b/test/java/com/google/devtools/build/android/desugar/testdata_desugared_without_lambda_desugared_jar_toc_golden.txt index 256760b..4fdc023 100644 --- a/test/java/com/google/devtools/build/android/desugar/testdata_desugared_without_lambda_desugared_jar_toc_golden.txt +++ b/test/java/com/google/devtools/build/android/desugar/testdata_desugared_without_lambda_desugared_jar_toc_golden.txt @@ -11,6 +11,7 @@ com/google/devtools/build/android/desugar/testdata/CaptureLambda.class com/google/devtools/build/android/desugar/testdata/ClassCallingLongCompare$LongCmpFunc.class com/google/devtools/build/android/desugar/testdata/ClassCallingLongCompare.class com/google/devtools/build/android/desugar/testdata/ClassCallingRequireNonNull.class +com/google/devtools/build/android/desugar/testdata/ClassUsingTryWithResources$InheritanceResource.class com/google/devtools/build/android/desugar/testdata/ClassUsingTryWithResources$SimpleResource.class com/google/devtools/build/android/desugar/testdata/ClassUsingTryWithResources.class com/google/devtools/build/android/desugar/testdata/ConcreteFunction$Parser.class -- cgit v1.2.3 From 485a7d2e293bea5cbf8e36702fe26499d22a54b7 Mon Sep 17 00:00:00 2001 From: Liam Miller-Cushon Date: Wed, 10 Jan 2018 08:00:00 -0800 Subject: Fix StreamResourceLeak error Fixes #4414 Change-Id: If47d9b97a220ae9e9feec2996be1f7df6491e93b PiperOrigin-RevId: 181465165 GitOrigin-RevId: 65c13dd5a4c1b4b5a072f7680b8f1cf3c5079b52 --- .../devtools/build/android/desugar/LambdaClassMaker.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/java/com/google/devtools/build/android/desugar/LambdaClassMaker.java b/java/com/google/devtools/build/android/desugar/LambdaClassMaker.java index 72f039a..94c6bbc 100644 --- a/java/com/google/devtools/build/android/desugar/LambdaClassMaker.java +++ b/java/com/google/devtools/build/android/desugar/LambdaClassMaker.java @@ -81,12 +81,12 @@ class LambdaClassMaker { if (!Files.exists(rootPathPrefix.getParent())) { return ImmutableList.of(); } - try (Stream paths = - Files.list(rootPathPrefix.getParent()) - .filter( - path -> path.toString().startsWith(rootPathPrefixStr) - && !existingPaths.contains(path))) { - return paths.collect(ImmutableList.toImmutableList()); + try (Stream paths = Files.list(rootPathPrefix.getParent())) { + return paths + .filter( + path -> + path.toString().startsWith(rootPathPrefixStr) && !existingPaths.contains(path)) + .collect(ImmutableList.toImmutableList()); } } } -- cgit v1.2.3 From 47bb3bfbc969ea3ac98381cb67c3a6b8821012f5 Mon Sep 17 00:00:00 2001 From: kmb Date: Mon, 5 Feb 2018 18:18:15 -0800 Subject: Basic tooling to desugar select core libraries RELNOTES: None. PiperOrigin-RevId: 184619885 GitOrigin-RevId: 1324318ea0fe60350c0a5179818fc1c97d4ec854 Change-Id: I2d9bc87180067959b618641a188d83a8d7c24b3b --- .../desugar/CoreLibraryInvocationRewriter.java | 81 +++++++ .../build/android/desugar/CoreLibraryRewriter.java | 6 +- .../build/android/desugar/CoreLibrarySupport.java | 152 ++++++++++++++ .../build/android/desugar/CorePackageRenamer.java | 54 +++++ .../devtools/build/android/desugar/Desugar.java | 72 ++++++- .../build/android/desugar/InterfaceDesugaring.java | 3 +- .../android/desugar/CoreLibrarySupportTest.java | 232 +++++++++++++++++++++ .../android/desugar/CorePackageRenamerTest.java | 90 ++++++++ 8 files changed, 681 insertions(+), 9 deletions(-) create mode 100644 java/com/google/devtools/build/android/desugar/CoreLibraryInvocationRewriter.java create mode 100644 java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java create mode 100644 java/com/google/devtools/build/android/desugar/CorePackageRenamer.java create mode 100644 test/java/com/google/devtools/build/android/desugar/CoreLibrarySupportTest.java create mode 100644 test/java/com/google/devtools/build/android/desugar/CorePackageRenamerTest.java diff --git a/java/com/google/devtools/build/android/desugar/CoreLibraryInvocationRewriter.java b/java/com/google/devtools/build/android/desugar/CoreLibraryInvocationRewriter.java new file mode 100644 index 0000000..417248b --- /dev/null +++ b/java/com/google/devtools/build/android/desugar/CoreLibraryInvocationRewriter.java @@ -0,0 +1,81 @@ +// Copyright 2018 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package com.google.devtools.build.android.desugar; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; + +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +/** + * Rewriter of default and static interface methods defined in some core libraries. + * + *

This is conceptually similar to call site rewriting in {@link InterfaceDesugaring} but here + * we're doing it for certain bootclasspath methods and in particular for invokeinterface and + * invokevirtual, which are ignored in regular {@link InterfaceDesugaring}. + */ +public class CoreLibraryInvocationRewriter extends ClassVisitor { + + private final CoreLibrarySupport support; + + public CoreLibraryInvocationRewriter(ClassVisitor cv, CoreLibrarySupport support) { + super(Opcodes.ASM6, cv); + this.support = support; + } + + @Override + public MethodVisitor visitMethod( + int access, String name, String desc, String signature, String[] exceptions) { + MethodVisitor result = super.visitMethod(access, name, desc, signature, exceptions); + return result != null ? new CoreLibraryMethodInvocationRewriter(result) : null; + } + + private class CoreLibraryMethodInvocationRewriter extends MethodVisitor { + public CoreLibraryMethodInvocationRewriter(MethodVisitor mv) { + super(Opcodes.ASM6, mv); + } + + @Override + public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) { + Class coreInterface = + support.getEmulatedCoreLibraryInvocationTarget(opcode, owner, name, desc, itf); + if (coreInterface != null) { + String coreInterfaceName = coreInterface.getName().replace('.', '/'); + name = + InterfaceDesugaring.normalizeInterfaceMethodName( + name, name.startsWith("lambda$"), opcode == Opcodes.INVOKESTATIC); + if (opcode == Opcodes.INVOKESTATIC) { + checkState(owner.equals(coreInterfaceName)); + } else { + desc = + InterfaceDesugaring.companionDefaultMethodDescriptor( + opcode == Opcodes.INVOKESPECIAL ? owner : coreInterfaceName, desc); + } + + if (opcode == Opcodes.INVOKESTATIC || opcode == Opcodes.INVOKESPECIAL) { + checkArgument(itf, "Expected interface to rewrite %s.%s : %s", owner, name, desc); + owner = InterfaceDesugaring.getCompanionClassName(owner); + } else { + // TODO(kmb): Simulate dynamic dispatch instead of calling most general default method + owner = InterfaceDesugaring.getCompanionClassName(coreInterfaceName); + } + opcode = Opcodes.INVOKESTATIC; + itf = false; + } + super.visitMethodInsn(opcode, owner, name, desc, itf); + } + } +} diff --git a/java/com/google/devtools/build/android/desugar/CoreLibraryRewriter.java b/java/com/google/devtools/build/android/desugar/CoreLibraryRewriter.java index 7f1591b..456fdb5 100644 --- a/java/com/google/devtools/build/android/desugar/CoreLibraryRewriter.java +++ b/java/com/google/devtools/build/android/desugar/CoreLibraryRewriter.java @@ -85,6 +85,10 @@ class CoreLibraryRewriter { return false; } + public String getPrefix() { + return prefix; + } + /** Removes prefix from class names */ public String unprefix(String typeName) { if (prefix.isEmpty() || !typeName.startsWith(prefix)) { @@ -159,7 +163,7 @@ class CoreLibraryRewriter { if (!prefix.isEmpty()) { this.cv = new ClassRemapper( - this.cv, + this.writer, new Remapper() { @Override public String map(String typeName) { diff --git a/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java b/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java new file mode 100644 index 0000000..56e5f18 --- /dev/null +++ b/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java @@ -0,0 +1,152 @@ +// Copyright 2018 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package com.google.devtools.build.android.desugar; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.common.collect.ImmutableList; +import java.lang.reflect.Method; +import java.util.LinkedHashSet; +import java.util.Objects; +import java.util.Set; +import javax.annotation.Nullable; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; + +/** + * Helper that keeps track of which core library classes and methods we want to rewrite. + */ +class CoreLibrarySupport { + + private final CoreLibraryRewriter rewriter; + private final ClassLoader targetLoader; + /** Internal name prefixes that we want to move to a custom package. */ + private final ImmutableList renamedPrefixes; + /** Internal names of interfaces whose default and static interface methods we'll emulate. */ + private final ImmutableList> emulatedInterfaces; + + public CoreLibrarySupport(CoreLibraryRewriter rewriter, ClassLoader targetLoader, + ImmutableList renamedPrefixes, ImmutableList emulatedInterfaces) + throws ClassNotFoundException { + this.rewriter = rewriter; + this.targetLoader = targetLoader; + checkArgument( + renamedPrefixes.stream().allMatch(prefix -> prefix.startsWith("java/")), renamedPrefixes); + this.renamedPrefixes = renamedPrefixes; + ImmutableList.Builder> classBuilder = ImmutableList.builder(); + for (String itf : emulatedInterfaces) { + checkArgument(itf.startsWith("java/util/"), itf); + Class clazz = targetLoader.loadClass((rewriter.getPrefix() + itf).replace('/', '.')); + checkArgument(clazz.isInterface(), itf); + classBuilder.add(clazz); + } + this.emulatedInterfaces = classBuilder.build(); + } + + public boolean isRenamedCoreLibrary(String internalName) { + String unprefixedName = rewriter.unprefix(internalName); + return renamedPrefixes.stream().anyMatch(prefix -> unprefixedName.startsWith(prefix)); + } + + public String renameCoreLibrary(String internalName) { + internalName = rewriter.unprefix(internalName); + return (internalName.startsWith("java/")) + ? "j$/" + internalName.substring(/* cut away "java/" prefix */ 5) + : internalName; + } + + public boolean isEmulatedCoreLibraryInvocation( + int opcode, String owner, String name, String desc, boolean itf) { + return getEmulatedCoreLibraryInvocationTarget(opcode, owner, name, desc, itf) != null; + } + + @Nullable + public Class getEmulatedCoreLibraryInvocationTarget( + int opcode, String owner, String name, String desc, boolean itf) { + if (owner.contains("$$Lambda$") || owner.endsWith("$$CC")) { + return null; // regular desugaring handles invocations on generated classes, no emulation + } + Class clazz = getEmulatedCoreClassOrInterface(owner); + if (clazz == null) { + return null; + } + + if (itf && opcode == Opcodes.INVOKESTATIC) { + return clazz; // static interface method + } + + Method callee = findInterfaceMethod(clazz, name, desc); + if (callee != null && callee.isDefault()) { + return callee.getDeclaringClass(); + } + return null; + } + + private Class getEmulatedCoreClassOrInterface(String internalName) { + { + String unprefixedOwner = rewriter.unprefix(internalName); + if (!unprefixedOwner.startsWith("java/util/") || isRenamedCoreLibrary(unprefixedOwner)) { + return null; + } + } + + Class clazz; + try { + clazz = targetLoader.loadClass(internalName.replace('/', '.')); + } catch (ClassNotFoundException e) { + throw (NoClassDefFoundError) new NoClassDefFoundError().initCause(e); + } + + if (emulatedInterfaces.stream().anyMatch(itf -> itf.isAssignableFrom(clazz))) { + return clazz; + } + return null; + } + + private static Method findInterfaceMethod(Class clazz, String name, String desc) { + return collectImplementedInterfaces(clazz, new LinkedHashSet<>()) + .stream() + // search more subtypes before supertypes + .sorted(DefaultMethodClassFixer.InterfaceComparator.INSTANCE) + .map(itf -> findMethod(itf, name, desc)) + .filter(Objects::nonNull) + .findFirst() + .orElse((Method) null); + } + + + private static Method findMethod(Class clazz, String name, String desc) { + for (Method m : clazz.getMethods()) { + if (m.getName().equals(name) && Type.getMethodDescriptor(m).equals(desc)) { + return m; + } + } + return null; + } + + private static Set> collectImplementedInterfaces(Class clazz, Set> dest) { + if (clazz.isInterface()) { + if (!dest.add(clazz)) { + return dest; + } + } else if (clazz.getSuperclass() != null) { + collectImplementedInterfaces(clazz.getSuperclass(), dest); + } + + for (Class itf : clazz.getInterfaces()) { + collectImplementedInterfaces(itf, dest); + } + return dest; + } +} diff --git a/java/com/google/devtools/build/android/desugar/CorePackageRenamer.java b/java/com/google/devtools/build/android/desugar/CorePackageRenamer.java new file mode 100644 index 0000000..3d58ef6 --- /dev/null +++ b/java/com/google/devtools/build/android/desugar/CorePackageRenamer.java @@ -0,0 +1,54 @@ +// Copyright 2018 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package com.google.devtools.build.android.desugar; + +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.Handle; +import org.objectweb.asm.commons.ClassRemapper; +import org.objectweb.asm.commons.Remapper; + +/** + * A visitor that renames some type names and only when the owner is also renamed. + */ +class CorePackageRenamer extends ClassRemapper { + + public CorePackageRenamer(ClassVisitor cv, CoreLibrarySupport support) { + super(cv, new CorePackageRemapper(support)); + } + + private static final class CorePackageRemapper extends Remapper { + private final CoreLibrarySupport support; + + private CorePackageRemapper(CoreLibrarySupport support) { + this.support = support; + } + + public boolean isRenamed(String owner) { + return support.isRenamedCoreLibrary(owner); + } + + @Override + public String map(String typeName) { + return isRenamed(typeName) ? support.renameCoreLibrary(typeName) : typeName; + } + + @Override + public Object mapValue(Object value) { + if (value instanceof Handle && !isRenamed(((Handle) value).getOwner())) { + return value; + } + return super.mapValue(value); + } + } +} diff --git a/java/com/google/devtools/build/android/desugar/Desugar.java b/java/com/google/devtools/build/android/desugar/Desugar.java index 5d3df4a..c86b406 100644 --- a/java/com/google/devtools/build/android/desugar/Desugar.java +++ b/java/com/google/devtools/build/android/desugar/Desugar.java @@ -248,6 +248,28 @@ class Desugar { ) public boolean coreLibrary; + /** Type prefixes that we'll move to a custom package. */ + @Option( + name = "rewrite_core_library_prefix", + defaultValue = "", // ignored + allowMultiple = true, + documentationCategory = OptionDocumentationCategory.UNDOCUMENTED, + effectTags = {OptionEffectTag.UNKNOWN}, + help = "Assume the given java.* prefixes are desugared." + ) + public List rewriteCoreLibraryPrefixes; + + /** Interfaces whose default and static interface methods we'll emulate. */ + @Option( + name = "emulate_core_library_interface", + defaultValue = "", // ignored + allowMultiple = true, + documentationCategory = OptionDocumentationCategory.UNDOCUMENTED, + effectTags = {OptionEffectTag.UNKNOWN}, + help = "Assume the given java.* interfaces are emulated." + ) + public List emulateCoreLibraryInterfaces; + /** Set to work around b/62623509 with JaCoCo versions prior to 0.7.9. */ // TODO(kmb): Remove when Android Studio doesn't need it anymore (see b/37116789) @Option( @@ -265,7 +287,7 @@ class Desugar { private final DesugarOptions options; private final CoreLibraryRewriter rewriter; private final LambdaClassMaker lambdas; - private final GeneratedClassStore store; + private final GeneratedClassStore store = new GeneratedClassStore(); private final Set visitedExceptionTypes = new HashSet<>(); /** The counter to record the times of try-with-resources desugaring is invoked. */ private final AtomicInteger numOfTryWithResourcesInvoked = new AtomicInteger(); @@ -282,7 +304,6 @@ class Desugar { this.options = options; this.rewriter = new CoreLibraryRewriter(options.coreLibrary ? "__desugar__/" : ""); this.lambdas = new LambdaClassMaker(dumpDirectory); - this.store = new GeneratedClassStore(); this.outputJava7 = options.minSdkVersion < 24; this.allowDefaultMethods = options.desugarInterfaceMethodBodiesIfNeeded || options.minSdkVersion >= 24; @@ -358,6 +379,16 @@ class Desugar { ImmutableSet.Builder interfaceLambdaMethodCollector = ImmutableSet.builder(); ClassVsInterface interfaceCache = new ClassVsInterface(classpathReader); + CoreLibrarySupport coreLibrarySupport = + options.rewriteCoreLibraryPrefixes.isEmpty() + && options.emulateCoreLibraryInterfaces.isEmpty() + ? null + : new CoreLibrarySupport( + rewriter, + loader, + ImmutableList.copyOf(options.rewriteCoreLibraryPrefixes), + ImmutableList.copyOf(options.emulateCoreLibraryInterfaces)); + desugarClassesInInput( inputFiles, outputFileProvider, @@ -365,6 +396,7 @@ class Desugar { classpathReader, depsCollector, bootclasspathReader, + coreLibrarySupport, interfaceCache, interfaceLambdaMethodCollector); @@ -374,11 +406,12 @@ class Desugar { classpathReader, depsCollector, bootclasspathReader, + coreLibrarySupport, interfaceCache, interfaceLambdaMethodCollector.build(), bridgeMethodReader); - desugarAndWriteGeneratedClasses(outputFileProvider, bootclasspathReader); + desugarAndWriteGeneratedClasses(outputFileProvider, bootclasspathReader, coreLibrarySupport); copyThrowableExtensionClass(outputFileProvider); byte[] depsInfo = depsCollector.toByteArray(); @@ -445,6 +478,7 @@ class Desugar { @Nullable ClassReaderFactory classpathReader, DependencyCollector depsCollector, ClassReaderFactory bootclasspathReader, + @Nullable CoreLibrarySupport coreLibrarySupport, ClassVsInterface interfaceCache, ImmutableSet.Builder interfaceLambdaMethodCollector) throws IOException { @@ -466,6 +500,7 @@ class Desugar { classpathReader, depsCollector, bootclasspathReader, + coreLibrarySupport, interfaceCache, interfaceLambdaMethodCollector, writer, @@ -494,6 +529,7 @@ class Desugar { @Nullable ClassReaderFactory classpathReader, DependencyCollector depsCollector, ClassReaderFactory bootclasspathReader, + @Nullable CoreLibrarySupport coreLibrarySupport, ClassVsInterface interfaceCache, ImmutableSet interfaceLambdaMethods, @Nullable ClassReaderFactory bridgeMethodReader) @@ -525,6 +561,7 @@ class Desugar { classpathReader, depsCollector, bootclasspathReader, + coreLibrarySupport, interfaceCache, interfaceLambdaMethods, bridgeMethodReader, @@ -540,7 +577,9 @@ class Desugar { } private void desugarAndWriteGeneratedClasses( - OutputFileProvider outputFileProvider, ClassReaderFactory bootclasspathReader) + OutputFileProvider outputFileProvider, + ClassReaderFactory bootclasspathReader, + @Nullable CoreLibrarySupport coreLibrarySupport) throws IOException { // Write out any classes we generated along the way ImmutableMap generatedClasses = store.drain(); @@ -552,8 +591,13 @@ class Desugar { UnprefixingClassWriter writer = rewriter.writer(ClassWriter.COMPUTE_MAXS); // checkState above implies that we want Java 7 .class files, so send through that visitor. // Don't need a ClassReaderFactory b/c static interface methods should've been moved. - ClassVisitor visitor = - new Java7Compatibility(writer, (ClassReaderFactory) null, bootclasspathReader); + ClassVisitor visitor = writer; + if (coreLibrarySupport != null) { + visitor = new CorePackageRenamer(visitor, coreLibrarySupport); + visitor = new CoreLibraryInvocationRewriter(visitor, coreLibrarySupport); + } + + visitor = new Java7Compatibility(visitor, (ClassReaderFactory) null, bootclasspathReader); generated.getValue().accept(visitor); String filename = rewriter.unprefix(generated.getKey()) + ".class"; outputFileProvider.write(filename, writer.toByteArray()); @@ -569,6 +613,7 @@ class Desugar { @Nullable ClassReaderFactory classpathReader, DependencyCollector depsCollector, ClassReaderFactory bootclasspathReader, + @Nullable CoreLibrarySupport coreLibrarySupport, ClassVsInterface interfaceCache, ImmutableSet interfaceLambdaMethods, @Nullable ClassReaderFactory bridgeMethodReader, @@ -576,6 +621,12 @@ class Desugar { UnprefixingClassWriter writer, ClassReader input) { ClassVisitor visitor = checkNotNull(writer); + + if (coreLibrarySupport != null) { + visitor = new CorePackageRenamer(visitor, coreLibrarySupport); + visitor = new CoreLibraryInvocationRewriter(visitor, coreLibrarySupport); + } + if (!allowTryWithResources) { CloseResourceMethodScanner closeResourceMethodScanner = new CloseResourceMethodScanner(); input.accept(closeResourceMethodScanner, ClassReader.SKIP_DEBUG); @@ -612,6 +663,7 @@ class Desugar { options.legacyJacocoFix); } } + visitor = new LambdaClassFixer( visitor, @@ -639,11 +691,18 @@ class Desugar { @Nullable ClassReaderFactory classpathReader, DependencyCollector depsCollector, ClassReaderFactory bootclasspathReader, + @Nullable CoreLibrarySupport coreLibrarySupport, ClassVsInterface interfaceCache, ImmutableSet.Builder interfaceLambdaMethodCollector, UnprefixingClassWriter writer, ClassReader input) { ClassVisitor visitor = checkNotNull(writer); + + if (coreLibrarySupport != null) { + visitor = new CorePackageRenamer(visitor, coreLibrarySupport); + visitor = new CoreLibraryInvocationRewriter(visitor, coreLibrarySupport); + } + if (!allowTryWithResources) { CloseResourceMethodScanner closeResourceMethodScanner = new CloseResourceMethodScanner(); input.accept(closeResourceMethodScanner, ClassReader.SKIP_DEBUG); @@ -678,6 +737,7 @@ class Desugar { options.legacyJacocoFix); } } + // LambdaDesugaring is relatively expensive, so check first whether we need it. Additionally, // we need to collect lambda methods referenced by invokedynamic instructions up-front anyway. // TODO(kmb): Scan constant pool instead of visiting the class to find bootstrap methods etc. diff --git a/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java b/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java index b8b3ead..3524fae 100644 --- a/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java +++ b/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java @@ -265,8 +265,7 @@ class InterfaceDesugaring extends ClassVisitor { return "".equals(methodName); } - private static String normalizeInterfaceMethodName( - String name, boolean isLambda, boolean isStatic) { + static String normalizeInterfaceMethodName(String name, boolean isLambda, boolean isStatic) { if (isLambda) { // Rename lambda method to reflect the new owner. Not doing so confuses LambdaDesugaring // if it's run over this class again. LambdaDesugaring has already renamed the method from diff --git a/test/java/com/google/devtools/build/android/desugar/CoreLibrarySupportTest.java b/test/java/com/google/devtools/build/android/desugar/CoreLibrarySupportTest.java new file mode 100644 index 0000000..d7fcad4 --- /dev/null +++ b/test/java/com/google/devtools/build/android/desugar/CoreLibrarySupportTest.java @@ -0,0 +1,232 @@ +// Copyright 2018 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package com.google.devtools.build.android.desugar; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.collect.ImmutableList; +import java.util.Collection; +import java.util.Comparator; +import java.util.Map; +import java.util.concurrent.ConcurrentMap; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.objectweb.asm.Opcodes; + +@RunWith(JUnit4.class) +public class CoreLibrarySupportTest { + + @Test + public void testIsRenamedCoreLibrary() throws Exception { + CoreLibrarySupport support = + new CoreLibrarySupport( + new CoreLibraryRewriter(""), null, ImmutableList.of("java/time/"), ImmutableList.of()); + assertThat(support.isRenamedCoreLibrary("java/time/X")).isTrue(); + assertThat(support.isRenamedCoreLibrary("java/time/y/X")).isTrue(); + assertThat(support.isRenamedCoreLibrary("java/io/X")).isFalse(); + assertThat(support.isRenamedCoreLibrary("com/google/X")).isFalse(); + } + + @Test + public void testIsRenamedCoreLibrary_prefixedLoader() throws Exception { + CoreLibrarySupport support = + new CoreLibrarySupport( + new CoreLibraryRewriter("__/"), + null, + ImmutableList.of("java/time/"), + ImmutableList.of()); + assertThat(support.isRenamedCoreLibrary("__/java/time/X")).isTrue(); + assertThat(support.isRenamedCoreLibrary("__/java/time/y/X")).isTrue(); + assertThat(support.isRenamedCoreLibrary("__/java/io/X")).isFalse(); + assertThat(support.isRenamedCoreLibrary("com/google/X")).isFalse(); + } + @Test + public void testRenameCoreLibrary() throws Exception { + CoreLibrarySupport support = + new CoreLibrarySupport( + new CoreLibraryRewriter(""), null, ImmutableList.of(), ImmutableList.of()); + assertThat(support.renameCoreLibrary("java/time/X")).isEqualTo("j$/time/X"); + assertThat(support.renameCoreLibrary("com/google/X")).isEqualTo("com/google/X"); + } + + @Test + public void testRenameCoreLibrary_prefixedLoader() throws Exception { + CoreLibrarySupport support = + new CoreLibrarySupport( + new CoreLibraryRewriter("__/"), null, ImmutableList.of(), ImmutableList.of()); + assertThat(support.renameCoreLibrary("__/java/time/X")).isEqualTo("j$/time/X"); + assertThat(support.renameCoreLibrary("com/google/X")).isEqualTo("com/google/X"); + } + + @Test + public void testIsEmulatedCoreLibraryInvocation() throws Exception { + CoreLibrarySupport support = + new CoreLibrarySupport( + new CoreLibraryRewriter(""), + Thread.currentThread().getContextClassLoader(), + ImmutableList.of(), + ImmutableList.of("java/util/Collection")); + assertThat( + support.isEmulatedCoreLibraryInvocation( + Opcodes.INVOKEINTERFACE, + "java/util/Collection", + "removeIf", + "(Ljava/util/function/Predicate;)Z", + true)) + .isTrue(); // true for default method + assertThat( + support.isEmulatedCoreLibraryInvocation( + Opcodes.INVOKEINTERFACE, "java/util/Collection", "size", "()I", true)) + .isFalse(); // false for abstract method + } + + @Test + public void testGetEmulatedCoreLibraryInvocationTarget_defaultMethod() throws Exception { + CoreLibrarySupport support = + new CoreLibrarySupport( + new CoreLibraryRewriter(""), + Thread.currentThread().getContextClassLoader(), + ImmutableList.of(), + ImmutableList.of("java/util/Collection")); + assertThat( + support.getEmulatedCoreLibraryInvocationTarget( + Opcodes.INVOKEINTERFACE, + "java/util/Collection", + "removeIf", + "(Ljava/util/function/Predicate;)Z", + true)) + .isEqualTo(Collection.class); + assertThat( + support.getEmulatedCoreLibraryInvocationTarget( + Opcodes.INVOKEVIRTUAL, + "java/util/ArrayList", + "removeIf", + "(Ljava/util/function/Predicate;)Z", + false)) + .isEqualTo(Collection.class); + assertThat( + support.getEmulatedCoreLibraryInvocationTarget( + Opcodes.INVOKEINTERFACE, + "com/google/common/collect/ImmutableList", + "removeIf", + "(Ljava/util/function/Predicate;)Z", + true)) + .isNull(); + } + + @Test + public void testGetEmulatedCoreLibraryInvocationTarget_abstractMethod() throws Exception { + CoreLibrarySupport support = + new CoreLibrarySupport( + new CoreLibraryRewriter(""), + Thread.currentThread().getContextClassLoader(), + ImmutableList.of(), + ImmutableList.of("java/util/Collection")); + assertThat( + support.getEmulatedCoreLibraryInvocationTarget( + Opcodes.INVOKEINTERFACE, + "java/util/Collection", + "size", + "()I", + true)) + .isNull(); + assertThat( + support.getEmulatedCoreLibraryInvocationTarget( + Opcodes.INVOKEVIRTUAL, + "java/util/ArrayList", + "size", + "()I", + false)) + .isNull(); + } + + @Test + public void testGetEmulatedCoreLibraryInvocationTarget_defaultOverride() throws Exception { + CoreLibrarySupport support = + new CoreLibrarySupport( + new CoreLibraryRewriter(""), + Thread.currentThread().getContextClassLoader(), + ImmutableList.of(), + ImmutableList.of("java/util/Map")); + assertThat( + support.getEmulatedCoreLibraryInvocationTarget( + Opcodes.INVOKEINTERFACE, + "java/util/Map", + "putIfAbsent", + "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", + true)) + .isEqualTo(Map.class); + assertThat( + support.getEmulatedCoreLibraryInvocationTarget( + Opcodes.INVOKEINTERFACE, + "java/util/concurrent/ConcurrentMap", + "putIfAbsent", + "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", + true)) + .isNull(); // putIfAbsent is default in Map but abstract in ConcurrentMap + assertThat( + support.getEmulatedCoreLibraryInvocationTarget( + Opcodes.INVOKEINTERFACE, + "java/util/concurrent/ConcurrentMap", + "getOrDefault", + "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", + true)) + .isEqualTo(ConcurrentMap.class); // conversely, getOrDefault is overridden as default method + } + + @Test + public void testGetEmulatedCoreLibraryInvocationTarget_staticInterfaceMethod() throws Exception { + CoreLibrarySupport support = + new CoreLibrarySupport( + new CoreLibraryRewriter(""), + Thread.currentThread().getContextClassLoader(), + ImmutableList.of(), + ImmutableList.of("java/util/Comparator")); + assertThat( + support.getEmulatedCoreLibraryInvocationTarget( + Opcodes.INVOKESTATIC, + "java/util/Comparator", + "reverseOrder", + "()Ljava/util/Comparator;", + true)) + .isEqualTo(Comparator.class); + } + + @Test + public void testGetEmulatedCoreLibraryInvocationTarget_ignoreRenamed() throws Exception { + CoreLibrarySupport support = + new CoreLibrarySupport( + new CoreLibraryRewriter(""), + Thread.currentThread().getContextClassLoader(), + ImmutableList.of("java/util/concurrent/"), // should return null for these + ImmutableList.of("java/util/Map")); + assertThat( + support.getEmulatedCoreLibraryInvocationTarget( + Opcodes.INVOKEINTERFACE, + "java/util/Map", + "getOrDefault", + "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", + true)) + .isEqualTo(Map.class); + assertThat( + support.getEmulatedCoreLibraryInvocationTarget( + Opcodes.INVOKEINTERFACE, + "java/util/concurrent/ConcurrentMap", + "getOrDefault", + "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", + true)) + .isNull(); + } +} diff --git a/test/java/com/google/devtools/build/android/desugar/CorePackageRenamerTest.java b/test/java/com/google/devtools/build/android/desugar/CorePackageRenamerTest.java new file mode 100644 index 0000000..0626b46 --- /dev/null +++ b/test/java/com/google/devtools/build/android/desugar/CorePackageRenamerTest.java @@ -0,0 +1,90 @@ +// Copyright 2018 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package com.google.devtools.build.android.desugar; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.collect.ImmutableList; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +@RunWith(JUnit4.class) +public class CorePackageRenamerTest { + + @Test + public void testSymbolRewrite() throws Exception { + MockClassVisitor out = new MockClassVisitor(); + CorePackageRenamer renamer = new CorePackageRenamer( + out, + new CoreLibrarySupport( + new CoreLibraryRewriter(""), null, ImmutableList.of("java/time/"), ImmutableList.of())); + MethodVisitor mv = renamer.visitMethod(0, "test", "()V", null, null); + + mv.visitMethodInsn( + Opcodes.INVOKESTATIC, "java/time/Instant", "now", "()Ljava/time/Instant;", false); + assertThat(out.mv.owner).isEqualTo("j$/time/Instant"); + assertThat(out.mv.desc).isEqualTo("()Lj$/time/Instant;"); + + mv.visitMethodInsn( + Opcodes.INVOKESTATIC, "other/time/Instant", "now", "()Ljava/time/Instant;", false); + assertThat(out.mv.owner).isEqualTo("other/time/Instant"); + assertThat(out.mv.desc).isEqualTo("()Lj$/time/Instant;"); + + mv.visitFieldInsn( + Opcodes.GETFIELD, "other/time/Instant", "now", "Ljava/time/Instant;"); + assertThat(out.mv.owner).isEqualTo("other/time/Instant"); + assertThat(out.mv.desc).isEqualTo("Lj$/time/Instant;"); + } + + private static class MockClassVisitor extends ClassVisitor { + + final MockMethodVisitor mv = new MockMethodVisitor(); + + public MockClassVisitor() { + super(Opcodes.ASM6); + } + + @Override + public MethodVisitor visitMethod( + int access, String name, String desc, String signature, String[] exceptions) { + return mv; + } + } + + private static class MockMethodVisitor extends MethodVisitor { + + String owner; + String desc; + + public MockMethodVisitor() { + super(Opcodes.ASM6); + } + + @Override + public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) { + this.owner = owner; + this.desc = desc; + } + + @Override + public void visitFieldInsn(int opcode, String owner, String name, String desc) { + this.owner = owner; + this.desc = desc; + } + } +} -- cgit v1.2.3 From 40fa0d9a56e8b29219f37cdc8743b63c7b65ab55 Mon Sep 17 00:00:00 2001 From: kmb Date: Tue, 6 Feb 2018 13:43:30 -0800 Subject: drop debug info when loading classes in desugar as a workaround for https://bugs.openjdk.java.net/browse/JDK-8066981 RELNOTES: None. PiperOrigin-RevId: 184732576 GitOrigin-RevId: e85e280645f579ffd5511a41553e95713c80177d Change-Id: Ic2e2372810c649b0376183b011441e70f08d57d1 --- .../build/android/desugar/HeaderClassLoader.java | 3 ++- .../build/android/desugar/b72690624_testdata.jar | Bin 0 -> 3088 bytes 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 test/java/com/google/devtools/build/android/desugar/b72690624_testdata.jar diff --git a/java/com/google/devtools/build/android/desugar/HeaderClassLoader.java b/java/com/google/devtools/build/android/desugar/HeaderClassLoader.java index 0a757bf..77d99bb 100644 --- a/java/com/google/devtools/build/android/desugar/HeaderClassLoader.java +++ b/java/com/google/devtools/build/android/desugar/HeaderClassLoader.java @@ -58,7 +58,8 @@ class HeaderClassLoader extends ClassLoader { // Have ASM compute maxs so we don't need to figure out how many formal parameters there are ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS); ImmutableList interfaceFieldNames = getFieldsIfReaderIsInterface(reader); - reader.accept(new CodeStubber(writer, interfaceFieldNames), 0); + // TODO(kmb): Consider SKIP_CODE and stubbing everything so class loader doesn't verify code + reader.accept(new CodeStubber(writer, interfaceFieldNames), ClassReader.SKIP_DEBUG); bytecode = writer.toByteArray(); } catch (IOException e) { throw new IOError(e); diff --git a/test/java/com/google/devtools/build/android/desugar/b72690624_testdata.jar b/test/java/com/google/devtools/build/android/desugar/b72690624_testdata.jar new file mode 100644 index 0000000..6cca3a0 Binary files /dev/null and b/test/java/com/google/devtools/build/android/desugar/b72690624_testdata.jar differ -- cgit v1.2.3 From c69bee050bf45cc49fc500dd4a495181cb23a645 Mon Sep 17 00:00:00 2001 From: kmb Date: Wed, 7 Feb 2018 11:35:54 -0800 Subject: Reflect renamed classes in desugar output file names RELNOTES: None. PiperOrigin-RevId: 184869773 GitOrigin-RevId: 005affa263e01afecf913a18edf830670f09c5f3 Change-Id: Ic36dfcf021efdcc29540791af52fa9f19054c671 --- .../build/android/desugar/CoreLibraryRewriter.java | 21 +++++++++++++++ .../devtools/build/android/desugar/Desugar.java | 31 ++++++++++++++-------- 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/java/com/google/devtools/build/android/desugar/CoreLibraryRewriter.java b/java/com/google/devtools/build/android/desugar/CoreLibraryRewriter.java index 456fdb5..698fc53 100644 --- a/java/com/google/devtools/build/android/desugar/CoreLibraryRewriter.java +++ b/java/com/google/devtools/build/android/desugar/CoreLibraryRewriter.java @@ -15,6 +15,7 @@ package com.google.devtools.build.android.desugar; import java.io.IOException; import java.io.InputStream; +import javax.annotation.Nullable; import org.objectweb.asm.Attribute; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassVisitor; @@ -156,6 +157,8 @@ class CoreLibraryRewriter { public class UnprefixingClassWriter extends ClassVisitor { private final ClassWriter writer; + private String finalClassName; + UnprefixingClassWriter(int flags) { super(Opcodes.ASM6); this.writer = new ClassWriter(flags); @@ -173,8 +176,26 @@ class CoreLibraryRewriter { } } + /** Returns the (unprefixed) name of the class once written. */ + @Nullable + String getClassName() { + return finalClassName; + } + byte[] toByteArray() { return writer.toByteArray(); } + + @Override + public void visit( + int version, + int access, + String name, + String signature, + String superName, + String[] interfaces) { + finalClassName = unprefix(name); + super.visit(version, access, name, signature, superName, interfaces); + } } } diff --git a/java/com/google/devtools/build/android/desugar/Desugar.java b/java/com/google/devtools/build/android/desugar/Desugar.java index c86b406..ab7a336 100644 --- a/java/com/google/devtools/build/android/desugar/Desugar.java +++ b/java/com/google/devtools/build/android/desugar/Desugar.java @@ -482,16 +482,16 @@ class Desugar { ClassVsInterface interfaceCache, ImmutableSet.Builder interfaceLambdaMethodCollector) throws IOException { - for (String filename : inputFiles) { - if (OutputFileProvider.DESUGAR_DEPS_FILENAME.equals(filename)) { + for (String inputFilename : inputFiles) { + if (OutputFileProvider.DESUGAR_DEPS_FILENAME.equals(inputFilename)) { // TODO(kmb): rule out that this happens or merge input file with what's in depsCollector continue; // skip as we're writing a new file like this at the end or don't want it } - try (InputStream content = inputFiles.getInputStream(filename)) { + try (InputStream content = inputFiles.getInputStream(inputFilename)) { // We can write classes uncompressed since they need to be converted to .dex format // for Android anyways. Resources are written as they were in the input jar to avoid // any danger of accidentally uncompressed resources ending up in an .apk. - if (filename.endsWith(".class")) { + if (inputFilename.endsWith(".class")) { ClassReader reader = rewriter.reader(content); UnprefixingClassWriter writer = rewriter.writer(ClassWriter.COMPUTE_MAXS); ClassVisitor visitor = @@ -507,13 +507,17 @@ class Desugar { reader); if (writer == visitor) { // Just copy the input if there are no rewritings - outputFileProvider.write(filename, reader.b); + outputFileProvider.write(inputFilename, reader.b); } else { reader.accept(visitor, 0); + String filename = writer.getClassName() + ".class"; + checkState( + (options.coreLibrary && coreLibrarySupport != null) + || filename.equals(inputFilename)); outputFileProvider.write(filename, writer.toByteArray()); } } else { - outputFileProvider.copyFrom(filename, inputFiles); + outputFileProvider.copyFrom(inputFilename, inputFiles); } } } @@ -569,9 +573,12 @@ class Desugar { writer, reader); reader.accept(visitor, 0); - String filename = - rewriter.unprefix(lambdaClass.getValue().desiredInternalName()) + ".class"; - outputFileProvider.write(filename, writer.toByteArray()); + checkState( + (options.coreLibrary && coreLibrarySupport != null) + || rewriter + .unprefix(lambdaClass.getValue().desiredInternalName()) + .equals(writer.getClassName())); + outputFileProvider.write(writer.getClassName() + ".class", writer.toByteArray()); } } } @@ -599,8 +606,10 @@ class Desugar { visitor = new Java7Compatibility(visitor, (ClassReaderFactory) null, bootclasspathReader); generated.getValue().accept(visitor); - String filename = rewriter.unprefix(generated.getKey()) + ".class"; - outputFileProvider.write(filename, writer.toByteArray()); + checkState( + (options.coreLibrary && coreLibrarySupport != null) + || rewriter.unprefix(generated.getKey()).equals(writer.getClassName())); + outputFileProvider.write(writer.getClassName() + ".class", writer.toByteArray()); } } -- cgit v1.2.3 From 0633971679f2f1e15eb2bb2c2326e2a0e26033c6 Mon Sep 17 00:00:00 2001 From: kmb Date: Wed, 7 Feb 2018 16:27:17 -0800 Subject: Rename generated core classes during core library desugaring RELNOTES: None. PiperOrigin-RevId: 184915177 GitOrigin-RevId: 154317e1269b1925722754291a8c7181ccd005f6 Change-Id: I2974e07e3154ec481579cb191c48bc2f8d0af06f --- .../devtools/build/android/desugar/CoreLibrarySupport.java | 9 ++++++++- .../devtools/build/android/desugar/CoreLibrarySupportTest.java | 4 ++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java b/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java index 56e5f18..c6fd0b4 100644 --- a/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java +++ b/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java @@ -56,7 +56,14 @@ class CoreLibrarySupport { public boolean isRenamedCoreLibrary(String internalName) { String unprefixedName = rewriter.unprefix(internalName); - return renamedPrefixes.stream().anyMatch(prefix -> unprefixedName.startsWith(prefix)); + if (!unprefixedName.startsWith("java/")) { + return false; // shortcut + } + // Rename any classes desugar might generate under java/ (for emulated interfaces) as well as + // configured prefixes + return unprefixedName.contains("$$Lambda$") + || unprefixedName.endsWith("$$CC") + || renamedPrefixes.stream().anyMatch(prefix -> unprefixedName.startsWith(prefix)); } public String renameCoreLibrary(String internalName) { diff --git a/test/java/com/google/devtools/build/android/desugar/CoreLibrarySupportTest.java b/test/java/com/google/devtools/build/android/desugar/CoreLibrarySupportTest.java index d7fcad4..089e231 100644 --- a/test/java/com/google/devtools/build/android/desugar/CoreLibrarySupportTest.java +++ b/test/java/com/google/devtools/build/android/desugar/CoreLibrarySupportTest.java @@ -36,6 +36,8 @@ public class CoreLibrarySupportTest { assertThat(support.isRenamedCoreLibrary("java/time/X")).isTrue(); assertThat(support.isRenamedCoreLibrary("java/time/y/X")).isTrue(); assertThat(support.isRenamedCoreLibrary("java/io/X")).isFalse(); + assertThat(support.isRenamedCoreLibrary("java/io/X$$CC")).isTrue(); + assertThat(support.isRenamedCoreLibrary("java/io/X$$Lambda$17")).isTrue(); assertThat(support.isRenamedCoreLibrary("com/google/X")).isFalse(); } @@ -50,6 +52,8 @@ public class CoreLibrarySupportTest { assertThat(support.isRenamedCoreLibrary("__/java/time/X")).isTrue(); assertThat(support.isRenamedCoreLibrary("__/java/time/y/X")).isTrue(); assertThat(support.isRenamedCoreLibrary("__/java/io/X")).isFalse(); + assertThat(support.isRenamedCoreLibrary("__/java/io/X$$CC")).isTrue(); + assertThat(support.isRenamedCoreLibrary("__/java/io/X$$Lambda$17")).isTrue(); assertThat(support.isRenamedCoreLibrary("com/google/X")).isFalse(); } @Test -- cgit v1.2.3 From dffdca8bcbb17f5150a8a3856871055cfe1a17ce Mon Sep 17 00:00:00 2001 From: cnsun Date: Thu, 8 Feb 2018 11:46:47 -0800 Subject: Refactor the command line argument parser to use the latest API. RELNOTES:none PiperOrigin-RevId: 185027580 GitOrigin-RevId: 5ac4d7ad1ef9685b04aa58d4dfa15a38a42573d8 Change-Id: Idb27e1c1be02a5b8e0e9702fabeb9366424826ef --- .../devtools/build/android/desugar/Desugar.java | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/java/com/google/devtools/build/android/desugar/Desugar.java b/java/com/google/devtools/build/android/desugar/Desugar.java index ab7a336..4b35575 100644 --- a/java/com/google/devtools/build/android/desugar/Desugar.java +++ b/java/com/google/devtools/build/android/desugar/Desugar.java @@ -17,7 +17,6 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import static com.google.devtools.build.android.desugar.LambdaClassMaker.LAMBDA_METAFACTORY_DUMPER_PROPERTY; -import static java.nio.charset.StandardCharsets.ISO_8859_1; import com.google.auto.value.AutoValue; import com.google.common.annotations.VisibleForTesting; @@ -32,13 +31,15 @@ import com.google.devtools.build.android.desugar.CoreLibraryRewriter.Unprefixing import com.google.devtools.common.options.Option; import com.google.devtools.common.options.OptionDocumentationCategory; import com.google.devtools.common.options.OptionEffectTag; -import com.google.devtools.common.options.Options; import com.google.devtools.common.options.OptionsBase; +import com.google.devtools.common.options.OptionsParser; +import com.google.devtools.common.options.ShellQuotedParamsFilePreProcessor; import com.google.errorprone.annotations.MustBeClosed; import java.io.IOError; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Field; +import java.nio.file.FileSystems; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; @@ -427,7 +428,7 @@ class Desugar { } /** - * Returns a dependency collector for use with a single input Jar. If + * Returns a dependency collector for use with a single input Jar. If * {@link DesugarOptions#emitDependencyMetadata} is set, this method instantiates the collector * reflectively to allow compiling and using the desugar tool without this mechanism. */ @@ -799,7 +800,7 @@ class Desugar { } catch (ReflectiveOperationException e) { // We do not want to crash Desugar, if we cannot load or access these classes or fields. // We aim to provide better diagnostics. If we cannot, just let it go. - e.printStackTrace(); + e.printStackTrace(System.err); // To silence error-prone's complaint. } } @@ -830,12 +831,11 @@ class Desugar { } private static DesugarOptions parseCommandLineOptions(String[] args) throws IOException { - if (args.length == 1 && args[0].startsWith("@")) { - args = Files.readAllLines(Paths.get(args[0].substring(1)), ISO_8859_1).toArray(new String[0]); - } - DesugarOptions options = - Options.parseAndExitUponError(DesugarOptions.class, /*allowResidue=*/ false, args) - .getOptions(); + OptionsParser parser = OptionsParser.newOptionsParser(DesugarOptions.class); + parser.setAllowResidue(false); + parser.enableParamsFileSupport(new ShellQuotedParamsFilePreProcessor(FileSystems.getDefault())); + parser.parseAndExitUponError(args); + DesugarOptions options = parser.getOptions(DesugarOptions.class); checkArgument(!options.inputJars.isEmpty(), "--input is required"); checkArgument( -- cgit v1.2.3 From 17d008dc0cc602c46135f4a4f6f55a6d93431d77 Mon Sep 17 00:00:00 2001 From: kmb Date: Thu, 8 Feb 2018 18:11:29 -0800 Subject: Stub default methods as needed for core library desugaring RELNOTES: None PiperOrigin-RevId: 185082719 GitOrigin-RevId: aa79fd483daff0db9be274c33de109257f8a6804 Change-Id: I90cad779653c93f9917f69fe06daad2bbf919f65 --- .../build/android/desugar/CoreLibrarySupport.java | 15 ++- .../android/desugar/DefaultMethodClassFixer.java | 130 ++++++++++++++------- .../devtools/build/android/desugar/Desugar.java | 14 ++- .../android/desugar/CoreLibrarySupportTest.java | 17 +++ .../desugar/DefaultMethodClassFixerTest.java | 1 + 5 files changed, 133 insertions(+), 44 deletions(-) diff --git a/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java b/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java index c6fd0b4..2437a19 100644 --- a/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java +++ b/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java @@ -73,6 +73,14 @@ class CoreLibrarySupport { : internalName; } + /** + * Returns {@code true} for java.* classes or interfaces that are subtypes of emulated interfaces. + * Note that implies that this method always returns {@code false} for user-written classes. + */ + public boolean isEmulatedCoreClassOrInterface(String internalName) { + return getEmulatedCoreClassOrInterface(internalName) != null; + } + public boolean isEmulatedCoreLibraryInvocation( int opcode, String owner, String name, String desc, boolean itf) { return getEmulatedCoreLibraryInvocationTarget(opcode, owner, name, desc, itf) != null; @@ -81,9 +89,6 @@ class CoreLibrarySupport { @Nullable public Class getEmulatedCoreLibraryInvocationTarget( int opcode, String owner, String name, String desc, boolean itf) { - if (owner.contains("$$Lambda$") || owner.endsWith("$$CC")) { - return null; // regular desugaring handles invocations on generated classes, no emulation - } Class clazz = getEmulatedCoreClassOrInterface(owner); if (clazz == null) { return null; @@ -101,6 +106,10 @@ class CoreLibrarySupport { } private Class getEmulatedCoreClassOrInterface(String internalName) { + if (internalName.contains("$$Lambda$") || internalName.endsWith("$$CC")) { + // Regular desugaring handles generated classes, no emulation is needed + return null; + } { String unprefixedOwner = rewriter.unprefix(internalName); if (!unprefixedOwner.startsWith("java/util/") || isRenamedCoreLibrary(unprefixedOwner)) { diff --git a/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java b/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java index 1aaf0b6..2eda141 100644 --- a/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java +++ b/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java @@ -23,6 +23,7 @@ import java.util.HashSet; import java.util.Iterator; import java.util.Set; import java.util.TreeSet; +import javax.annotation.Nullable; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.MethodVisitor; @@ -44,6 +45,7 @@ public class DefaultMethodClassFixer extends ClassVisitor { private final ClassReaderFactory bootclasspath; private final ClassLoader targetLoader; private final DependencyCollector depsCollector; + @Nullable private final CoreLibrarySupport coreLibrarySupport; private final HashSet instanceMethods = new HashSet<>(); private boolean isInterface; @@ -57,10 +59,12 @@ public class DefaultMethodClassFixer extends ClassVisitor { ClassVisitor dest, ClassReaderFactory classpath, DependencyCollector depsCollector, + @Nullable CoreLibrarySupport coreLibrarySupport, ClassReaderFactory bootclasspath, ClassLoader targetLoader) { super(Opcodes.ASM6, dest); this.classpath = classpath; + this.coreLibrarySupport = coreLibrarySupport; this.bootclasspath = bootclasspath; this.targetLoader = targetLoader; this.depsCollector = depsCollector; @@ -88,7 +92,9 @@ public class DefaultMethodClassFixer extends ClassVisitor { @Override public void visitEnd() { - if (!isInterface && defaultMethodsDefined(directInterfaces)) { + if (!isInterface + && (mayNeedInterfaceStubsForEmulatedSuperclass() + || defaultMethodsDefined(directInterfaces))) { // Inherited methods take precedence over default methods, so visit all superclasses and // figure out what methods they declare before stubbing in any missing default methods. recordInheritedMethods(); @@ -190,6 +196,12 @@ public class DefaultMethodClassFixer extends ClassVisitor { return super.visitMethod(access, name, desc, signature, exceptions); } + private boolean mayNeedInterfaceStubsForEmulatedSuperclass() { + return coreLibrarySupport != null + && !coreLibrarySupport.isEmulatedCoreClassOrInterface(internalName) + && coreLibrarySupport.isEmulatedCoreClassOrInterface(superName); + } + private void stubMissingDefaultAndBridgeMethods() { TreeSet> allInterfaces = new TreeSet<>(InterfaceComparator.INSTANCE); for (String direct : directInterfaces) { @@ -203,19 +215,51 @@ public class DefaultMethodClassFixer extends ClassVisitor { } Class superclass = loadFromInternal(superName); + boolean mayNeedStubsForSuperclass = mayNeedInterfaceStubsForEmulatedSuperclass(); + if (mayNeedStubsForSuperclass) { + // Collect interfaces inherited from emulated superclasses as well, to handle things like + // extending AbstractList without explicitly implementing List. + for (Class clazz = superclass; clazz != null; clazz = clazz.getSuperclass()) { + for (Class itf : superclass.getInterfaces()) { + collectInterfaces(itf, allInterfaces); + } + } + } for (Class interfaceToVisit : allInterfaces) { // if J extends I, J is allowed to redefine I's default methods. The comparator we used // above makes sure we visit J before I in that case so we can use J's definition. - if (superclass != null && interfaceToVisit.isAssignableFrom(superclass)) { - // superclass already implements this interface, so we must skip it. The superclass will - // be similarly rewritten or comes from the bootclasspath; either way we don't need to and - // shouldn't stub default methods for this interface. + if (!mayNeedStubsForSuperclass && interfaceToVisit.isAssignableFrom(superclass)) { + // superclass is also rewritten and already implements this interface, so we _must_ skip it. continue; } stubMissingDefaultAndBridgeMethods(interfaceToVisit.getName().replace('.', '/')); } } + private void stubMissingDefaultAndBridgeMethods(String implemented) { + ClassReader bytecode; + boolean isBootclasspath; + if (bootclasspath.isKnown(implemented)) { + if (coreLibrarySupport != null + && (coreLibrarySupport.isRenamedCoreLibrary(implemented) + || coreLibrarySupport.isEmulatedCoreClassOrInterface(implemented))) { + bytecode = checkNotNull(bootclasspath.readIfKnown(implemented), implemented); + isBootclasspath = true; + } else { + // Default methods from interfaces on the bootclasspath that we're not renaming or emulating + // are assumed available at runtime, so just ignore them. + return; + } + } else { + bytecode = + checkNotNull( + classpath.readIfKnown(implemented), + "Couldn't find interface %s implemented by %s", implemented, internalName); + isBootclasspath = false; + } + bytecode.accept(new DefaultMethodStubber(isBootclasspath), ClassReader.SKIP_DEBUG); + } + private Class loadFromInternal(String internalName) { try { return targetLoader.loadClass(internalName.replace('/', '.')); @@ -313,26 +357,38 @@ public class DefaultMethodClassFixer extends ClassVisitor { */ private boolean defaultMethodsDefined(ImmutableList interfaces) { for (String implemented : interfaces) { + ClassReader bytecode; if (bootclasspath.isKnown(implemented)) { - continue; - } - ClassReader bytecode = classpath.readIfKnown(implemented); - if (bytecode == null) { - // Interface isn't on the classpath, which indicates incomplete classpaths. Record missing - // dependency so we can check it later. If we don't check then we may get runtime failures - // or wrong behavior from default methods that should've been stubbed in. - // TODO(kmb): Print a warning so people can start fixing their deps? - depsCollector.missingImplementedInterface(internalName, implemented); + if (coreLibrarySupport != null + && coreLibrarySupport.isEmulatedCoreClassOrInterface(implemented)) { + return true; // need to stub in emulated interface methods such as Collection.stream() + } else if (coreLibrarySupport != null + && coreLibrarySupport.isRenamedCoreLibrary(implemented)) { + // Check default methods of renamed interfaces + bytecode = checkNotNull(bootclasspath.readIfKnown(implemented), implemented); + } else { + continue; + } } else { - // Class in classpath and bootclasspath is a bad idea but in any event, assume the - // bootclasspath will take precedence like in a classloader. - // We can skip code attributes as we just need to find default methods to stub. - DefaultMethodFinder finder = new DefaultMethodFinder(); - bytecode.accept(finder, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG); - if (finder.foundDefaultMethods()) { - return true; + bytecode = classpath.readIfKnown(implemented); + if (bytecode == null) { + // Interface isn't on the classpath, which indicates incomplete classpaths. Record missing + // dependency so we can check it later. If we don't check then we may get runtime + // failures or wrong behavior from default methods that should've been stubbed in. + // TODO(kmb): Print a warning so people can start fixing their deps? + depsCollector.missingImplementedInterface(internalName, implemented); + continue; } } + + // Class in classpath and bootclasspath is a bad idea but in any event, assume the + // bootclasspath will take precedence like in a classloader. + // We can skip code attributes as we just need to find default methods to stub. + DefaultMethodFinder finder = new DefaultMethodFinder(); + bytecode.accept(finder, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG); + if (finder.foundDefaultMethods()) { + return true; + } } return false; } @@ -365,30 +421,18 @@ public class DefaultMethodClassFixer extends ClassVisitor { && !instanceMethods.contains(name + ":" + desc); } - private void stubMissingDefaultAndBridgeMethods(String implemented) { - if (bootclasspath.isKnown(implemented)) { - // Default methods on the bootclasspath will be available at runtime, so just ignore them. - return; - } - ClassReader bytecode = - checkNotNull( - classpath.readIfKnown(implemented), - "Couldn't find interface %s implemented by %s", - implemented, - internalName); - bytecode.accept(new DefaultMethodStubber(), ClassReader.SKIP_DEBUG); - } - /** * Visitor for interfaces that produces delegates in the class visited by the outer {@link * DefaultMethodClassFixer} for every default method encountered. */ private class DefaultMethodStubber extends ClassVisitor { + private final boolean isBootclasspathInterface; private String stubbedInterfaceName; - public DefaultMethodStubber() { + public DefaultMethodStubber(boolean isBootclasspathInterface) { super(Opcodes.ASM6); + this.isBootclasspathInterface = isBootclasspathInterface; } @Override @@ -413,8 +457,11 @@ public class DefaultMethodClassFixer extends ClassVisitor { // definitions conflict, but see stubMissingDefaultMethods() for how we deal with default // methods redefined in interfaces extending another. recordIfInstanceMethod(access, name, desc); - depsCollector.assumeCompanionClass( - internalName, InterfaceDesugaring.getCompanionClassName(stubbedInterfaceName)); + if (!isBootclasspathInterface) { + // Don't record these dependencies, as we can't check them + depsCollector.assumeCompanionClass( + internalName, InterfaceDesugaring.getCompanionClassName(stubbedInterfaceName)); + } // Add this method to the class we're desugaring and stub in a body to call the default // implementation in the interface's companion class. ijar omits these methods when setting @@ -444,6 +491,10 @@ public class DefaultMethodClassFixer extends ClassVisitor { return null; } else if (shouldStubAsBridgeDefaultMethod(access, name, desc)) { recordIfInstanceMethod(access, name, desc); + // If we're visiting a bootclasspath interface then we most likely don't have the code. + // That means we can't just copy the method bodies as we're trying to do below. + checkState(!isBootclasspathInterface, + "TODO stub bridge methods for core interfaces if ever needed"); // For bridges we just copy their bodies instead of going through the companion class. // Meanwhile, we also need to desugar the copied method bodies, so that any calls to // interface methods are correctly handled. @@ -454,7 +505,7 @@ public class DefaultMethodClassFixer extends ClassVisitor { depsCollector, internalName); } else { - return null; // we don't care about the actual code in these methods + return null; // not a default or bridge method or the class already defines this method. } } } @@ -529,6 +580,7 @@ public class DefaultMethodClassFixer extends ClassVisitor { @Override public MethodVisitor visitMethod( int access, String name, String desc, String signature, String[] exceptions) { + // TODO(kmb): what if this method only exists on some devices, e.g., ArrayList.spliterator? recordIfInstanceMethod(access, name, desc); return null; } diff --git a/java/com/google/devtools/build/android/desugar/Desugar.java b/java/com/google/devtools/build/android/desugar/Desugar.java index 4b35575..109093c 100644 --- a/java/com/google/devtools/build/android/desugar/Desugar.java +++ b/java/com/google/devtools/build/android/desugar/Desugar.java @@ -662,7 +662,12 @@ class Desugar { if (options.desugarInterfaceMethodBodiesIfNeeded) { visitor = new DefaultMethodClassFixer( - visitor, classpathReader, depsCollector, bootclasspathReader, loader); + visitor, + classpathReader, + depsCollector, + coreLibrarySupport, + bootclasspathReader, + loader); visitor = new InterfaceDesugaring( visitor, @@ -736,7 +741,12 @@ class Desugar { if (options.desugarInterfaceMethodBodiesIfNeeded) { visitor = new DefaultMethodClassFixer( - visitor, classpathReader, depsCollector, bootclasspathReader, loader); + visitor, + classpathReader, + depsCollector, + coreLibrarySupport, + bootclasspathReader, + loader); visitor = new InterfaceDesugaring( visitor, diff --git a/test/java/com/google/devtools/build/android/desugar/CoreLibrarySupportTest.java b/test/java/com/google/devtools/build/android/desugar/CoreLibrarySupportTest.java index 089e231..d52ef78 100644 --- a/test/java/com/google/devtools/build/android/desugar/CoreLibrarySupportTest.java +++ b/test/java/com/google/devtools/build/android/desugar/CoreLibrarySupportTest.java @@ -74,6 +74,23 @@ public class CoreLibrarySupportTest { assertThat(support.renameCoreLibrary("com/google/X")).isEqualTo("com/google/X"); } + @Test + public void testIsEmulatedCoreClassOrInterface() throws Exception { + CoreLibrarySupport support = + new CoreLibrarySupport( + new CoreLibraryRewriter(""), + Thread.currentThread().getContextClassLoader(), + ImmutableList.of("java/util/concurrent/"), + ImmutableList.of("java/util/Map")); + assertThat(support.isEmulatedCoreClassOrInterface("java/util/Map")).isTrue(); + assertThat(support.isEmulatedCoreClassOrInterface("java/util/Map$$Lambda$17")).isFalse(); + assertThat(support.isEmulatedCoreClassOrInterface("java/util/Map$$CC")).isFalse(); + assertThat(support.isEmulatedCoreClassOrInterface("java/util/HashMap")).isTrue(); + assertThat(support.isEmulatedCoreClassOrInterface("java/util/concurrent/ConcurrentMap")) + .isFalse(); // false for renamed prefixes + assertThat(support.isEmulatedCoreClassOrInterface("com/google/Map")).isFalse(); + } + @Test public void testIsEmulatedCoreLibraryInvocation() throws Exception { CoreLibrarySupport support = diff --git a/test/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixerTest.java b/test/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixerTest.java index c74febb..27083db 100644 --- a/test/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixerTest.java +++ b/test/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixerTest.java @@ -102,6 +102,7 @@ public class DefaultMethodClassFixerTest { writer, classpathReader, DependencyCollector.NoWriteCollectors.FAIL_ON_MISSING, + /*coreLibrarySupport=*/ null, bootclassPath, classLoader); reader.accept(fixer, 0); -- cgit v1.2.3 From f84654f4c88fd87a97ecbde885496b0d2db6a1cf Mon Sep 17 00:00:00 2001 From: kmb Date: Fri, 9 Feb 2018 17:57:15 -0800 Subject: Delete erroneous piece of desugar's renaming logic RELNOTES: None. PiperOrigin-RevId: 185218745 GitOrigin-RevId: c3c5d9bc0e52362bf37129099ba3af1b06229501 Change-Id: I0f277a39360f1de651dd81f2af8490cb5ca695a8 --- .../devtools/build/android/desugar/CorePackageRenamer.java | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/java/com/google/devtools/build/android/desugar/CorePackageRenamer.java b/java/com/google/devtools/build/android/desugar/CorePackageRenamer.java index 3d58ef6..5f1dc2e 100644 --- a/java/com/google/devtools/build/android/desugar/CorePackageRenamer.java +++ b/java/com/google/devtools/build/android/desugar/CorePackageRenamer.java @@ -14,12 +14,11 @@ package com.google.devtools.build.android.desugar; import org.objectweb.asm.ClassVisitor; -import org.objectweb.asm.Handle; import org.objectweb.asm.commons.ClassRemapper; import org.objectweb.asm.commons.Remapper; /** - * A visitor that renames some type names and only when the owner is also renamed. + * A visitor that renames packages so configured using {@link CoreLibrarySupport}.. */ class CorePackageRenamer extends ClassRemapper { @@ -42,13 +41,5 @@ class CorePackageRenamer extends ClassRemapper { public String map(String typeName) { return isRenamed(typeName) ? support.renameCoreLibrary(typeName) : typeName; } - - @Override - public Object mapValue(Object value) { - if (value instanceof Handle && !isRenamed(((Handle) value).getOwner())) { - return value; - } - return super.mapValue(value); - } } } -- cgit v1.2.3 From b104f2eaa4f376eb2c3b94ec16a7ef44c1c286b1 Mon Sep 17 00:00:00 2001 From: kmb Date: Sat, 10 Feb 2018 12:16:21 -0800 Subject: Desugar fixes: - make Objects.requireNonNull and Long.compare rewrites compatible with --core_library - apply those and try-with-resources rewrites to generated companion classes RELNOTES: None. PiperOrigin-RevId: 185262256 GitOrigin-RevId: f13a7ef7c9eb7ce400ffbbaca0bdc7945172a332 Change-Id: I07a3e5877bc7de8cdade93a6748d511a7669cafe --- .../devtools/build/android/desugar/Desugar.java | 35 ++++++++++++++++++---- .../android/desugar/LongCompareMethodRewriter.java | 19 +++++++----- .../ObjectsRequireNonNullMethodRewriter.java | 30 ++++++++++--------- 3 files changed, 56 insertions(+), 28 deletions(-) diff --git a/java/com/google/devtools/build/android/desugar/Desugar.java b/java/com/google/devtools/build/android/desugar/Desugar.java index 109093c..053e55e 100644 --- a/java/com/google/devtools/build/android/desugar/Desugar.java +++ b/java/com/google/devtools/build/android/desugar/Desugar.java @@ -412,7 +412,9 @@ class Desugar { interfaceLambdaMethodCollector.build(), bridgeMethodReader); - desugarAndWriteGeneratedClasses(outputFileProvider, bootclasspathReader, coreLibrarySupport); + desugarAndWriteGeneratedClasses( + outputFileProvider, loader, bootclasspathReader, coreLibrarySupport); + copyThrowableExtensionClass(outputFileProvider); byte[] depsInfo = depsCollector.toByteArray(); @@ -586,6 +588,7 @@ class Desugar { private void desugarAndWriteGeneratedClasses( OutputFileProvider outputFileProvider, + ClassLoader loader, ClassReaderFactory bootclasspathReader, @Nullable CoreLibrarySupport coreLibrarySupport) throws IOException { @@ -605,6 +608,26 @@ class Desugar { visitor = new CoreLibraryInvocationRewriter(visitor, coreLibrarySupport); } + if (!allowTryWithResources) { + CloseResourceMethodScanner closeResourceMethodScanner = new CloseResourceMethodScanner(); + generated.getValue().accept(closeResourceMethodScanner); + visitor = + new TryWithResourcesRewriter( + visitor, + loader, + visitedExceptionTypes, + numOfTryWithResourcesInvoked, + closeResourceMethodScanner.hasCloseResourceMethod()); + } + if (!allowCallsToObjectsNonNull) { + // Not sure whether there will be implicit null check emitted by javac, so we rerun + // the inliner again + visitor = new ObjectsRequireNonNullMethodRewriter(visitor, rewriter); + } + if (!allowCallsToLongCompare) { + visitor = new LongCompareMethodRewriter(visitor, rewriter); + } + visitor = new Java7Compatibility(visitor, (ClassReaderFactory) null, bootclasspathReader); generated.getValue().accept(visitor); checkState( @@ -651,10 +674,10 @@ class Desugar { if (!allowCallsToObjectsNonNull) { // Not sure whether there will be implicit null check emitted by javac, so we rerun // the inliner again - visitor = new ObjectsRequireNonNullMethodRewriter(visitor); + visitor = new ObjectsRequireNonNullMethodRewriter(visitor, rewriter); } if (!allowCallsToLongCompare) { - visitor = new LongCompareMethodRewriter(visitor); + visitor = new LongCompareMethodRewriter(visitor, rewriter); } if (outputJava7) { // null ClassReaderFactory b/c we don't expect to need it for lambda classes @@ -730,10 +753,10 @@ class Desugar { closeResourceMethodScanner.hasCloseResourceMethod()); } if (!allowCallsToObjectsNonNull) { - visitor = new ObjectsRequireNonNullMethodRewriter(visitor); + visitor = new ObjectsRequireNonNullMethodRewriter(visitor, rewriter); } if (!allowCallsToLongCompare) { - visitor = new LongCompareMethodRewriter(visitor); + visitor = new LongCompareMethodRewriter(visitor, rewriter); } if (!options.onlyDesugarJavac9ForLint) { if (outputJava7) { @@ -840,7 +863,7 @@ class Desugar { return dumpDirectory; } - private static DesugarOptions parseCommandLineOptions(String[] args) throws IOException { + private static DesugarOptions parseCommandLineOptions(String[] args) { OptionsParser parser = OptionsParser.newOptionsParser(DesugarOptions.class); parser.setAllowResidue(false); parser.enableParamsFileSupport(new ShellQuotedParamsFilePreProcessor(FileSystems.getDefault())); diff --git a/java/com/google/devtools/build/android/desugar/LongCompareMethodRewriter.java b/java/com/google/devtools/build/android/desugar/LongCompareMethodRewriter.java index f66d862..6ac415d 100644 --- a/java/com/google/devtools/build/android/desugar/LongCompareMethodRewriter.java +++ b/java/com/google/devtools/build/android/desugar/LongCompareMethodRewriter.java @@ -26,8 +26,11 @@ import org.objectweb.asm.MethodVisitor; */ public class LongCompareMethodRewriter extends ClassVisitor { - public LongCompareMethodRewriter(ClassVisitor cv) { + private final CoreLibraryRewriter rewriter; + + public LongCompareMethodRewriter(ClassVisitor cv, CoreLibraryRewriter rewriter) { super(ASM6, cv); + this.rewriter = rewriter; } @Override @@ -37,7 +40,7 @@ public class LongCompareMethodRewriter extends ClassVisitor { return visitor == null ? visitor : new LongCompareMethodVisitor(visitor); } - private static class LongCompareMethodVisitor extends MethodVisitor { + private class LongCompareMethodVisitor extends MethodVisitor { public LongCompareMethodVisitor(MethodVisitor visitor) { super(ASM6, visitor); @@ -45,14 +48,14 @@ public class LongCompareMethodRewriter extends ClassVisitor { @Override public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) { - if (opcode != INVOKESTATIC - || !owner.equals("java/lang/Long") - || !name.equals("compare") - || !desc.equals("(JJ)I")) { + if (opcode == INVOKESTATIC + && rewriter.unprefix(owner).equals("java/lang/Long") + && name.equals("compare") + && desc.equals("(JJ)I")) { + super.visitInsn(LCMP); + } else { super.visitMethodInsn(opcode, owner, name, desc, itf); - return; } - super.visitInsn(LCMP); } } } diff --git a/java/com/google/devtools/build/android/desugar/ObjectsRequireNonNullMethodRewriter.java b/java/com/google/devtools/build/android/desugar/ObjectsRequireNonNullMethodRewriter.java index 86465d6..5e0a344 100644 --- a/java/com/google/devtools/build/android/desugar/ObjectsRequireNonNullMethodRewriter.java +++ b/java/com/google/devtools/build/android/desugar/ObjectsRequireNonNullMethodRewriter.java @@ -29,8 +29,11 @@ import org.objectweb.asm.MethodVisitor; */ public class ObjectsRequireNonNullMethodRewriter extends ClassVisitor { - public ObjectsRequireNonNullMethodRewriter(ClassVisitor cv) { + private final CoreLibraryRewriter rewriter; + + public ObjectsRequireNonNullMethodRewriter(ClassVisitor cv, CoreLibraryRewriter rewriter) { super(ASM6, cv); + this.rewriter = rewriter; } @Override @@ -40,7 +43,7 @@ public class ObjectsRequireNonNullMethodRewriter extends ClassVisitor { return visitor == null ? visitor : new ObjectsMethodInlinerMethodVisitor(visitor); } - private static class ObjectsMethodInlinerMethodVisitor extends MethodVisitor { + private class ObjectsMethodInlinerMethodVisitor extends MethodVisitor { public ObjectsMethodInlinerMethodVisitor(MethodVisitor mv) { super(ASM6, mv); @@ -48,20 +51,19 @@ public class ObjectsRequireNonNullMethodRewriter extends ClassVisitor { @Override public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) { - if (opcode != INVOKESTATIC - || !owner.equals("java/util/Objects") - || !name.equals("requireNonNull") - || !desc.equals("(Ljava/lang/Object;)Ljava/lang/Object;")) { + if (opcode == INVOKESTATIC + && rewriter.unprefix(owner).equals("java/util/Objects") + && name.equals("requireNonNull") + && desc.equals("(Ljava/lang/Object;)Ljava/lang/Object;")) { + // a call to Objects.requireNonNull(Object o) + // duplicate the first argument 'o', as this method returns 'o'. + super.visitInsn(DUP); + super.visitMethodInsn( + INVOKEVIRTUAL, "java/lang/Object", "getClass", "()Ljava/lang/Class;", false); + super.visitInsn(POP); + } else { super.visitMethodInsn(opcode, owner, name, desc, itf); - return; } - - // a call to Objects.requireNonNull(Object o) - // duplicate the first argument 'o', as this method returns 'o'. - super.visitInsn(DUP); - super.visitMethodInsn( - INVOKEVIRTUAL, "java/lang/Object", "getClass", "()Ljava/lang/Class;", false); - super.visitInsn(POP); } } } -- cgit v1.2.3 From 25d5064e7f8cea5babb248b709348077a4f376bd Mon Sep 17 00:00:00 2001 From: kmb Date: Thu, 15 Feb 2018 10:43:41 -0800 Subject: Resolve the owner of interface.super calls to inherited default methods for android desugaring RELNOTES: None. PiperOrigin-RevId: 185863194 GitOrigin-RevId: c8e8749adc7b98c272b2421569dc97a88d487771 Change-Id: I063c2caa4b38fff2f9111f9fc09c317a5b097834 --- .../android/desugar/DefaultMethodClassFixer.java | 1 + .../devtools/build/android/desugar/Desugar.java | 2 + .../build/android/desugar/InterfaceDesugaring.java | 42 ++++++++++++++++++++- .../desugar/DesugarJava8FunctionalTest.java | 8 ++++ .../java8/InterfaceWithInheritedMethods.java | 44 ++++++++++++++++++++++ .../testdata_desugared_java8_jar_toc_golden.txt | 3 ++ 6 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 test/java/com/google/devtools/build/android/desugar/testdata/java8/InterfaceWithInheritedMethods.java diff --git a/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java b/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java index 2eda141..52964b7 100644 --- a/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java +++ b/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java @@ -502,6 +502,7 @@ public class DefaultMethodClassFixer extends ClassVisitor { DefaultMethodClassFixer.this.visitMethod(access, name, desc, (String) null, exceptions), stubbedInterfaceName, bootclasspath, + targetLoader, depsCollector, internalName); } else { diff --git a/java/com/google/devtools/build/android/desugar/Desugar.java b/java/com/google/devtools/build/android/desugar/Desugar.java index 053e55e..58b9b88 100644 --- a/java/com/google/devtools/build/android/desugar/Desugar.java +++ b/java/com/google/devtools/build/android/desugar/Desugar.java @@ -697,6 +697,7 @@ class Desugar { interfaceCache, depsCollector, bootclasspathReader, + loader, store, options.legacyJacocoFix); } @@ -776,6 +777,7 @@ class Desugar { interfaceCache, depsCollector, bootclasspathReader, + loader, store, options.legacyJacocoFix); } diff --git a/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java b/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java index 3524fae..f17f114 100644 --- a/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java +++ b/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java @@ -17,6 +17,7 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; +import java.lang.reflect.Method; import javax.annotation.Nullable; import org.objectweb.asm.AnnotationVisitor; import org.objectweb.asm.ClassVisitor; @@ -47,6 +48,7 @@ class InterfaceDesugaring extends ClassVisitor { private final ClassVsInterface interfaceCache; private final DependencyCollector depsCollector; private final ClassReaderFactory bootclasspath; + private final ClassLoader targetLoader; private final GeneratedClassStore store; private final boolean legacyJaCoCo; @@ -62,12 +64,14 @@ class InterfaceDesugaring extends ClassVisitor { ClassVsInterface interfaceCache, DependencyCollector depsCollector, ClassReaderFactory bootclasspath, + ClassLoader targetLoader, GeneratedClassStore store, boolean legacyJaCoCo) { super(Opcodes.ASM6, dest); this.interfaceCache = interfaceCache; this.depsCollector = depsCollector; this.bootclasspath = bootclasspath; + this.targetLoader = targetLoader; this.store = store; this.legacyJaCoCo = legacyJaCoCo; } @@ -234,7 +238,12 @@ class InterfaceDesugaring extends ClassVisitor { } return result != null ? new InterfaceInvocationRewriter( - result, isInterface() ? internalName : null, bootclasspath, depsCollector, codeOwner) + result, + isInterface() ? internalName : null, + bootclasspath, + targetLoader, + depsCollector, + codeOwner) : null; } @@ -354,6 +363,7 @@ class InterfaceDesugaring extends ClassVisitor { @Nullable private final String interfaceName; private final ClassReaderFactory bootclasspath; + private final ClassLoader targetLoader; private final DependencyCollector depsCollector; /** Internal name that'll be used to record any dependencies on interface methods. */ private final String declaringClass; @@ -362,11 +372,13 @@ class InterfaceDesugaring extends ClassVisitor { MethodVisitor dest, @Nullable String knownInterfaceName, ClassReaderFactory bootclasspath, + ClassLoader targetLoader, DependencyCollector depsCollector, String declaringClass) { super(Opcodes.ASM6, dest); this.interfaceName = knownInterfaceName; this.bootclasspath = bootclasspath; + this.targetLoader = targetLoader; this.depsCollector = depsCollector; this.declaringClass = declaringClass; } @@ -409,7 +421,16 @@ class InterfaceDesugaring extends ClassVisitor { checkArgument(!owner.endsWith(DependencyCollector.INTERFACE_COMPANION_SUFFIX), "shouldn't consider %s an interface", owner); if (opcode == Opcodes.INVOKESPECIAL) { - // Turn Interface.super.m() into Interface$$CC.m(receiver) + // Turn Interface.super.m() into DefiningInterface$$CC.m(receiver). Note that owner + // always refers to the current type's immediate super-interface, but the default method + // may be inherited by that interface, so we have to figure out where the method is + // defined and invoke it in the corresponding companion class (b/73355452). Note that + // we're always dealing with interfaces here, and all interface methods are public, + // so using Class.getMethods should suffice to find inherited methods. Also note this + // can only be a default method invocation, no abstract method invocation. + owner = + findDefaultMethod(owner, name, desc) + .getDeclaringClass().getName().replace('.', '/'); opcode = Opcodes.INVOKESTATIC; desc = companionDefaultMethodDescriptor(owner, desc); } @@ -421,6 +442,23 @@ class InterfaceDesugaring extends ClassVisitor { } super.visitMethodInsn(opcode, owner, name, desc, itf); } + + private Method findDefaultMethod(String owner, String name, String desc) { + try { + Class clazz = targetLoader.loadClass(owner.replace('/', '.')); + // otherwise getting public methods with getMethods() below isn't enough + checkArgument(clazz.isInterface(), "Not an interface: %s", owner); + for (Method m : clazz.getMethods()) { + if (m.getName().equals(name) && Type.getMethodDescriptor(m).equals(desc)) { + checkState(m.isDefault(), "Found non-default method: %s", m); + return m; + } + } + } catch (ClassNotFoundException e) { + throw new IllegalStateException("Couldn't load " + owner, e); + } + throw new IllegalArgumentException("Method not found: " + owner + "." + name + desc); + } } /** diff --git a/test/java/com/google/devtools/build/android/desugar/DesugarJava8FunctionalTest.java b/test/java/com/google/devtools/build/android/desugar/DesugarJava8FunctionalTest.java index 20e6028..75d4f43 100644 --- a/test/java/com/google/devtools/build/android/desugar/DesugarJava8FunctionalTest.java +++ b/test/java/com/google/devtools/build/android/desugar/DesugarJava8FunctionalTest.java @@ -32,6 +32,7 @@ import com.google.devtools.build.android.desugar.testdata.java8.GenericDefaultIn import com.google.devtools.build.android.desugar.testdata.java8.InterfaceMethod; import com.google.devtools.build.android.desugar.testdata.java8.InterfaceWithDefaultMethod; import com.google.devtools.build.android.desugar.testdata.java8.InterfaceWithDuplicateMethods.ClassWithDuplicateMethods; +import com.google.devtools.build.android.desugar.testdata.java8.InterfaceWithInheritedMethods; import com.google.devtools.build.android.desugar.testdata.java8.Java7InterfaceWithBridges; import com.google.devtools.build.android.desugar.testdata.java8.Named; import com.google.devtools.build.android.desugar.testdata.java8.TwoInheritedDefaultMethods; @@ -415,4 +416,11 @@ public class DesugarJava8FunctionalTest extends DesugarFunctionalTest { assertThat(new DefaultMethodTransitivelyFromSeparateJava8Target().dflt()).isEqualTo("dflt"); assertThat(new DefaultMethodFromSeparateJava8TargetOverridden().dflt()).isEqualTo("override"); } + + /** Regression test for b/73355452 */ + @Test + public void testSuperCallToInheritedDefaultMethod() { + assertThat(new InterfaceWithInheritedMethods.Impl().name()).isEqualTo("Base"); + assertThat(new InterfaceWithInheritedMethods.Impl().suffix()).isEqualTo("!"); + } } diff --git a/test/java/com/google/devtools/build/android/desugar/testdata/java8/InterfaceWithInheritedMethods.java b/test/java/com/google/devtools/build/android/desugar/testdata/java8/InterfaceWithInheritedMethods.java new file mode 100644 index 0000000..8656e26 --- /dev/null +++ b/test/java/com/google/devtools/build/android/desugar/testdata/java8/InterfaceWithInheritedMethods.java @@ -0,0 +1,44 @@ +// Copyright 2018 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package com.google.devtools.build.android.desugar.testdata.java8; + +/** Regression test data for b/73355452 that also includes calling static methods. */ +public interface InterfaceWithInheritedMethods { + default String name() { + return "Base"; + } + + static String staticSuffix() { + return "!"; + } + + static interface Passthrough extends InterfaceWithInheritedMethods { + // inherits name(). Note that desugar doesn't produce a companion class for this interface + // since it doesn't define any default or static interface methods itself. + } + + static class Impl implements Passthrough { + @Override + public String name() { + // Even though Passthrough itself doesn't define name(), bytecode refers to Passthrough.name. + return Passthrough.super.name(); + } + + public String suffix() { + // Note that Passthrough.defaultSuffix doesn't compile and bytecode refers to + // InterfaceWithInheritedMethods.staticSuffix, so this shouldn't cause issues like b/73355452 + return staticSuffix(); + } + } +} diff --git a/test/java/com/google/devtools/build/android/desugar/testdata_desugared_java8_jar_toc_golden.txt b/test/java/com/google/devtools/build/android/desugar/testdata_desugared_java8_jar_toc_golden.txt index 67a46c3..16439ae 100644 --- a/test/java/com/google/devtools/build/android/desugar/testdata_desugared_java8_jar_toc_golden.txt +++ b/test/java/com/google/devtools/build/android/desugar/testdata_desugared_java8_jar_toc_golden.txt @@ -82,6 +82,9 @@ com/google/devtools/build/android/desugar/testdata/java8/InterfaceWithDefaultMet com/google/devtools/build/android/desugar/testdata/java8/InterfaceWithDefaultMethod.class com/google/devtools/build/android/desugar/testdata/java8/InterfaceWithDuplicateMethods$ClassWithDuplicateMethods.class com/google/devtools/build/android/desugar/testdata/java8/InterfaceWithDuplicateMethods.class +com/google/devtools/build/android/desugar/testdata/java8/InterfaceWithInheritedMethods$Impl.class +com/google/devtools/build/android/desugar/testdata/java8/InterfaceWithInheritedMethods$Passthrough.class +com/google/devtools/build/android/desugar/testdata/java8/InterfaceWithInheritedMethods.class com/google/devtools/build/android/desugar/testdata/java8/Java7InterfaceWithBridges$AbstractClassOne.class com/google/devtools/build/android/desugar/testdata/java8/Java7InterfaceWithBridges$ClassAddOne.class com/google/devtools/build/android/desugar/testdata/java8/Java7InterfaceWithBridges$ClassAddTwo.class -- cgit v1.2.3 From 3a8eb0fd7142741300dc19805da37734617a2bb2 Mon Sep 17 00:00:00 2001 From: corysmith Date: Fri, 16 Feb 2018 13:14:29 -0800 Subject: Normalized the serialization proto to save space and allow greater versatility in storage. RELNOTES: None PiperOrigin-RevId: 186036607 GitOrigin-RevId: f672a31b8b19baab95373e4f2f6d110aa8b8f0fb Change-Id: I71aa7e424993ec32007389c78e1b4ae061787f56 --- java/com/google/devtools/build/android/Converters.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/java/com/google/devtools/build/android/Converters.java b/java/com/google/devtools/build/android/Converters.java index e58dd2d..75c5ead 100644 --- a/java/com/google/devtools/build/android/Converters.java +++ b/java/com/google/devtools/build/android/Converters.java @@ -218,7 +218,7 @@ public final class Converters { @Override public List convert(String input) throws OptionsParsingException { if (input.isEmpty()) { - return ImmutableList.of(); + return ImmutableList.of(); } try { ImmutableList.Builder builder = ImmutableList.builder(); @@ -473,7 +473,7 @@ public final class Converters { @Override public List convert(String input) throws OptionsParsingException { - final Builder builder = ImmutableList.builder(); + final Builder builder = ImmutableList.builder(); for (String path : SPLITTER.splitToList(input)) { builder.add(libraryConverter.convert(path)); } -- cgit v1.2.3 From f6818a14b59efd7e23b454669e4123d9c9ca4b7d Mon Sep 17 00:00:00 2001 From: corysmith Date: Fri, 16 Feb 2018 15:48:49 -0800 Subject: Automated rollback of commit f672a31b8b19baab95373e4f2f6d110aa8b8f0fb. *** Reason for rollback *** Unclassified general breakages in tests. Rolling back for further investigation. *** Original change description *** Normalized the serialization proto to save space and allow greater versatility in storage. RELNOTES: None PiperOrigin-RevId: 186057879 GitOrigin-RevId: d18d3e2f83f9d582858a3edab7a450c60044028c Change-Id: I0d722e4139074466d491b4c8ffb75c6777010f51 --- java/com/google/devtools/build/android/Converters.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/java/com/google/devtools/build/android/Converters.java b/java/com/google/devtools/build/android/Converters.java index 75c5ead..e58dd2d 100644 --- a/java/com/google/devtools/build/android/Converters.java +++ b/java/com/google/devtools/build/android/Converters.java @@ -218,7 +218,7 @@ public final class Converters { @Override public List convert(String input) throws OptionsParsingException { if (input.isEmpty()) { - return ImmutableList.of(); + return ImmutableList.of(); } try { ImmutableList.Builder builder = ImmutableList.builder(); @@ -473,7 +473,7 @@ public final class Converters { @Override public List convert(String input) throws OptionsParsingException { - final Builder builder = ImmutableList.builder(); + final Builder builder = ImmutableList.builder(); for (String path : SPLITTER.splitToList(input)) { builder.add(libraryConverter.convert(path)); } -- cgit v1.2.3 From cfff73917d13d5629fc6d247921e4db46fb841c8 Mon Sep 17 00:00:00 2001 From: kmb Date: Tue, 20 Feb 2018 15:30:22 -0800 Subject: Tool that scans a given Jar for references to select classes and outputs corresponding Proguard-style -keep rules RELNOTES: None. PiperOrigin-RevId: 186372769 GitOrigin-RevId: c1042f2adc55d040495a1159100146fad607d32a Change-Id: I8c3509e9d48145cc90faa143016c3f2cb0d23c27 --- .../build/android/desugar/scan/KeepReference.java | 51 +++ .../build/android/desugar/scan/KeepScanner.java | 174 +++++++++ .../desugar/scan/PrefixReferenceScanner.java | 405 +++++++++++++++++++++ .../android/desugar/scan/test_keep_scanner.sh | 27 ++ .../scan/testdata/CollectionReferences.java | 60 +++ .../testdata/OverlappingCollectionReferences.java | 49 +++ .../build/android/desugar/scan/testdata_golden.txt | 46 +++ 7 files changed, 812 insertions(+) create mode 100644 java/com/google/devtools/build/android/desugar/scan/KeepReference.java create mode 100644 java/com/google/devtools/build/android/desugar/scan/KeepScanner.java create mode 100644 java/com/google/devtools/build/android/desugar/scan/PrefixReferenceScanner.java create mode 100755 test/java/com/google/devtools/build/android/desugar/scan/test_keep_scanner.sh create mode 100644 test/java/com/google/devtools/build/android/desugar/scan/testdata/CollectionReferences.java create mode 100644 test/java/com/google/devtools/build/android/desugar/scan/testdata/OverlappingCollectionReferences.java create mode 100644 test/java/com/google/devtools/build/android/desugar/scan/testdata_golden.txt diff --git a/java/com/google/devtools/build/android/desugar/scan/KeepReference.java b/java/com/google/devtools/build/android/desugar/scan/KeepReference.java new file mode 100644 index 0000000..bae3f38 --- /dev/null +++ b/java/com/google/devtools/build/android/desugar/scan/KeepReference.java @@ -0,0 +1,51 @@ +// Copyright 2018 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package com.google.devtools.build.android.desugar.scan; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.auto.value.AutoValue; +import com.google.errorprone.annotations.Immutable; + +@AutoValue +@Immutable +abstract class KeepReference { + public static KeepReference classReference(String internalName) { + checkArgument(!internalName.isEmpty()); + return new AutoValue_KeepReference(internalName, "", ""); + } + + public static KeepReference memberReference(String internalName, String name, String desc) { + checkArgument(!internalName.isEmpty()); + checkArgument(!name.isEmpty()); + checkArgument(!desc.isEmpty()); + return new AutoValue_KeepReference(internalName, name, desc); + } + + public final boolean isMemberReference() { + return !name().isEmpty(); + } + + public final boolean isMethodReference() { + return desc().startsWith("("); + } + + public final boolean isFieldReference() { + return isMemberReference() && !isMethodReference(); + } + + public abstract String internalName(); + public abstract String name(); + public abstract String desc(); +} diff --git a/java/com/google/devtools/build/android/desugar/scan/KeepScanner.java b/java/com/google/devtools/build/android/desugar/scan/KeepScanner.java new file mode 100644 index 0000000..5892bf5 --- /dev/null +++ b/java/com/google/devtools/build/android/desugar/scan/KeepScanner.java @@ -0,0 +1,174 @@ +// Copyright 2018 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package com.google.devtools.build.android.desugar.scan; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static java.nio.file.StandardOpenOption.CREATE; +import static java.util.Comparator.comparing; + +import com.google.common.collect.ImmutableSet; +import com.google.devtools.build.android.Converters.ExistingPathConverter; +import com.google.devtools.build.android.Converters.PathConverter; +import com.google.devtools.common.options.Option; +import com.google.devtools.common.options.OptionDocumentationCategory; +import com.google.devtools.common.options.OptionEffectTag; +import com.google.devtools.common.options.OptionsBase; +import com.google.devtools.common.options.OptionsParser; +import com.google.devtools.common.options.ShellQuotedParamsFilePreProcessor; +import java.io.IOError; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintStream; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.Type; + +class KeepScanner { + + public static class KeepScannerOptions extends OptionsBase { + @Option( + name = "input", + defaultValue = "null", + documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, + effectTags = OptionEffectTag.UNKNOWN, + converter = ExistingPathConverter.class, + abbrev = 'i', + help = "Input Jar with classes to scan." + ) + public Path inputJars; + + @Option( + name = "keep_file", + defaultValue = "null", + documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, + effectTags = OptionEffectTag.UNKNOWN, + converter = PathConverter.class, + help = "Where to write keep rules to." + ) + public Path keepDest; + + @Option( + name = "prefix", + defaultValue = "j$/", + documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, + effectTags = OptionEffectTag.UNKNOWN, + help = "type to scan for." + ) + public String prefix; + } + + public static void main(String... args) throws Exception { + OptionsParser parser = OptionsParser.newOptionsParser(KeepScannerOptions.class); + parser.setAllowResidue(false); + parser.enableParamsFileSupport(new ShellQuotedParamsFilePreProcessor(FileSystems.getDefault())); + parser.parseAndExitUponError(args); + + KeepScannerOptions options = parser.getOptions(KeepScannerOptions.class); + Map> seeds = + scan(checkNotNull(options.inputJars), options.prefix); + + try (PrintStream out = + new PrintStream( + Files.newOutputStream(options.keepDest, CREATE), /*autoFlush=*/ false, "UTF-8")) { + writeKeepDirectives(out, seeds); + } + } + + /** + * Writes a -keep rule for each class listing any members to keep. We sort classes and members + * so the output is deterministic. + */ + private static void writeKeepDirectives( + PrintStream out, Map> seeds) { + seeds + .entrySet() + .stream() + .sorted(comparing(Map.Entry::getKey)) + .forEachOrdered( + type -> { + out.printf("-keep class %s {%n", type.getKey().replace('/', '.')); + type.getValue() + .stream() + .filter(KeepReference::isMemberReference) + .sorted(comparing(KeepReference::name).thenComparing(KeepReference::desc)) + .map(ref -> toKeepDescriptor(ref)) + .distinct() // drop duplicates due to method descriptors with different returns + .forEachOrdered(line -> out.append(" ").append(line).append(";").println()); + out.printf("}%n"); + }); + } + + /** + * Scans for and returns references with owners matching the given prefix grouped by owner. + */ + private static Map> scan(Path jarFile, String prefix) + throws IOException { + // We read the Jar sequentially since ZipFile uses locks anyway but then allow scanning each + // class in parallel. + try (ZipFile zip = new ZipFile(jarFile.toFile())) { + return zip.stream() + .filter(entry -> entry.getName().endsWith(".class")) + .map(entry -> readFully(zip, entry)) + .parallel() + .flatMap( + content -> PrefixReferenceScanner.scan(new ClassReader(content), prefix).stream()) + .collect( + Collectors.groupingByConcurrent( + KeepReference::internalName, ImmutableSet.toImmutableSet())); + } + } + + private static byte[] readFully(ZipFile zip, ZipEntry entry) { + byte[] result = new byte[(int) entry.getSize()]; + try (InputStream content = zip.getInputStream(entry)) { + checkState(content.read(result) == result.length); + checkState(content.read() == -1); + } catch (IOException e) { + throw new IOError(e); + } + return result; + } + + private static CharSequence toKeepDescriptor(KeepReference member) { + StringBuilder result = new StringBuilder(); + if (member.isMethodReference()) { + result.append("*** ").append(member.name()).append("("); + // Ignore return type as it's unique in the source language + boolean first = true; + for (Type param : Type.getMethodType(member.desc()).getArgumentTypes()) { + if (first) { + first = false; + } else { + result.append(", "); + } + result.append(param.getClassName()); + } + result.append(")"); + } else { + checkArgument(member.isFieldReference()); + result.append("*** ").append(member.name()); // field names are unique so ignore descriptor + } + return result; + } + + private KeepScanner() {} +} diff --git a/java/com/google/devtools/build/android/desugar/scan/PrefixReferenceScanner.java b/java/com/google/devtools/build/android/desugar/scan/PrefixReferenceScanner.java new file mode 100644 index 0000000..b899ccc --- /dev/null +++ b/java/com/google/devtools/build/android/desugar/scan/PrefixReferenceScanner.java @@ -0,0 +1,405 @@ +// Copyright 2018 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package com.google.devtools.build.android.desugar.scan; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.common.collect.ImmutableSet; +import javax.annotation.Nullable; +import org.objectweb.asm.AnnotationVisitor; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.Handle; +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.TypePath; + +/** {@link ClassVisitor} that records references to classes starting with a given prefix. */ +class PrefixReferenceScanner extends ClassVisitor { + + /** + * Returns references with the given prefix in the given class. + * + * @param prefix an internal name prefix, typically a package such as {@code com/google/} + */ + public static ImmutableSet scan(ClassReader reader, String prefix) { + PrefixReferenceScanner scanner = new PrefixReferenceScanner(prefix); + // Frames irrelevant for Android so skip them. Don't skip debug info in case the class we're + // visiting has local variable tables (typically it doesn't anyways). + reader.accept(scanner, ClassReader.SKIP_FRAMES); + return scanner.roots.build(); + } + + private final ImmutableSet.Builder roots = ImmutableSet.builder(); + private final PrefixReferenceMethodVisitor mv = new PrefixReferenceMethodVisitor(); + private final PrefixReferenceFieldVisitor fv = new PrefixReferenceFieldVisitor(); + private final PrefixReferenceAnnotationVisitor av = new PrefixReferenceAnnotationVisitor(); + + private final String prefix; + + public PrefixReferenceScanner(String prefix) { + super(Opcodes.ASM6); + this.prefix = prefix; + } + + @Override + public void visit( + int version, + int access, + String name, + String signature, + String superName, + String[] interfaces) { + checkArgument(!name.startsWith(prefix)); + if (superName != null) { + classReference(superName); + } + classReferences(interfaces); + super.visit(version, access, name, signature, superName, interfaces); + } + + @Override + public AnnotationVisitor visitAnnotation(String desc, boolean visible) { + typeReference(desc); + return av; + } + + @Override + public void visitOuterClass(String owner, String name, String desc) { + classReference(owner); + if (desc != null) { + typeReference(Type.getMethodType(desc)); + } + } + + @Override + public AnnotationVisitor visitTypeAnnotation( + int typeRef, TypePath typePath, String desc, boolean visible) { + typeReference(desc); + return av; + } + + @Override + public void visitInnerClass(String name, String outerName, String innerName, int access) { + classReference(name); + if (outerName != null) { + classReference(outerName); + } + } + + @Override + public FieldVisitor visitField( + int access, String name, String desc, String signature, Object value) { + typeReference(desc); + return fv; + } + + @Override + public MethodVisitor visitMethod( + int access, String name, String desc, String signature, String[] exceptions) { + typeReference(Type.getMethodType(desc)); + classReferences(exceptions); + return mv; + } + + private void classReferences(@Nullable String[] internalNames) { + if (internalNames != null) { + for (String itf : internalNames) { + classReference(itf); + } + } + } + + // The following methods are package-private so they don't incur bridge methods when called from + // inner classes below. + + void classReference(String internalName) { + checkArgument(internalName.charAt(0) != '[' && internalName.charAt(0) != '(', internalName); + checkArgument(!internalName.endsWith(";"), internalName); + if (internalName.startsWith(prefix)) { + roots.add(KeepReference.classReference(internalName)); + } + } + + void objectReference(String internalName) { + // don't call this for method types, convert to Type instead + checkArgument(internalName.charAt(0) != '(', internalName); + if (internalName.charAt(0) == '[') { + typeReference(internalName); + } else { + classReference(internalName); + } + } + + void typeReference(String typeDesc) { + // don't call this for method types, convert to Type instead + checkArgument(typeDesc.charAt(0) != '(', typeDesc); + + int lpos = typeDesc.lastIndexOf('[') + 1; + if (typeDesc.charAt(lpos) == 'L') { + checkArgument(typeDesc.endsWith(";"), typeDesc); + classReference(typeDesc.substring(lpos, typeDesc.length() - 1)); + } else { + // else primitive or primitive array + checkArgument(typeDesc.length() == lpos + 1, typeDesc); + switch (typeDesc.charAt(lpos)) { + case 'B': + case 'C': + case 'S': + case 'I': + case 'J': + case 'D': + case 'F': + case 'Z': + break; + default: + throw new AssertionError("Unexpected type descriptor: " + typeDesc); + } + } + } + + void typeReference(Type type) { + switch (type.getSort()) { + case Type.ARRAY: + typeReference(type.getElementType()); + break; + case Type.OBJECT: + classReference(type.getInternalName()); + break; + + case Type.METHOD: + for (Type param : type.getArgumentTypes()) { + typeReference(param); + } + typeReference(type.getReturnType()); + break; + + default: + break; + } + } + + void fieldReference(String owner, String name, String desc) { + objectReference(owner); + typeReference(desc); + if (owner.startsWith(prefix)) { + roots.add(KeepReference.memberReference(owner, name, desc)); + } + } + + void methodReference(String owner, String name, String desc) { + checkArgument(desc.charAt(0) == '(', desc); + objectReference(owner); + typeReference(Type.getMethodType(desc)); + if (owner.startsWith(prefix)) { + roots.add(KeepReference.memberReference(owner, name, desc)); + } + } + + void handleReference(Handle handle) { + switch (handle.getTag()) { + case Opcodes.H_GETFIELD: + case Opcodes.H_GETSTATIC: + case Opcodes.H_PUTFIELD: + case Opcodes.H_PUTSTATIC: + fieldReference(handle.getOwner(), handle.getName(), handle.getDesc()); + break; + + default: + methodReference(handle.getOwner(), handle.getName(), handle.getDesc()); + break; + } + } + + private class PrefixReferenceMethodVisitor extends MethodVisitor { + + public PrefixReferenceMethodVisitor() { + super(Opcodes.ASM6); + } + + @Override + public AnnotationVisitor visitAnnotationDefault() { + return av; + } + + @Override + public AnnotationVisitor visitAnnotation(String desc, boolean visible) { + typeReference(desc); + return av; + } + + @Override + public AnnotationVisitor visitTypeAnnotation( + int typeRef, TypePath typePath, String desc, boolean visible) { + typeReference(desc); + return av; + } + + @Override + public AnnotationVisitor visitParameterAnnotation(int parameter, String desc, boolean visible) { + typeReference(desc); + return av; + } + + @Override + public void visitTypeInsn(int opcode, String type) { + objectReference(type); + } + + @Override + public void visitFieldInsn(int opcode, String owner, String name, String desc) { + fieldReference(owner, name, desc); + } + + @Override + @SuppressWarnings("deprecation") // Implementing deprecated method to be sure + public void visitMethodInsn(int opcode, String owner, String name, String desc) { + visitMethodInsn(opcode, owner, name, desc, opcode == Opcodes.INVOKEINTERFACE); + } + + @Override + public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) { + methodReference(owner, name, desc); + } + + @Override + public void visitInvokeDynamicInsn(String name, String desc, Handle bsm, Object... bsmArgs) { + typeReference(Type.getMethodType(desc)); + handleReference(bsm); + for (Object bsmArg : bsmArgs) { + visitConstant(bsmArg); + } + } + + @Override + public void visitLdcInsn(Object cst) { + visitConstant(cst); + } + + private void visitConstant(Object cst) { + if (cst instanceof Type) { + typeReference((Type) cst); + } else if (cst instanceof Handle) { + handleReference((Handle) cst); + } else { + // Check for other expected types as javadoc recommends + checkArgument( + cst instanceof String + || cst instanceof Integer + || cst instanceof Long + || cst instanceof Float + || cst instanceof Double, + "Unexpected constant: ", cst); + } + } + + @Override + public void visitMultiANewArrayInsn(String desc, int dims) { + typeReference(desc); + } + + @Override + public AnnotationVisitor visitInsnAnnotation( + int typeRef, TypePath typePath, String desc, boolean visible) { + typeReference(desc); + return av; + } + + @Override + public void visitTryCatchBlock(Label start, Label end, Label handler, String type) { + if (type != null) { + classReference(type); + } + } + + @Override + public AnnotationVisitor visitTryCatchAnnotation( + int typeRef, TypePath typePath, String desc, boolean visible) { + typeReference(desc); + return av; + } + + @Override + public void visitLocalVariable( + String name, String desc, String signature, Label start, Label end, int index) { + typeReference(desc); + } + + @Override + public AnnotationVisitor visitLocalVariableAnnotation( + int typeRef, + TypePath typePath, + Label[] start, + Label[] end, + int[] index, + String desc, + boolean visible) { + typeReference(desc); + return av; + } + } + + private class PrefixReferenceFieldVisitor extends FieldVisitor { + + public PrefixReferenceFieldVisitor() { + super(Opcodes.ASM6); + } + + @Override + public AnnotationVisitor visitAnnotation(String desc, boolean visible) { + typeReference(desc); + return av; + } + + @Override + public AnnotationVisitor visitTypeAnnotation( + int typeRef, TypePath typePath, String desc, boolean visible) { + typeReference(desc); + return av; + } + } + + private class PrefixReferenceAnnotationVisitor extends AnnotationVisitor { + + public PrefixReferenceAnnotationVisitor() { + super(Opcodes.ASM6); + } + + @Override + public void visit(String name, Object value) { + if (value instanceof Type) { + typeReference((Type) value); + } + } + + @Override + public void visitEnum(String name, String desc, String value) { + fieldReference(desc.substring(1, desc.length() - 1), value, desc); + } + + @Override + public AnnotationVisitor visitAnnotation(String name, String desc) { + typeReference(desc); + return av; + } + + @Override + public AnnotationVisitor visitArray(String name) { + return av; + } + } +} diff --git a/test/java/com/google/devtools/build/android/desugar/scan/test_keep_scanner.sh b/test/java/com/google/devtools/build/android/desugar/scan/test_keep_scanner.sh new file mode 100755 index 0000000..d42859f --- /dev/null +++ b/test/java/com/google/devtools/build/android/desugar/scan/test_keep_scanner.sh @@ -0,0 +1,27 @@ +#!/bin/bash +# +# Copyright 2018 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +set -eux + +out=$(mktemp) +"$1" --input "$2" --keep_file "${out}" --prefix java/ + +if ! diff "$3" "${out}"; then + echo "Unexpected output" + cat "${out}" + rm "${out}" + exit 1 +fi +rm "${out}" diff --git a/test/java/com/google/devtools/build/android/desugar/scan/testdata/CollectionReferences.java b/test/java/com/google/devtools/build/android/desugar/scan/testdata/CollectionReferences.java new file mode 100644 index 0000000..482c32a --- /dev/null +++ b/test/java/com/google/devtools/build/android/desugar/scan/testdata/CollectionReferences.java @@ -0,0 +1,60 @@ +// Copyright 2018 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package com.google.devtools.build.android.desugar.scan.testdata; + +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.LinkedList; +import java.util.List; + +/** Test data for {@code KeepScanner} with references to java.* */ +public class CollectionReferences { + + private final List dates; + + public CollectionReferences() { + dates = new ArrayList<>(7); + assert !(dates instanceof LinkedList); + } + + @SuppressWarnings("unchecked") + public void add(Date date) { + List l = (AbstractList) Collection.class.cast(dates); + l.add(date); + } + + public Date first() { + try { + return dates.get(0); + } catch (IndexOutOfBoundsException e) { + return null; + } + } + + public long min() { + long result = Long.MAX_VALUE; // compile-time constant, no ref + for (Date d : dates) { + if (d.getTime() < result) { + result = d.getTime(); + } + } + return result; + } + + static { + System.out.println("Hello!"); + } +} diff --git a/test/java/com/google/devtools/build/android/desugar/scan/testdata/OverlappingCollectionReferences.java b/test/java/com/google/devtools/build/android/desugar/scan/testdata/OverlappingCollectionReferences.java new file mode 100644 index 0000000..e743a0d --- /dev/null +++ b/test/java/com/google/devtools/build/android/desugar/scan/testdata/OverlappingCollectionReferences.java @@ -0,0 +1,49 @@ +// Copyright 2018 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package com.google.devtools.build.android.desugar.scan.testdata; + +import java.util.ArrayList; +import java.util.Date; + +/** Supplements {@link CollectionReferences} with additional and overlapping references to java.* */ +public class OverlappingCollectionReferences { + + private final ArrayList dates; + + public OverlappingCollectionReferences() { + dates = new ArrayList<>(); + } + + public void add(Date date) { + dates.add(date); + } + + public Date first() { + try { + return dates.get(0); + } catch (IndexOutOfBoundsException e) { + return null; + } + } + + public Date max() { + long result = Long.MIN_VALUE; // compile-time constant, no ref + for (Date d : dates) { + if (d.getTime() > result) { + result = d.getTime(); + } + } + return new Date(result); + } +} diff --git a/test/java/com/google/devtools/build/android/desugar/scan/testdata_golden.txt b/test/java/com/google/devtools/build/android/desugar/scan/testdata_golden.txt new file mode 100644 index 0000000..e4509b4 --- /dev/null +++ b/test/java/com/google/devtools/build/android/desugar/scan/testdata_golden.txt @@ -0,0 +1,46 @@ +-keep class java.io.PrintStream { + *** println(java.lang.String); +} +-keep class java.lang.AssertionError { + *** (); +} +-keep class java.lang.Class { + *** cast(java.lang.Object); + *** desiredAssertionStatus(); +} +-keep class java.lang.IndexOutOfBoundsException { +} +-keep class java.lang.Object { + *** (); +} +-keep class java.lang.String { +} +-keep class java.lang.System { + *** out; +} +-keep class java.util.AbstractList { +} +-keep class java.util.ArrayList { + *** (); + *** (int); + *** add(java.lang.Object); + *** get(int); + *** iterator(); +} +-keep class java.util.Collection { +} +-keep class java.util.Date { + *** (long); + *** getTime(); +} +-keep class java.util.Iterator { + *** hasNext(); + *** next(); +} +-keep class java.util.LinkedList { +} +-keep class java.util.List { + *** add(java.lang.Object); + *** get(int); + *** iterator(); +} -- cgit v1.2.3 From 669a724b8244e89d40ffd2ea0390d05c078857a3 Mon Sep 17 00:00:00 2001 From: kmb Date: Tue, 20 Feb 2018 20:16:39 -0800 Subject: Apply interface invocation desugaring to renamed core libraries. Fix invokespecial invocations for core interfaces. RELNOTES: None. PiperOrigin-RevId: 186404206 GitOrigin-RevId: f4d2dad976907abea8a727a8360c2e4e087b893f Change-Id: Ic6ddd94802f83596c35999db68ad3b28bdc93c73 --- .../desugar/CoreLibraryInvocationRewriter.java | 10 +- .../build/android/desugar/CoreLibrarySupport.java | 79 +++++++++---- .../android/desugar/DefaultMethodClassFixer.java | 2 +- .../android/desugar/CoreLibrarySupportTest.java | 127 ++++++++++++++------- 4 files changed, 151 insertions(+), 67 deletions(-) diff --git a/java/com/google/devtools/build/android/desugar/CoreLibraryInvocationRewriter.java b/java/com/google/devtools/build/android/desugar/CoreLibraryInvocationRewriter.java index 417248b..e83ae41 100644 --- a/java/com/google/devtools/build/android/desugar/CoreLibraryInvocationRewriter.java +++ b/java/com/google/devtools/build/android/desugar/CoreLibraryInvocationRewriter.java @@ -51,7 +51,8 @@ public class CoreLibraryInvocationRewriter extends ClassVisitor { @Override public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) { Class coreInterface = - support.getEmulatedCoreLibraryInvocationTarget(opcode, owner, name, desc, itf); + support.getCoreInterfaceRewritingTarget(opcode, owner, name, desc, itf); + if (coreInterface != null) { String coreInterfaceName = coreInterface.getName().replace('.', '/'); name = @@ -60,18 +61,17 @@ public class CoreLibraryInvocationRewriter extends ClassVisitor { if (opcode == Opcodes.INVOKESTATIC) { checkState(owner.equals(coreInterfaceName)); } else { - desc = - InterfaceDesugaring.companionDefaultMethodDescriptor( - opcode == Opcodes.INVOKESPECIAL ? owner : coreInterfaceName, desc); + desc = InterfaceDesugaring.companionDefaultMethodDescriptor(coreInterfaceName, desc); } if (opcode == Opcodes.INVOKESTATIC || opcode == Opcodes.INVOKESPECIAL) { checkArgument(itf, "Expected interface to rewrite %s.%s : %s", owner, name, desc); - owner = InterfaceDesugaring.getCompanionClassName(owner); + owner = InterfaceDesugaring.getCompanionClassName(coreInterfaceName); } else { // TODO(kmb): Simulate dynamic dispatch instead of calling most general default method owner = InterfaceDesugaring.getCompanionClassName(coreInterfaceName); } + opcode = Opcodes.INVOKESTATIC; itf = false; } diff --git a/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java b/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java index 2437a19..9f01638 100644 --- a/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java +++ b/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java @@ -14,6 +14,7 @@ package com.google.devtools.build.android.desugar; import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; import com.google.common.collect.ImmutableList; import java.lang.reflect.Method; @@ -37,8 +38,7 @@ class CoreLibrarySupport { private final ImmutableList> emulatedInterfaces; public CoreLibrarySupport(CoreLibraryRewriter rewriter, ClassLoader targetLoader, - ImmutableList renamedPrefixes, ImmutableList emulatedInterfaces) - throws ClassNotFoundException { + ImmutableList renamedPrefixes, ImmutableList emulatedInterfaces) { this.rewriter = rewriter; this.targetLoader = targetLoader; checkArgument( @@ -47,7 +47,7 @@ class CoreLibrarySupport { ImmutableList.Builder> classBuilder = ImmutableList.builder(); for (String itf : emulatedInterfaces) { checkArgument(itf.startsWith("java/util/"), itf); - Class clazz = targetLoader.loadClass((rewriter.getPrefix() + itf).replace('/', '.')); + Class clazz = loadFromInternal(rewriter.getPrefix() + itf); checkArgument(clazz.isInterface(), itf); classBuilder.add(clazz); } @@ -56,7 +56,7 @@ class CoreLibrarySupport { public boolean isRenamedCoreLibrary(String internalName) { String unprefixedName = rewriter.unprefix(internalName); - if (!unprefixedName.startsWith("java/")) { + if (!unprefixedName.startsWith("java/") || renamedPrefixes.isEmpty()) { return false; // shortcut } // Rename any classes desugar might generate under java/ (for emulated interfaces) as well as @@ -81,26 +81,60 @@ class CoreLibrarySupport { return getEmulatedCoreClassOrInterface(internalName) != null; } - public boolean isEmulatedCoreLibraryInvocation( - int opcode, String owner, String name, String desc, boolean itf) { - return getEmulatedCoreLibraryInvocationTarget(opcode, owner, name, desc, itf) != null; - } - + /** + * If the given invocation needs to go through a companion class of an emulated or renamed + * core interface, this methods returns that interface. This is a helper method for + * {@link CoreLibraryInvocationRewriter}. + * + *

Always returns an interface (or {@code null}), even if {@code owner} is a class. Can only + * return non-{@code null} if {@code owner} is a core library type. + */ @Nullable - public Class getEmulatedCoreLibraryInvocationTarget( + public Class getCoreInterfaceRewritingTarget( int opcode, String owner, String name, String desc, boolean itf) { - Class clazz = getEmulatedCoreClassOrInterface(owner); - if (clazz == null) { + if (owner.contains("$$Lambda$") || owner.endsWith("$$CC")) { + // Regular desugaring handles generated classes, no emulation is needed + return null; + } + if (!itf && (opcode == Opcodes.INVOKESTATIC || opcode == Opcodes.INVOKESPECIAL)) { + // Ignore staticly dispatched invocations on classes--they never need rewriting return null; } + Class clazz; + if (isRenamedCoreLibrary(owner)) { + // For renamed invocation targets we just need to do what InterfaceDesugaring does, that is, + // only worry about invokestatic and invokespecial interface invocations; nothing to do for + // invokevirtual and invokeinterface. InterfaceDesugaring ignores bootclasspath interfaces, + // so we have to do its work here for renamed interfaces. + if (itf + && (opcode == Opcodes.INVOKESTATIC || opcode == Opcodes.INVOKESPECIAL)) { + clazz = loadFromInternal(owner); + } else { + return null; + } + } else { + // If not renamed, see if the owner needs emulation. + clazz = getEmulatedCoreClassOrInterface(owner); + if (clazz == null) { + return null; + } + } + checkArgument(itf == clazz.isInterface(), "%s expected to be interface: %s", owner, itf); - if (itf && opcode == Opcodes.INVOKESTATIC) { - return clazz; // static interface method + if (opcode == Opcodes.INVOKESTATIC) { + // Static interface invocation always goes to the given owner + checkState(itf); // we should've bailed out above. + return clazz; } + // See if the invoked method is a default method, which will need rewriting. For invokespecial + // we can only get here if its a default method, and invokestatic we handled above. Method callee = findInterfaceMethod(clazz, name, desc); if (callee != null && callee.isDefault()) { return callee.getDeclaringClass(); + } else { + checkArgument(opcode != Opcodes.INVOKESPECIAL, + "Couldn't resolve interface super call %s.super.%s : %s", owner, name, desc); } return null; } @@ -117,19 +151,21 @@ class CoreLibrarySupport { } } - Class clazz; - try { - clazz = targetLoader.loadClass(internalName.replace('/', '.')); - } catch (ClassNotFoundException e) { - throw (NoClassDefFoundError) new NoClassDefFoundError().initCause(e); - } - + Class clazz = loadFromInternal(internalName); if (emulatedInterfaces.stream().anyMatch(itf -> itf.isAssignableFrom(clazz))) { return clazz; } return null; } + private Class loadFromInternal(String internalName) { + try { + return targetLoader.loadClass(internalName.replace('/', '.')); + } catch (ClassNotFoundException e) { + throw (NoClassDefFoundError) new NoClassDefFoundError().initCause(e); + } + } + private static Method findInterfaceMethod(Class clazz, String name, String desc) { return collectImplementedInterfaces(clazz, new LinkedHashSet<>()) .stream() @@ -141,7 +177,6 @@ class CoreLibrarySupport { .orElse((Method) null); } - private static Method findMethod(Class clazz, String name, String desc) { for (Method m : clazz.getMethods()) { if (m.getName().equals(name) && Type.getMethodDescriptor(m).equals(desc)) { diff --git a/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java b/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java index 52964b7..6143940 100644 --- a/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java +++ b/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java @@ -494,7 +494,7 @@ public class DefaultMethodClassFixer extends ClassVisitor { // If we're visiting a bootclasspath interface then we most likely don't have the code. // That means we can't just copy the method bodies as we're trying to do below. checkState(!isBootclasspathInterface, - "TODO stub bridge methods for core interfaces if ever needed"); + "TODO stub core interface %s bridge methods in %s", stubbedInterfaceName, internalName); // For bridges we just copy their bodies instead of going through the companion class. // Meanwhile, we also need to desugar the copied method bodies, so that any calls to // interface methods are correctly handled. diff --git a/test/java/com/google/devtools/build/android/desugar/CoreLibrarySupportTest.java b/test/java/com/google/devtools/build/android/desugar/CoreLibrarySupportTest.java index d52ef78..853dbbe 100644 --- a/test/java/com/google/devtools/build/android/desugar/CoreLibrarySupportTest.java +++ b/test/java/com/google/devtools/build/android/desugar/CoreLibrarySupportTest.java @@ -92,7 +92,7 @@ public class CoreLibrarySupportTest { } @Test - public void testIsEmulatedCoreLibraryInvocation() throws Exception { + public void testGetCoreInterfaceRewritingTarget_emulatedDefaultMethod() throws Exception { CoreLibrarySupport support = new CoreLibrarySupport( new CoreLibraryRewriter(""), @@ -100,29 +100,7 @@ public class CoreLibrarySupportTest { ImmutableList.of(), ImmutableList.of("java/util/Collection")); assertThat( - support.isEmulatedCoreLibraryInvocation( - Opcodes.INVOKEINTERFACE, - "java/util/Collection", - "removeIf", - "(Ljava/util/function/Predicate;)Z", - true)) - .isTrue(); // true for default method - assertThat( - support.isEmulatedCoreLibraryInvocation( - Opcodes.INVOKEINTERFACE, "java/util/Collection", "size", "()I", true)) - .isFalse(); // false for abstract method - } - - @Test - public void testGetEmulatedCoreLibraryInvocationTarget_defaultMethod() throws Exception { - CoreLibrarySupport support = - new CoreLibrarySupport( - new CoreLibraryRewriter(""), - Thread.currentThread().getContextClassLoader(), - ImmutableList.of(), - ImmutableList.of("java/util/Collection")); - assertThat( - support.getEmulatedCoreLibraryInvocationTarget( + support.getCoreInterfaceRewritingTarget( Opcodes.INVOKEINTERFACE, "java/util/Collection", "removeIf", @@ -130,7 +108,7 @@ public class CoreLibrarySupportTest { true)) .isEqualTo(Collection.class); assertThat( - support.getEmulatedCoreLibraryInvocationTarget( + support.getCoreInterfaceRewritingTarget( Opcodes.INVOKEVIRTUAL, "java/util/ArrayList", "removeIf", @@ -138,9 +116,9 @@ public class CoreLibrarySupportTest { false)) .isEqualTo(Collection.class); assertThat( - support.getEmulatedCoreLibraryInvocationTarget( + support.getCoreInterfaceRewritingTarget( Opcodes.INVOKEINTERFACE, - "com/google/common/collect/ImmutableList", + "com/google/HypotheticalListInterface", "removeIf", "(Ljava/util/function/Predicate;)Z", true)) @@ -148,7 +126,7 @@ public class CoreLibrarySupportTest { } @Test - public void testGetEmulatedCoreLibraryInvocationTarget_abstractMethod() throws Exception { + public void testGetCoreInterfaceRewritingTarget_abstractMethod() throws Exception { CoreLibrarySupport support = new CoreLibrarySupport( new CoreLibraryRewriter(""), @@ -156,7 +134,7 @@ public class CoreLibrarySupportTest { ImmutableList.of(), ImmutableList.of("java/util/Collection")); assertThat( - support.getEmulatedCoreLibraryInvocationTarget( + support.getCoreInterfaceRewritingTarget( Opcodes.INVOKEINTERFACE, "java/util/Collection", "size", @@ -164,7 +142,7 @@ public class CoreLibrarySupportTest { true)) .isNull(); assertThat( - support.getEmulatedCoreLibraryInvocationTarget( + support.getCoreInterfaceRewritingTarget( Opcodes.INVOKEVIRTUAL, "java/util/ArrayList", "size", @@ -174,7 +152,7 @@ public class CoreLibrarySupportTest { } @Test - public void testGetEmulatedCoreLibraryInvocationTarget_defaultOverride() throws Exception { + public void testGetCoreInterfaceRewritingTarget_emulatedDefaultOverride() throws Exception { CoreLibrarySupport support = new CoreLibrarySupport( new CoreLibraryRewriter(""), @@ -182,7 +160,7 @@ public class CoreLibrarySupportTest { ImmutableList.of(), ImmutableList.of("java/util/Map")); assertThat( - support.getEmulatedCoreLibraryInvocationTarget( + support.getCoreInterfaceRewritingTarget( Opcodes.INVOKEINTERFACE, "java/util/Map", "putIfAbsent", @@ -190,7 +168,7 @@ public class CoreLibrarySupportTest { true)) .isEqualTo(Map.class); assertThat( - support.getEmulatedCoreLibraryInvocationTarget( + support.getCoreInterfaceRewritingTarget( Opcodes.INVOKEINTERFACE, "java/util/concurrent/ConcurrentMap", "putIfAbsent", @@ -198,7 +176,7 @@ public class CoreLibrarySupportTest { true)) .isNull(); // putIfAbsent is default in Map but abstract in ConcurrentMap assertThat( - support.getEmulatedCoreLibraryInvocationTarget( + support.getCoreInterfaceRewritingTarget( Opcodes.INVOKEINTERFACE, "java/util/concurrent/ConcurrentMap", "getOrDefault", @@ -208,7 +186,7 @@ public class CoreLibrarySupportTest { } @Test - public void testGetEmulatedCoreLibraryInvocationTarget_staticInterfaceMethod() throws Exception { + public void testGetCoreInterfaceRewritingTarget_staticInterfaceMethod() throws Exception { CoreLibrarySupport support = new CoreLibrarySupport( new CoreLibraryRewriter(""), @@ -216,7 +194,7 @@ public class CoreLibrarySupportTest { ImmutableList.of(), ImmutableList.of("java/util/Comparator")); assertThat( - support.getEmulatedCoreLibraryInvocationTarget( + support.getCoreInterfaceRewritingTarget( Opcodes.INVOKESTATIC, "java/util/Comparator", "reverseOrder", @@ -225,8 +203,79 @@ public class CoreLibrarySupportTest { .isEqualTo(Comparator.class); } + /** + * Tests that call sites of renamed core libraries are treated like call sites in regular + * {@link InterfaceDesugaring}. + */ + @Test + public void testGetEmulatedCoreLibraryInvocationTarget_renamed() throws Exception { + CoreLibrarySupport support = + new CoreLibrarySupport( + new CoreLibraryRewriter(""), + Thread.currentThread().getContextClassLoader(), + ImmutableList.of("java/util/"), + ImmutableList.of()); + + // regular invocations of default methods: ignored + assertThat( + support.getCoreInterfaceRewritingTarget( + Opcodes.INVOKEINTERFACE, + "java/util/Collection", + "removeIf", + "(Ljava/util/function/Predicate;)Z", + true)) + .isNull(); + assertThat( + support.getCoreInterfaceRewritingTarget( + Opcodes.INVOKEVIRTUAL, + "java/util/ArrayList", + "removeIf", + "(Ljava/util/function/Predicate;)Z", + false)) + .isNull(); + + // abstract methods: ignored + assertThat( + support.getCoreInterfaceRewritingTarget( + Opcodes.INVOKEINTERFACE, + "java/util/Collection", + "size", + "()I", + true)) + .isNull(); + + // static interface method + assertThat( + support.getCoreInterfaceRewritingTarget( + Opcodes.INVOKESTATIC, + "java/util/Comparator", + "reverseOrder", + "()Ljava/util/Comparator;", + true)) + .isEqualTo(Comparator.class); + + // invokespecial for default methods: find nearest definition + assertThat( + support.getCoreInterfaceRewritingTarget( + Opcodes.INVOKESPECIAL, + "java/util/List", + "removeIf", + "(Ljava/util/function/Predicate;)Z", + true)) + .isEqualTo(Collection.class); + // invokespecial on a class: ignore (even if there's an inherited default method) + assertThat( + support.getCoreInterfaceRewritingTarget( + Opcodes.INVOKESPECIAL, + "java/util/ArrayList", + "removeIf", + "(Ljava/util/function/Predicate;)Z", + false)) + .isNull(); + } + @Test - public void testGetEmulatedCoreLibraryInvocationTarget_ignoreRenamed() throws Exception { + public void testGetCoreInterfaceRewritingTarget_ignoreRenamedInvokeInterface() throws Exception { CoreLibrarySupport support = new CoreLibrarySupport( new CoreLibraryRewriter(""), @@ -234,7 +283,7 @@ public class CoreLibrarySupportTest { ImmutableList.of("java/util/concurrent/"), // should return null for these ImmutableList.of("java/util/Map")); assertThat( - support.getEmulatedCoreLibraryInvocationTarget( + support.getCoreInterfaceRewritingTarget( Opcodes.INVOKEINTERFACE, "java/util/Map", "getOrDefault", @@ -242,7 +291,7 @@ public class CoreLibrarySupportTest { true)) .isEqualTo(Map.class); assertThat( - support.getEmulatedCoreLibraryInvocationTarget( + support.getCoreInterfaceRewritingTarget( Opcodes.INVOKEINTERFACE, "java/util/concurrent/ConcurrentMap", "getOrDefault", -- cgit v1.2.3 From 4f68e2ecebee00ab4fe882c691bb750ce6dab64b Mon Sep 17 00:00:00 2001 From: kmb Date: Wed, 21 Feb 2018 21:34:01 -0800 Subject: add ability to move individual core library methods RELNOTES: None. PiperOrigin-RevId: 186565673 GitOrigin-RevId: deb99ccfb4e6b236c21e6d425281870aa598804a Change-Id: I56030d75aa6b3666299aa98ec961ef7078917975 --- .../desugar/CoreLibraryInvocationRewriter.java | 12 ++++- .../build/android/desugar/CoreLibrarySupport.java | 30 +++++++++++- .../devtools/build/android/desugar/Desugar.java | 15 +++++- .../android/desugar/CoreLibrarySupportTest.java | 55 ++++++++++++++++++---- .../android/desugar/CorePackageRenamerTest.java | 20 ++++++-- 5 files changed, 115 insertions(+), 17 deletions(-) diff --git a/java/com/google/devtools/build/android/desugar/CoreLibraryInvocationRewriter.java b/java/com/google/devtools/build/android/desugar/CoreLibraryInvocationRewriter.java index e83ae41..b4bc98b 100644 --- a/java/com/google/devtools/build/android/desugar/CoreLibraryInvocationRewriter.java +++ b/java/com/google/devtools/build/android/desugar/CoreLibraryInvocationRewriter.java @@ -52,8 +52,18 @@ public class CoreLibraryInvocationRewriter extends ClassVisitor { public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) { Class coreInterface = support.getCoreInterfaceRewritingTarget(opcode, owner, name, desc, itf); + String newOwner = support.getMoveTarget(owner, name); - if (coreInterface != null) { + if (newOwner != null) { + checkState(coreInterface == null, + "Can't move and use companion: %s.%s : %s", owner, name, desc); + if (opcode != Opcodes.INVOKESTATIC) { + // assuming a static method + desc = InterfaceDesugaring.companionDefaultMethodDescriptor(owner, desc); + opcode = Opcodes.INVOKESTATIC; + } + itf = false; // assuming a class + } else if (coreInterface != null) { String coreInterfaceName = coreInterface.getName().replace('.', '/'); name = InterfaceDesugaring.normalizeInterfaceMethodName( diff --git a/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java b/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java index 9f01638..76eb346 100644 --- a/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java +++ b/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java @@ -16,9 +16,12 @@ package com.google.devtools.build.android.desugar; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkState; +import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import java.lang.reflect.Method; import java.util.LinkedHashSet; +import java.util.List; import java.util.Objects; import java.util.Set; import javax.annotation.Nullable; @@ -36,9 +39,12 @@ class CoreLibrarySupport { private final ImmutableList renamedPrefixes; /** Internal names of interfaces whose default and static interface methods we'll emulate. */ private final ImmutableList> emulatedInterfaces; + /** Map from {@code owner#name} core library members to their new owners. */ + private final ImmutableMap memberMoves; public CoreLibrarySupport(CoreLibraryRewriter rewriter, ClassLoader targetLoader, - ImmutableList renamedPrefixes, ImmutableList emulatedInterfaces) { + ImmutableList renamedPrefixes, ImmutableList emulatedInterfaces, + List memberMoves) { this.rewriter = rewriter; this.targetLoader = targetLoader; checkArgument( @@ -52,6 +58,23 @@ class CoreLibrarySupport { classBuilder.add(clazz); } this.emulatedInterfaces = classBuilder.build(); + + // We can call isRenamed and rename below b/c we initialized the necessary fields above + ImmutableMap.Builder movesBuilder = ImmutableMap.builder(); + Splitter splitter = Splitter.on("->").trimResults().omitEmptyStrings(); + for (String move : memberMoves) { + List pair = splitter.splitToList(move); + checkArgument(pair.size() == 2, "Doesn't split as expected: %s", move); + checkArgument(pair.get(0).startsWith("java/"), "Unexpected member: %s", move); + int sep = pair.get(0).indexOf('#'); + checkArgument(sep > 0 && sep == pair.get(0).lastIndexOf('#'), "invalid member: %s", move); + checkArgument(!isRenamedCoreLibrary(pair.get(0).substring(0, sep)), + "Original renamed, no need to move it: %s", move); + checkArgument(isRenamedCoreLibrary(pair.get(1)), "Target not renamed: %s", move); + + movesBuilder.put(pair.get(0), renameCoreLibrary(pair.get(1))); + } + this.memberMoves = movesBuilder.build(); } public boolean isRenamedCoreLibrary(String internalName) { @@ -73,6 +96,11 @@ class CoreLibrarySupport { : internalName; } + @Nullable + public String getMoveTarget(String owner, String name) { + return memberMoves.get(rewriter.unprefix(owner) + '#' + name); + } + /** * Returns {@code true} for java.* classes or interfaces that are subtypes of emulated interfaces. * Note that implies that this method always returns {@code false} for user-written classes. diff --git a/java/com/google/devtools/build/android/desugar/Desugar.java b/java/com/google/devtools/build/android/desugar/Desugar.java index 58b9b88..cd55655 100644 --- a/java/com/google/devtools/build/android/desugar/Desugar.java +++ b/java/com/google/devtools/build/android/desugar/Desugar.java @@ -271,6 +271,18 @@ class Desugar { ) public List emulateCoreLibraryInterfaces; + /** Members that we will retarget to the given new owner. */ + @Option( + name = "retarget_core_library_member", + defaultValue = "", // ignored + allowMultiple = true, + documentationCategory = OptionDocumentationCategory.UNDOCUMENTED, + effectTags = {OptionEffectTag.UNKNOWN}, + help = "Method invocations to retarget, given as \"class/Name#member->new/class/Name\". " + + "The new owner is blindly assumed to exist." + ) + public List retargetCoreLibraryMembers; + /** 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( @@ -388,7 +400,8 @@ class Desugar { rewriter, loader, ImmutableList.copyOf(options.rewriteCoreLibraryPrefixes), - ImmutableList.copyOf(options.emulateCoreLibraryInterfaces)); + ImmutableList.copyOf(options.emulateCoreLibraryInterfaces), + options.retargetCoreLibraryMembers); desugarClassesInInput( inputFiles, diff --git a/test/java/com/google/devtools/build/android/desugar/CoreLibrarySupportTest.java b/test/java/com/google/devtools/build/android/desugar/CoreLibrarySupportTest.java index 853dbbe..cc17748 100644 --- a/test/java/com/google/devtools/build/android/desugar/CoreLibrarySupportTest.java +++ b/test/java/com/google/devtools/build/android/desugar/CoreLibrarySupportTest.java @@ -32,7 +32,11 @@ public class CoreLibrarySupportTest { public void testIsRenamedCoreLibrary() throws Exception { CoreLibrarySupport support = new CoreLibrarySupport( - new CoreLibraryRewriter(""), null, ImmutableList.of("java/time/"), ImmutableList.of()); + new CoreLibraryRewriter(""), + null, + ImmutableList.of("java/time/"), + ImmutableList.of(), + ImmutableList.of()); assertThat(support.isRenamedCoreLibrary("java/time/X")).isTrue(); assertThat(support.isRenamedCoreLibrary("java/time/y/X")).isTrue(); assertThat(support.isRenamedCoreLibrary("java/io/X")).isFalse(); @@ -48,6 +52,7 @@ public class CoreLibrarySupportTest { new CoreLibraryRewriter("__/"), null, ImmutableList.of("java/time/"), + ImmutableList.of(), ImmutableList.of()); assertThat(support.isRenamedCoreLibrary("__/java/time/X")).isTrue(); assertThat(support.isRenamedCoreLibrary("__/java/time/y/X")).isTrue(); @@ -60,7 +65,11 @@ public class CoreLibrarySupportTest { public void testRenameCoreLibrary() throws Exception { CoreLibrarySupport support = new CoreLibrarySupport( - new CoreLibraryRewriter(""), null, ImmutableList.of(), ImmutableList.of()); + new CoreLibraryRewriter(""), + null, + ImmutableList.of(), + ImmutableList.of(), + ImmutableList.of()); assertThat(support.renameCoreLibrary("java/time/X")).isEqualTo("j$/time/X"); assertThat(support.renameCoreLibrary("com/google/X")).isEqualTo("com/google/X"); } @@ -69,11 +78,30 @@ public class CoreLibrarySupportTest { public void testRenameCoreLibrary_prefixedLoader() throws Exception { CoreLibrarySupport support = new CoreLibrarySupport( - new CoreLibraryRewriter("__/"), null, ImmutableList.of(), ImmutableList.of()); + new CoreLibraryRewriter("__/"), + null, + ImmutableList.of(), + ImmutableList.of(), + ImmutableList.of()); assertThat(support.renameCoreLibrary("__/java/time/X")).isEqualTo("j$/time/X"); assertThat(support.renameCoreLibrary("com/google/X")).isEqualTo("com/google/X"); } + @Test + public void testMoveTarget() throws Exception { + CoreLibrarySupport support = + new CoreLibrarySupport( + new CoreLibraryRewriter("__/"), + null, + ImmutableList.of("java/util/Helper"), + ImmutableList.of(), + ImmutableList.of("java/util/Existing#match -> java/util/Helper")); + assertThat(support.getMoveTarget("__/java/util/Existing", "match")).isEqualTo("j$/util/Helper"); + assertThat(support.getMoveTarget("java/util/Existing", "match")).isEqualTo("j$/util/Helper"); + assertThat(support.getMoveTarget("__/java/util/Existing", "matchesnot")).isNull(); + assertThat(support.getMoveTarget("__/java/util/ExistingOther", "match")).isNull(); + } + @Test public void testIsEmulatedCoreClassOrInterface() throws Exception { CoreLibrarySupport support = @@ -81,7 +109,8 @@ public class CoreLibrarySupportTest { new CoreLibraryRewriter(""), Thread.currentThread().getContextClassLoader(), ImmutableList.of("java/util/concurrent/"), - ImmutableList.of("java/util/Map")); + ImmutableList.of("java/util/Map"), + ImmutableList.of()); assertThat(support.isEmulatedCoreClassOrInterface("java/util/Map")).isTrue(); assertThat(support.isEmulatedCoreClassOrInterface("java/util/Map$$Lambda$17")).isFalse(); assertThat(support.isEmulatedCoreClassOrInterface("java/util/Map$$CC")).isFalse(); @@ -98,7 +127,8 @@ public class CoreLibrarySupportTest { new CoreLibraryRewriter(""), Thread.currentThread().getContextClassLoader(), ImmutableList.of(), - ImmutableList.of("java/util/Collection")); + ImmutableList.of("java/util/Collection"), + ImmutableList.of()); assertThat( support.getCoreInterfaceRewritingTarget( Opcodes.INVOKEINTERFACE, @@ -132,7 +162,8 @@ public class CoreLibrarySupportTest { new CoreLibraryRewriter(""), Thread.currentThread().getContextClassLoader(), ImmutableList.of(), - ImmutableList.of("java/util/Collection")); + ImmutableList.of("java/util/Collection"), + ImmutableList.of()); assertThat( support.getCoreInterfaceRewritingTarget( Opcodes.INVOKEINTERFACE, @@ -158,7 +189,8 @@ public class CoreLibrarySupportTest { new CoreLibraryRewriter(""), Thread.currentThread().getContextClassLoader(), ImmutableList.of(), - ImmutableList.of("java/util/Map")); + ImmutableList.of("java/util/Map"), + ImmutableList.of()); assertThat( support.getCoreInterfaceRewritingTarget( Opcodes.INVOKEINTERFACE, @@ -192,7 +224,8 @@ public class CoreLibrarySupportTest { new CoreLibraryRewriter(""), Thread.currentThread().getContextClassLoader(), ImmutableList.of(), - ImmutableList.of("java/util/Comparator")); + ImmutableList.of("java/util/Comparator"), + ImmutableList.of()); assertThat( support.getCoreInterfaceRewritingTarget( Opcodes.INVOKESTATIC, @@ -208,12 +241,13 @@ public class CoreLibrarySupportTest { * {@link InterfaceDesugaring}. */ @Test - public void testGetEmulatedCoreLibraryInvocationTarget_renamed() throws Exception { + public void testGetCoreInterfaceRewritingTarget_renamed() throws Exception { CoreLibrarySupport support = new CoreLibrarySupport( new CoreLibraryRewriter(""), Thread.currentThread().getContextClassLoader(), ImmutableList.of("java/util/"), + ImmutableList.of(), ImmutableList.of()); // regular invocations of default methods: ignored @@ -281,7 +315,8 @@ public class CoreLibrarySupportTest { new CoreLibraryRewriter(""), Thread.currentThread().getContextClassLoader(), ImmutableList.of("java/util/concurrent/"), // should return null for these - ImmutableList.of("java/util/Map")); + ImmutableList.of("java/util/Map"), + ImmutableList.of()); assertThat( support.getCoreInterfaceRewritingTarget( Opcodes.INVOKEINTERFACE, diff --git a/test/java/com/google/devtools/build/android/desugar/CorePackageRenamerTest.java b/test/java/com/google/devtools/build/android/desugar/CorePackageRenamerTest.java index 0626b46..0ff0f25 100644 --- a/test/java/com/google/devtools/build/android/desugar/CorePackageRenamerTest.java +++ b/test/java/com/google/devtools/build/android/desugar/CorePackageRenamerTest.java @@ -29,10 +29,15 @@ public class CorePackageRenamerTest { @Test public void testSymbolRewrite() throws Exception { MockClassVisitor out = new MockClassVisitor(); - CorePackageRenamer renamer = new CorePackageRenamer( - out, - new CoreLibrarySupport( - new CoreLibraryRewriter(""), null, ImmutableList.of("java/time/"), ImmutableList.of())); + CorePackageRenamer renamer = + new CorePackageRenamer( + out, + new CoreLibrarySupport( + new CoreLibraryRewriter(""), + null, + ImmutableList.of("java/time/"), + ImmutableList.of(), + ImmutableList.of("java/util/A#m->java/time/B"))); MethodVisitor mv = renamer.visitMethod(0, "test", "()V", null, null); mv.visitMethodInsn( @@ -40,6 +45,13 @@ public class CorePackageRenamerTest { assertThat(out.mv.owner).isEqualTo("j$/time/Instant"); assertThat(out.mv.desc).isEqualTo("()Lj$/time/Instant;"); + // Ignore moved methods but not their descriptors + mv.visitMethodInsn( + Opcodes.INVOKESTATIC, "java/util/A", "m", "()Ljava/time/Instant;", false); + assertThat(out.mv.owner).isEqualTo("java/util/A"); + assertThat(out.mv.desc).isEqualTo("()Lj$/time/Instant;"); + + // Ignore arbitrary other methods but not their descriptors mv.visitMethodInsn( Opcodes.INVOKESTATIC, "other/time/Instant", "now", "()Ljava/time/Instant;", false); assertThat(out.mv.owner).isEqualTo("other/time/Instant"); -- cgit v1.2.3 From 904e6dc6adeb2390c9216d5ed95e2090c4045ce4 Mon Sep 17 00:00:00 2001 From: kmb Date: Thu, 22 Feb 2018 14:32:56 -0800 Subject: Add a check to avoid core library default methods that (accidentally) aren't being desugared. RELNOTES: None. PiperOrigin-RevId: 186675372 GitOrigin-RevId: f13d6f5b153d8713a8af7e2ba0d5dce0e9a577e8 Change-Id: Ie58fefa56a2eabf67ddaef4b0cea565eede64b45 --- .../build/android/desugar/CoreLibrarySupport.java | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java b/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java index 76eb346..72c7edd 100644 --- a/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java +++ b/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java @@ -20,6 +20,7 @@ import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import java.lang.reflect.Method; +import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Objects; @@ -159,7 +160,24 @@ class CoreLibrarySupport { // we can only get here if its a default method, and invokestatic we handled above. Method callee = findInterfaceMethod(clazz, name, desc); if (callee != null && callee.isDefault()) { - return callee.getDeclaringClass(); + Class result = callee.getDeclaringClass(); + if (isRenamedCoreLibrary(result.getName().replace('.', '/')) + || emulatedInterfaces.stream().anyMatch(emulated -> emulated.isAssignableFrom(result))) { + return result; + } + // We get here if the declaring class is a supertype of an emulated interface. In that case + // use the emulated interface instead (since we don't desugar the supertype). Fail in case + // there are multiple possibilities. + Iterator> roots = + emulatedInterfaces + .stream() + .filter( + emulated -> emulated.isAssignableFrom(clazz) && result.isAssignableFrom(emulated)) + .iterator(); + checkState(roots.hasNext()); // must exist + Class substitute = roots.next(); + checkState(!roots.hasNext(), "Ambiguous emulation substitute: %s", callee); + return substitute; } else { checkArgument(opcode != Opcodes.INVOKESPECIAL, "Couldn't resolve interface super call %s.super.%s : %s", owner, name, desc); -- cgit v1.2.3 From e3ba7606e4f4f8b65c7bebc0b81ecd768bc14abb Mon Sep 17 00:00:00 2001 From: kmb Date: Thu, 22 Feb 2018 16:09:27 -0800 Subject: Actually retarget so-configured invocations in android desugaring RELNOTES: None. PiperOrigin-RevId: 186690865 GitOrigin-RevId: c4f1df5b05e6b39c7c3d6538e702e4d7ff041cfb Change-Id: Ib773bdc615639b82eab4943726dacf7004ce2983 --- .../devtools/build/android/desugar/CoreLibraryInvocationRewriter.java | 1 + 1 file changed, 1 insertion(+) diff --git a/java/com/google/devtools/build/android/desugar/CoreLibraryInvocationRewriter.java b/java/com/google/devtools/build/android/desugar/CoreLibraryInvocationRewriter.java index b4bc98b..fb62219 100644 --- a/java/com/google/devtools/build/android/desugar/CoreLibraryInvocationRewriter.java +++ b/java/com/google/devtools/build/android/desugar/CoreLibraryInvocationRewriter.java @@ -62,6 +62,7 @@ public class CoreLibraryInvocationRewriter extends ClassVisitor { desc = InterfaceDesugaring.companionDefaultMethodDescriptor(owner, desc); opcode = Opcodes.INVOKESTATIC; } + owner = newOwner; itf = false; // assuming a class } else if (coreInterface != null) { String coreInterfaceName = coreInterface.getName().replace('.', '/'); -- cgit v1.2.3 From dc5db10e28ac50cbd3bbbb48ff815267a82907ac Mon Sep 17 00:00:00 2001 From: kmb Date: Sat, 24 Feb 2018 15:46:40 -0800 Subject: Rename and implement emulated interfaces as needed during android desugaring RELNOTES: None. PiperOrigin-RevId: 186904092 GitOrigin-RevId: 30af177d5cd2188ee6e23ba849d865b8a42ad8f8 Change-Id: I6ba0cd552638f560bdbfef1ff308ba436a2de720 --- .../devtools/build/android/desugar/Desugar.java | 3 + .../android/desugar/EmulatedInterfaceRewriter.java | 65 ++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 java/com/google/devtools/build/android/desugar/EmulatedInterfaceRewriter.java diff --git a/java/com/google/devtools/build/android/desugar/Desugar.java b/java/com/google/devtools/build/android/desugar/Desugar.java index cd55655..eee8802 100644 --- a/java/com/google/devtools/build/android/desugar/Desugar.java +++ b/java/com/google/devtools/build/android/desugar/Desugar.java @@ -617,6 +617,7 @@ class Desugar { // Don't need a ClassReaderFactory b/c static interface methods should've been moved. ClassVisitor visitor = writer; if (coreLibrarySupport != null) { + visitor = new EmulatedInterfaceRewriter(visitor, coreLibrarySupport); visitor = new CorePackageRenamer(visitor, coreLibrarySupport); visitor = new CoreLibraryInvocationRewriter(visitor, coreLibrarySupport); } @@ -669,6 +670,7 @@ class Desugar { ClassVisitor visitor = checkNotNull(writer); if (coreLibrarySupport != null) { + visitor = new EmulatedInterfaceRewriter(visitor, coreLibrarySupport); visitor = new CorePackageRenamer(visitor, coreLibrarySupport); visitor = new CoreLibraryInvocationRewriter(visitor, coreLibrarySupport); } @@ -751,6 +753,7 @@ class Desugar { ClassVisitor visitor = checkNotNull(writer); if (coreLibrarySupport != null) { + visitor = new EmulatedInterfaceRewriter(visitor, coreLibrarySupport); visitor = new CorePackageRenamer(visitor, coreLibrarySupport); visitor = new CoreLibraryInvocationRewriter(visitor, coreLibrarySupport); } diff --git a/java/com/google/devtools/build/android/desugar/EmulatedInterfaceRewriter.java b/java/com/google/devtools/build/android/desugar/EmulatedInterfaceRewriter.java new file mode 100644 index 0000000..d3e786d --- /dev/null +++ b/java/com/google/devtools/build/android/desugar/EmulatedInterfaceRewriter.java @@ -0,0 +1,65 @@ +// Copyright 2018 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package com.google.devtools.build.android.desugar; + +import java.util.ArrayList; +import java.util.Collections; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.Opcodes; + +/** + * Visitor that renames emulated interfaces and marks classes that extend emulated interfaces to + * also implement the renamed interfaces. {@link DefaultMethodClassFixer} makes sure the requisite + * methods are present. Doing this helps with dynamic dispatch on emulated interfaces. + */ +public class EmulatedInterfaceRewriter extends ClassVisitor { + + private final CoreLibrarySupport support; + + public EmulatedInterfaceRewriter(ClassVisitor dest, CoreLibrarySupport support) { + super(Opcodes.ASM6, dest); + this.support = support; + } + + @Override + public void visit( + int version, + int access, + String name, + String signature, + String superName, + String[] interfaces) { + boolean isEmulated = support.isEmulatedCoreClassOrInterface(name); + if (interfaces != null && interfaces.length > 0 && !isEmulated) { + // Make classes implementing emulated interfaces also implement the renamed interfaces we + // create below. + ArrayList newInterfaces = new ArrayList<>(interfaces.length + 2); + Collections.addAll(newInterfaces, interfaces); + for (String itf : interfaces) { + if (support.isEmulatedCoreClassOrInterface(itf)) { + newInterfaces.add(support.renameCoreLibrary(itf)); + } + } + if (interfaces.length != newInterfaces.size()) { + interfaces = newInterfaces.toArray(interfaces); + signature = null; // additional interfaces invalidate signature + } + } + + if (BitFlags.isInterface(access) && isEmulated) { + name = support.renameCoreLibrary(name); + } + super.visit(version, access, name, signature, superName, interfaces); + } +} -- cgit v1.2.3 From fbc46a6724558ac5cc73feac83b58098bef3da08 Mon Sep 17 00:00:00 2001 From: kmb Date: Mon, 26 Feb 2018 13:56:47 -0800 Subject: add binary flag for core library desugaring and gate existing configuration flags by it. RELNOTES: None. PiperOrigin-RevId: 187075897 GitOrigin-RevId: cc090ed9b8544deea7a7c5cab17b263926e8c48b Change-Id: I43a2d49e45095b23fc2c1249d1d3a97274e5b089 --- .../build/android/desugar/CoreLibrarySupport.java | 17 ++++++++------ .../devtools/build/android/desugar/Desugar.java | 26 ++++++++++++++++------ 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java b/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java index 72c7edd..90e6bc0 100644 --- a/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java +++ b/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java @@ -17,8 +17,8 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkState; import com.google.common.base.Splitter; -import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import java.lang.reflect.Method; import java.util.Iterator; import java.util.LinkedHashSet; @@ -37,21 +37,24 @@ class CoreLibrarySupport { private final CoreLibraryRewriter rewriter; private final ClassLoader targetLoader; /** Internal name prefixes that we want to move to a custom package. */ - private final ImmutableList renamedPrefixes; + private final ImmutableSet renamedPrefixes; /** Internal names of interfaces whose default and static interface methods we'll emulate. */ - private final ImmutableList> emulatedInterfaces; + private final ImmutableSet> emulatedInterfaces; /** Map from {@code owner#name} core library members to their new owners. */ private final ImmutableMap memberMoves; - public CoreLibrarySupport(CoreLibraryRewriter rewriter, ClassLoader targetLoader, - ImmutableList renamedPrefixes, ImmutableList emulatedInterfaces, + public CoreLibrarySupport( + CoreLibraryRewriter rewriter, + ClassLoader targetLoader, + List renamedPrefixes, + List emulatedInterfaces, List memberMoves) { this.rewriter = rewriter; this.targetLoader = targetLoader; checkArgument( renamedPrefixes.stream().allMatch(prefix -> prefix.startsWith("java/")), renamedPrefixes); - this.renamedPrefixes = renamedPrefixes; - ImmutableList.Builder> classBuilder = ImmutableList.builder(); + this.renamedPrefixes = ImmutableSet.copyOf(renamedPrefixes); + ImmutableSet.Builder> classBuilder = ImmutableSet.builder(); for (String itf : emulatedInterfaces) { checkArgument(itf.startsWith("java/util/"), itf); Class clazz = loadFromInternal(rewriter.getPrefix() + itf); diff --git a/java/com/google/devtools/build/android/desugar/Desugar.java b/java/com/google/devtools/build/android/desugar/Desugar.java index eee8802..0fad8c2 100644 --- a/java/com/google/devtools/build/android/desugar/Desugar.java +++ b/java/com/google/devtools/build/android/desugar/Desugar.java @@ -195,6 +195,15 @@ class Desugar { ) public boolean tolerateMissingDependencies; + @Option( + name = "desugar_supported_core_libs", + defaultValue = "false", + documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, + effectTags = {OptionEffectTag.UNKNOWN}, + help = "Enable core library desugaring, which requires configuration with related flags." + ) + public boolean desugarCoreLibs; + @Option( name = "desugar_interface_method_bodies_if_needed", defaultValue = "true", @@ -393,15 +402,14 @@ class Desugar { ImmutableSet.Builder interfaceLambdaMethodCollector = ImmutableSet.builder(); ClassVsInterface interfaceCache = new ClassVsInterface(classpathReader); CoreLibrarySupport coreLibrarySupport = - options.rewriteCoreLibraryPrefixes.isEmpty() - && options.emulateCoreLibraryInterfaces.isEmpty() - ? null - : new CoreLibrarySupport( + options.desugarCoreLibs + ? new CoreLibrarySupport( rewriter, loader, - ImmutableList.copyOf(options.rewriteCoreLibraryPrefixes), - ImmutableList.copyOf(options.emulateCoreLibraryInterfaces), - options.retargetCoreLibraryMembers); + options.rewriteCoreLibraryPrefixes, + options.emulateCoreLibraryInterfaces, + options.retargetCoreLibraryMembers) + : null; desugarClassesInInput( inputFiles, @@ -900,6 +908,10 @@ class Desugar { for (Path path : options.bootclasspath) { checkArgument(!Files.isDirectory(path), "Bootclasspath entry must be a jar file: %s", path); } + checkArgument(!options.desugarCoreLibs + || !options.rewriteCoreLibraryPrefixes.isEmpty() + || !options.emulateCoreLibraryInterfaces.isEmpty(), + "--desugar_supported_core_libs requires specifying renamed and/or emulated core libraries"); return options; } -- cgit v1.2.3 From e665bea7c471f64425df2e39db3314f2abe345d3 Mon Sep 17 00:00:00 2001 From: ccalvarin Date: Thu, 1 Mar 2018 07:29:45 -0800 Subject: Fix invocation policy's handling of the null default when filtering values. For a filter on option values (either by whitelist, allow_values, or blacklist, disallow_values), one of the options for what to do when encountering a disallowed value is to replace it with the default. This default must be itself an allowed value for this to make sense, so this is checked. This check, however, shouldn't apply to flags that are null by default, since these flags' default value is not parsed by the converter, so there is no guarantee that there exists an accepted user-input value that would also set the value to NULL. In these cases, we assume that "unset" is a distinct value that is always allowed. RELNOTES: None. PiperOrigin-RevId: 187475696 GitOrigin-RevId: 06e687495b4c85f86215c7cc7f1a01dc7f6709f9 Change-Id: I1949e180ce32094faf0f46bc7cd627f464ca53f6 --- .../common/options/InvocationPolicyEnforcer.java | 24 ++++++++++++++-------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/java/com/google/devtools/common/options/InvocationPolicyEnforcer.java b/java/com/google/devtools/common/options/InvocationPolicyEnforcer.java index a53ff5b..88deb46 100644 --- a/java/com/google/devtools/common/options/InvocationPolicyEnforcer.java +++ b/java/com/google/devtools/common/options/InvocationPolicyEnforcer.java @@ -306,9 +306,7 @@ public final class InvocationPolicyEnforcer { *

None of the flagPolicies returned should be on expansion flags. */ private static List expandPolicy( - FlagPolicyWithContext originalPolicy, - OptionsParser parser, - Level loglevel) + FlagPolicyWithContext originalPolicy, OptionsParser parser, Level loglevel) throws OptionsParsingException { List expandedPolicies = new ArrayList<>(); @@ -701,11 +699,15 @@ public final class InvocationPolicyEnforcer { } // Check that if the default value of the flag is disallowed by the policy, that the policy - // does not also set use_default. Otherwise the default value would will still be set if the + // does not also set use_default. Otherwise the default value would still be set if the // user uses a disallowed value. This doesn't apply to repeatable flags since the default - // value for repeatable flags is always the empty list. - if (!optionDescription.getOptionDefinition().allowsMultiple()) { - + // value for repeatable flags is always the empty list. It also doesn't apply to flags that + // are null by default, since these flags' default value is not parsed by the converter, so + // there is no guarantee that there exists an accepted user-input value that would also set + // the value to NULL. In these cases, we assume that "unset" is a distinct value that is + // always allowed. + if (!optionDescription.getOptionDefinition().allowsMultiple() + && !optionDescription.getOptionDefinition().isSpecialNullDefault()) { boolean defaultValueAllowed = isFlagValueAllowed( convertedPolicyValues, optionDescription.getOptionDefinition().getDefaultValue()); @@ -749,8 +751,12 @@ public final class InvocationPolicyEnforcer { throws OptionsParsingException { OptionDefinition optionDefinition = optionDescription.getOptionDefinition(); - if (!isFlagValueAllowed( - convertedPolicyValues, optionDescription.getOptionDefinition().getDefaultValue())) { + if (optionDefinition.isSpecialNullDefault()) { + // Do nothing, the unset value by definition cannot be set. In option filtering operations, + // the value is being filtered, but the value that is `no value` passes any filter. + // Otherwise, there is no way to "usedefault" on one of these options that has no value by + // default. + } else if (!isFlagValueAllowed(convertedPolicyValues, optionDefinition.getDefaultValue())) { if (newValue != null) { // Use the default value from the policy, since the original default is not allowed logger.log( -- cgit v1.2.3 From 6cd318887515e0e7ed0473ef0f8a7c89ee7aaa58 Mon Sep 17 00:00:00 2001 From: kmb Date: Thu, 1 Mar 2018 12:56:13 -0800 Subject: send invocations to emulated interfaces through dispatch helper. fix logic for implementing emulated interfaces. RELNOTES: None. PiperOrigin-RevId: 187520298 GitOrigin-RevId: 4b6c0ec4b54e258763ce22e1a7f529d293aff026 Change-Id: If35dfebaa31dc5ea170c945f0ae7b26edf260ba2 --- .../desugar/CoreLibraryInvocationRewriter.java | 3 +- .../build/android/desugar/CoreLibrarySupport.java | 129 ++++++++++++++++++++- .../devtools/build/android/desugar/Desugar.java | 3 + .../android/desugar/EmulatedInterfaceRewriter.java | 56 ++++++--- .../build/android/desugar/InterfaceDesugaring.java | 7 ++ .../android/desugar/CoreLibrarySupportTest.java | 12 ++ .../android/desugar/CorePackageRenamerTest.java | 1 + 7 files changed, 189 insertions(+), 22 deletions(-) diff --git a/java/com/google/devtools/build/android/desugar/CoreLibraryInvocationRewriter.java b/java/com/google/devtools/build/android/desugar/CoreLibraryInvocationRewriter.java index fb62219..0e0610f 100644 --- a/java/com/google/devtools/build/android/desugar/CoreLibraryInvocationRewriter.java +++ b/java/com/google/devtools/build/android/desugar/CoreLibraryInvocationRewriter.java @@ -79,8 +79,7 @@ public class CoreLibraryInvocationRewriter extends ClassVisitor { checkArgument(itf, "Expected interface to rewrite %s.%s : %s", owner, name, desc); owner = InterfaceDesugaring.getCompanionClassName(coreInterfaceName); } else { - // TODO(kmb): Simulate dynamic dispatch instead of calling most general default method - owner = InterfaceDesugaring.getCompanionClassName(coreInterfaceName); + owner = coreInterfaceName + "$$Dispatch"; } opcode = Opcodes.INVOKESTATIC; diff --git a/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java b/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java index 90e6bc0..c73874e 100644 --- a/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java +++ b/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java @@ -20,12 +20,16 @@ import com.google.common.base.Splitter; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import java.lang.reflect.Method; +import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Objects; import java.util.Set; import javax.annotation.Nullable; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; @@ -34,6 +38,8 @@ import org.objectweb.asm.Type; */ class CoreLibrarySupport { + private static final Object[] EMPTY_FRAME = new Object[0]; + private final CoreLibraryRewriter rewriter; private final ClassLoader targetLoader; /** Internal name prefixes that we want to move to a custom package. */ @@ -42,15 +48,20 @@ class CoreLibrarySupport { private final ImmutableSet> emulatedInterfaces; /** Map from {@code owner#name} core library members to their new owners. */ private final ImmutableMap memberMoves; + private final GeneratedClassStore store; + + private final HashMap dispatchHelpers = new HashMap<>(); public CoreLibrarySupport( CoreLibraryRewriter rewriter, ClassLoader targetLoader, + GeneratedClassStore store, List renamedPrefixes, List emulatedInterfaces, List memberMoves) { this.rewriter = rewriter; this.targetLoader = targetLoader; + this.store = store; checkArgument( renamedPrefixes.stream().allMatch(prefix -> prefix.startsWith("java/")), renamedPrefixes); this.renamedPrefixes = ImmutableSet.copyOf(renamedPrefixes); @@ -88,8 +99,7 @@ class CoreLibrarySupport { } // Rename any classes desugar might generate under java/ (for emulated interfaces) as well as // configured prefixes - return unprefixedName.contains("$$Lambda$") - || unprefixedName.endsWith("$$CC") + return looksGenerated(unprefixedName) || renamedPrefixes.stream().anyMatch(prefix -> unprefixedName.startsWith(prefix)); } @@ -113,6 +123,78 @@ class CoreLibrarySupport { return getEmulatedCoreClassOrInterface(internalName) != null; } + /** Includes the given method definition in any applicable core interface emulation logic. */ + public void registerIfEmulatedCoreInterface( + int access, + String owner, + String name, + String desc, + String[] exceptions) { + Class emulated = getEmulatedCoreClassOrInterface(owner); + if (emulated == null) { + return; + } + checkArgument(emulated.isInterface(), "Shouldn't be called for a class: %s.%s", owner, name); + checkArgument( + BitFlags.noneSet( + access, + Opcodes.ACC_ABSTRACT | Opcodes.ACC_NATIVE | Opcodes.ACC_STATIC | Opcodes.ACC_BRIDGE), + "Should only be called for default methods: %s.%s", owner, name); + + ClassVisitor helper = dispatchHelper(owner); + String companionDesc = InterfaceDesugaring.companionDefaultMethodDescriptor(owner, desc); + MethodVisitor dispatchMethod = + helper.visitMethod( + access | Opcodes.ACC_STATIC, + name, + companionDesc, + /*signature=*/ null, // signature is invalid due to extra "receiver" argument + exceptions); + + dispatchMethod.visitCode(); + { + // See if the receiver might come with its own implementation of the method, and call it. + // We do this by testing for the interface type created by EmulatedInterfaceRewriter + Label callCompanion = new Label(); + String emulationInterface = renameCoreLibrary(owner); + dispatchMethod.visitVarInsn(Opcodes.ALOAD, 0); // load "receiver" + dispatchMethod.visitTypeInsn(Opcodes.INSTANCEOF, emulationInterface); + dispatchMethod.visitJumpInsn(Opcodes.IFEQ, callCompanion); + dispatchMethod.visitVarInsn(Opcodes.ALOAD, 0); // load "receiver" + dispatchMethod.visitTypeInsn(Opcodes.CHECKCAST, emulationInterface); + + Type neededType = Type.getMethodType(desc); + visitLoadArgs(dispatchMethod, neededType, 1 /* receiver already loaded above*/); + dispatchMethod.visitMethodInsn( + Opcodes.INVOKEINTERFACE, + emulationInterface, + name, + desc, + /*itf=*/ true); + dispatchMethod.visitInsn(neededType.getReturnType().getOpcode(Opcodes.IRETURN)); + + dispatchMethod.visitLabel(callCompanion); + // Trivial frame for the branch target: same empty stack as before + dispatchMethod.visitFrame(Opcodes.F_SAME, 0, EMPTY_FRAME, 0, EMPTY_FRAME); + } + + // Call static type's default implementation in companion class + Type neededType = Type.getMethodType(companionDesc); + visitLoadArgs(dispatchMethod, neededType, 0); + // TODO(b/70681189): Also test emulated subtypes and call their implementations before falling + // back on static type's default implementation + dispatchMethod.visitMethodInsn( + Opcodes.INVOKESTATIC, + InterfaceDesugaring.getCompanionClassName(owner), + name, + companionDesc, + /*itf=*/ false); + dispatchMethod.visitInsn(neededType.getReturnType().getOpcode(Opcodes.IRETURN)); + + dispatchMethod.visitMaxs(0, 0); + dispatchMethod.visitEnd(); + } + /** * If the given invocation needs to go through a companion class of an emulated or renamed * core interface, this methods returns that interface. This is a helper method for @@ -124,7 +206,7 @@ class CoreLibrarySupport { @Nullable public Class getCoreInterfaceRewritingTarget( int opcode, String owner, String name, String desc, boolean itf) { - if (owner.contains("$$Lambda$") || owner.endsWith("$$CC")) { + if (looksGenerated(owner)) { // Regular desugaring handles generated classes, no emulation is needed return null; } @@ -188,8 +270,13 @@ class CoreLibrarySupport { return null; } - private Class getEmulatedCoreClassOrInterface(String internalName) { - if (internalName.contains("$$Lambda$") || internalName.endsWith("$$CC")) { + /** + * Returns the given class if it's a core library class or interface with emulated default + * methods. This is equivalent to calling {@link #isEmulatedCoreClassOrInterface} and then + * just loading the class (using the target class loader). + */ + public Class getEmulatedCoreClassOrInterface(String internalName) { + if (looksGenerated(internalName)) { // Regular desugaring handles generated classes, no emulation is needed return null; } @@ -215,6 +302,22 @@ class CoreLibrarySupport { } } + private ClassVisitor dispatchHelper(String internalName) { + return dispatchHelpers.computeIfAbsent(internalName, className -> { + className += "$$Dispatch"; + ClassVisitor result = store.add(className); + result.visit( + Opcodes.V1_7, + // Must be public so dispatch methods can be called from anywhere + Opcodes.ACC_SYNTHETIC | Opcodes.ACC_PUBLIC, + className, + /*signature=*/ null, + "java/lang/Object", + new String[0]); + return result; + }); + } + private static Method findInterfaceMethod(Class clazz, String name, String desc) { return collectImplementedInterfaces(clazz, new LinkedHashSet<>()) .stream() @@ -249,4 +352,20 @@ class CoreLibrarySupport { } return dest; } + + /** + * Emits instructions to load a method's parameters as arguments of a method call assumed to have + * compatible descriptor, starting at the given local variable slot. + */ + private static void visitLoadArgs(MethodVisitor dispatchMethod, Type neededType, int slot) { + for (Type arg : neededType.getArgumentTypes()) { + dispatchMethod.visitVarInsn(arg.getOpcode(Opcodes.ILOAD), slot); + slot += arg.getSize(); + } + } + + /** Checks whether the given class is (likely) generated by desugar itself. */ + private static boolean looksGenerated(String owner) { + return owner.contains("$$Lambda$") || owner.endsWith("$$CC") || owner.endsWith("$$Dispatch"); + } } diff --git a/java/com/google/devtools/build/android/desugar/Desugar.java b/java/com/google/devtools/build/android/desugar/Desugar.java index 0fad8c2..dd1992a 100644 --- a/java/com/google/devtools/build/android/desugar/Desugar.java +++ b/java/com/google/devtools/build/android/desugar/Desugar.java @@ -406,6 +406,7 @@ class Desugar { ? new CoreLibrarySupport( rewriter, loader, + store, options.rewriteCoreLibraryPrefixes, options.emulateCoreLibraryInterfaces, options.retargetCoreLibraryMembers) @@ -719,6 +720,7 @@ class Desugar { visitor, interfaceCache, depsCollector, + coreLibrarySupport, bootclasspathReader, loader, store, @@ -800,6 +802,7 @@ class Desugar { visitor, interfaceCache, depsCollector, + coreLibrarySupport, bootclasspathReader, loader, store, diff --git a/java/com/google/devtools/build/android/desugar/EmulatedInterfaceRewriter.java b/java/com/google/devtools/build/android/desugar/EmulatedInterfaceRewriter.java index d3e786d..f066f2a 100644 --- a/java/com/google/devtools/build/android/desugar/EmulatedInterfaceRewriter.java +++ b/java/com/google/devtools/build/android/desugar/EmulatedInterfaceRewriter.java @@ -13,18 +13,21 @@ // limitations under the License. package com.google.devtools.build.android.desugar; -import java.util.ArrayList; import java.util.Collections; +import java.util.LinkedHashSet; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.Opcodes; /** * Visitor that renames emulated interfaces and marks classes that extend emulated interfaces to * also implement the renamed interfaces. {@link DefaultMethodClassFixer} makes sure the requisite - * methods are present. Doing this helps with dynamic dispatch on emulated interfaces. + * methods are present in all classes implementing the renamed interface. Doing this helps with + * dynamic dispatch on emulated interfaces. */ public class EmulatedInterfaceRewriter extends ClassVisitor { + private static final String[] EMPTY_ARRAY = new String[0]; + private final CoreLibrarySupport support; public EmulatedInterfaceRewriter(ClassVisitor dest, CoreLibrarySupport support) { @@ -40,24 +43,47 @@ public class EmulatedInterfaceRewriter extends ClassVisitor { String signature, String superName, String[] interfaces) { - boolean isEmulated = support.isEmulatedCoreClassOrInterface(name); - if (interfaces != null && interfaces.length > 0 && !isEmulated) { - // Make classes implementing emulated interfaces also implement the renamed interfaces we - // create below. - ArrayList newInterfaces = new ArrayList<>(interfaces.length + 2); - Collections.addAll(newInterfaces, interfaces); - for (String itf : interfaces) { - if (support.isEmulatedCoreClassOrInterface(itf)) { - newInterfaces.add(support.renameCoreLibrary(itf)); + boolean emulated = support.isEmulatedCoreClassOrInterface(name); + { + // 1. see if we should implement any additional interfaces. + // Use LinkedHashSet to dedupe but maintain deterministic order + LinkedHashSet newInterfaces = new LinkedHashSet<>(); + if (interfaces != null && interfaces.length > 0) { + // Make classes implementing emulated interfaces also implement the renamed interfaces we + // create below. This includes making the renamed interfaces extends each other as needed. + Collections.addAll(newInterfaces, interfaces); + for (String itf : interfaces) { + if (support.isEmulatedCoreClassOrInterface(itf)) { + newInterfaces.add(support.renameCoreLibrary(itf)); + } + } + } + if (!emulated) { + // For an immediate subclass of an emulated class, also fill in any interfaces implemented + // by superclasses, similar to the additional default method stubbing performed in + // DefaultMethodClassFixer in this situation. + Class superclass = support.getEmulatedCoreClassOrInterface(superName); + while (superclass != null) { + for (Class implemented : superclass.getInterfaces()) { + String itf = implemented.getName().replace('.', '/'); + if (support.isEmulatedCoreClassOrInterface(itf)) { + newInterfaces.add(support.renameCoreLibrary(itf)); + } + } + superclass = superclass.getSuperclass(); } } - if (interfaces.length != newInterfaces.size()) { - interfaces = newInterfaces.toArray(interfaces); - signature = null; // additional interfaces invalidate signature + // Update implemented interfaces and signature if we did anything above + if (interfaces == null + ? !newInterfaces.isEmpty() + : interfaces.length != newInterfaces.size()) { + interfaces = newInterfaces.toArray(EMPTY_ARRAY); + signature = null; // additional interfaces invalidate any signature } } - if (BitFlags.isInterface(access) && isEmulated) { + // 2. see if we need to rename this interface itself + if (BitFlags.isInterface(access) && emulated) { name = support.renameCoreLibrary(name); } super.visit(version, access, name, signature, superName, interfaces); diff --git a/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java b/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java index f17f114..0a10df1 100644 --- a/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java +++ b/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java @@ -47,6 +47,7 @@ class InterfaceDesugaring extends ClassVisitor { private final ClassVsInterface interfaceCache; private final DependencyCollector depsCollector; + private final CoreLibrarySupport coreLibrarySupport; private final ClassReaderFactory bootclasspath; private final ClassLoader targetLoader; private final GeneratedClassStore store; @@ -63,6 +64,7 @@ class InterfaceDesugaring extends ClassVisitor { ClassVisitor dest, ClassVsInterface interfaceCache, DependencyCollector depsCollector, + @Nullable CoreLibrarySupport coreLibrarySupport, ClassReaderFactory bootclasspath, ClassLoader targetLoader, GeneratedClassStore store, @@ -70,6 +72,7 @@ class InterfaceDesugaring extends ClassVisitor { super(Opcodes.ASM6, dest); this.interfaceCache = interfaceCache; this.depsCollector = depsCollector; + this.coreLibrarySupport = coreLibrarySupport; this.bootclasspath = bootclasspath; this.targetLoader = targetLoader; this.store = store; @@ -214,6 +217,10 @@ class InterfaceDesugaring extends ClassVisitor { internalName, desc); ++numberOfDefaultMethods; + if (coreLibrarySupport != null) { + coreLibrarySupport.registerIfEmulatedCoreInterface( + access, internalName, name, desc, exceptions); + } abstractDest = super.visitMethod(access | Opcodes.ACC_ABSTRACT, name, desc, signature, exceptions); } diff --git a/test/java/com/google/devtools/build/android/desugar/CoreLibrarySupportTest.java b/test/java/com/google/devtools/build/android/desugar/CoreLibrarySupportTest.java index cc17748..90350ce 100644 --- a/test/java/com/google/devtools/build/android/desugar/CoreLibrarySupportTest.java +++ b/test/java/com/google/devtools/build/android/desugar/CoreLibrarySupportTest.java @@ -34,6 +34,7 @@ public class CoreLibrarySupportTest { new CoreLibrarySupport( new CoreLibraryRewriter(""), null, + null, ImmutableList.of("java/time/"), ImmutableList.of(), ImmutableList.of()); @@ -51,6 +52,7 @@ public class CoreLibrarySupportTest { new CoreLibrarySupport( new CoreLibraryRewriter("__/"), null, + null, ImmutableList.of("java/time/"), ImmutableList.of(), ImmutableList.of()); @@ -67,6 +69,7 @@ public class CoreLibrarySupportTest { new CoreLibrarySupport( new CoreLibraryRewriter(""), null, + null, ImmutableList.of(), ImmutableList.of(), ImmutableList.of()); @@ -80,6 +83,7 @@ public class CoreLibrarySupportTest { new CoreLibrarySupport( new CoreLibraryRewriter("__/"), null, + null, ImmutableList.of(), ImmutableList.of(), ImmutableList.of()); @@ -93,6 +97,7 @@ public class CoreLibrarySupportTest { new CoreLibrarySupport( new CoreLibraryRewriter("__/"), null, + null, ImmutableList.of("java/util/Helper"), ImmutableList.of(), ImmutableList.of("java/util/Existing#match -> java/util/Helper")); @@ -108,6 +113,7 @@ public class CoreLibrarySupportTest { new CoreLibrarySupport( new CoreLibraryRewriter(""), Thread.currentThread().getContextClassLoader(), + null, ImmutableList.of("java/util/concurrent/"), ImmutableList.of("java/util/Map"), ImmutableList.of()); @@ -126,6 +132,7 @@ public class CoreLibrarySupportTest { new CoreLibrarySupport( new CoreLibraryRewriter(""), Thread.currentThread().getContextClassLoader(), + null, ImmutableList.of(), ImmutableList.of("java/util/Collection"), ImmutableList.of()); @@ -161,6 +168,7 @@ public class CoreLibrarySupportTest { new CoreLibrarySupport( new CoreLibraryRewriter(""), Thread.currentThread().getContextClassLoader(), + null, ImmutableList.of(), ImmutableList.of("java/util/Collection"), ImmutableList.of()); @@ -188,6 +196,7 @@ public class CoreLibrarySupportTest { new CoreLibrarySupport( new CoreLibraryRewriter(""), Thread.currentThread().getContextClassLoader(), + null, ImmutableList.of(), ImmutableList.of("java/util/Map"), ImmutableList.of()); @@ -223,6 +232,7 @@ public class CoreLibrarySupportTest { new CoreLibrarySupport( new CoreLibraryRewriter(""), Thread.currentThread().getContextClassLoader(), + null, ImmutableList.of(), ImmutableList.of("java/util/Comparator"), ImmutableList.of()); @@ -246,6 +256,7 @@ public class CoreLibrarySupportTest { new CoreLibrarySupport( new CoreLibraryRewriter(""), Thread.currentThread().getContextClassLoader(), + null, ImmutableList.of("java/util/"), ImmutableList.of(), ImmutableList.of()); @@ -314,6 +325,7 @@ public class CoreLibrarySupportTest { new CoreLibrarySupport( new CoreLibraryRewriter(""), Thread.currentThread().getContextClassLoader(), + null, ImmutableList.of("java/util/concurrent/"), // should return null for these ImmutableList.of("java/util/Map"), ImmutableList.of()); diff --git a/test/java/com/google/devtools/build/android/desugar/CorePackageRenamerTest.java b/test/java/com/google/devtools/build/android/desugar/CorePackageRenamerTest.java index 0ff0f25..d998aa2 100644 --- a/test/java/com/google/devtools/build/android/desugar/CorePackageRenamerTest.java +++ b/test/java/com/google/devtools/build/android/desugar/CorePackageRenamerTest.java @@ -35,6 +35,7 @@ public class CorePackageRenamerTest { new CoreLibrarySupport( new CoreLibraryRewriter(""), null, + null, ImmutableList.of("java/time/"), ImmutableList.of(), ImmutableList.of("java/util/A#m->java/time/B"))); -- cgit v1.2.3 From 63cde3a65d10f7f94460547042566f940d5453f0 Mon Sep 17 00:00:00 2001 From: kmb Date: Thu, 1 Mar 2018 16:26:21 -0800 Subject: Android desugar config options to exclude methods from interface emulation RELNOTES: None. PiperOrigin-RevId: 187551970 GitOrigin-RevId: f090082d62c3ea779d2dd33eb0fd7355b0ee9456 Change-Id: Id9ff715440eace84432ae6c5b88f7daaa43f36db --- .../build/android/desugar/CoreLibrarySupport.java | 17 ++++++++- .../devtools/build/android/desugar/Desugar.java | 16 +++++++- .../android/desugar/CoreLibrarySupportTest.java | 44 +++++++++++++++++++++- .../android/desugar/CorePackageRenamerTest.java | 3 +- 4 files changed, 75 insertions(+), 5 deletions(-) diff --git a/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java b/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java index c73874e..e22c596 100644 --- a/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java +++ b/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java @@ -44,6 +44,7 @@ class CoreLibrarySupport { private final ClassLoader targetLoader; /** Internal name prefixes that we want to move to a custom package. */ private final ImmutableSet renamedPrefixes; + private final ImmutableSet excludeFromEmulation; /** Internal names of interfaces whose default and static interface methods we'll emulate. */ private final ImmutableSet> emulatedInterfaces; /** Map from {@code owner#name} core library members to their new owners. */ @@ -58,13 +59,16 @@ class CoreLibrarySupport { GeneratedClassStore store, List renamedPrefixes, List emulatedInterfaces, - List memberMoves) { + List memberMoves, + List excludeFromEmulation) { this.rewriter = rewriter; this.targetLoader = targetLoader; this.store = store; checkArgument( renamedPrefixes.stream().allMatch(prefix -> prefix.startsWith("java/")), renamedPrefixes); this.renamedPrefixes = ImmutableSet.copyOf(renamedPrefixes); + this.excludeFromEmulation = ImmutableSet.copyOf(excludeFromEmulation); + ImmutableSet.Builder> classBuilder = ImmutableSet.builder(); for (String itf : emulatedInterfaces) { checkArgument(itf.startsWith("java/util/"), itf); @@ -86,6 +90,8 @@ class CoreLibrarySupport { checkArgument(!isRenamedCoreLibrary(pair.get(0).substring(0, sep)), "Original renamed, no need to move it: %s", move); checkArgument(isRenamedCoreLibrary(pair.get(1)), "Target not renamed: %s", move); + checkArgument(!this.excludeFromEmulation.contains(pair.get(0)), + "Retargeted invocation %s shouldn't overlap with excluded", move); movesBuilder.put(pair.get(0), renameCoreLibrary(pair.get(1))); } @@ -245,6 +251,9 @@ class CoreLibrarySupport { // we can only get here if its a default method, and invokestatic we handled above. Method callee = findInterfaceMethod(clazz, name, desc); if (callee != null && callee.isDefault()) { + if (isExcluded(callee)) { + return null; + } Class result = callee.getDeclaringClass(); if (isRenamedCoreLibrary(result.getName().replace('.', '/')) || emulatedInterfaces.stream().anyMatch(emulated -> emulated.isAssignableFrom(result))) { @@ -294,6 +303,12 @@ class CoreLibrarySupport { return null; } + private boolean isExcluded(Method method) { + String unprefixedOwner = + rewriter.unprefix(method.getDeclaringClass().getName().replace('.', '/')); + return excludeFromEmulation.contains(unprefixedOwner + "#" + method.getName()); + } + private Class loadFromInternal(String internalName) { try { return targetLoader.loadClass(internalName.replace('/', '.')); diff --git a/java/com/google/devtools/build/android/desugar/Desugar.java b/java/com/google/devtools/build/android/desugar/Desugar.java index dd1992a..8b8635b 100644 --- a/java/com/google/devtools/build/android/desugar/Desugar.java +++ b/java/com/google/devtools/build/android/desugar/Desugar.java @@ -292,6 +292,17 @@ class Desugar { ) public List retargetCoreLibraryMembers; + /** Members not to rewrite. */ + @Option( + name = "dont_rewrite_core_library_invocation", + defaultValue = "", // ignored + allowMultiple = true, + documentationCategory = OptionDocumentationCategory.UNDOCUMENTED, + effectTags = {OptionEffectTag.UNKNOWN}, + help = "Method invocations not to rewrite, given as \"class/Name#method\"." + ) + public List dontTouchCoreLibraryMembers; + /** Set to work around b/62623509 with JaCoCo versions prior to 0.7.9. */ // TODO(kmb): Remove when Android Studio doesn't need it anymore (see b/37116789) @Option( @@ -409,8 +420,9 @@ class Desugar { store, options.rewriteCoreLibraryPrefixes, options.emulateCoreLibraryInterfaces, - options.retargetCoreLibraryMembers) - : null; + options.retargetCoreLibraryMembers, + options.dontTouchCoreLibraryMembers) + : null; desugarClassesInInput( inputFiles, diff --git a/test/java/com/google/devtools/build/android/desugar/CoreLibrarySupportTest.java b/test/java/com/google/devtools/build/android/desugar/CoreLibrarySupportTest.java index 90350ce..e6f34ba 100644 --- a/test/java/com/google/devtools/build/android/desugar/CoreLibrarySupportTest.java +++ b/test/java/com/google/devtools/build/android/desugar/CoreLibrarySupportTest.java @@ -37,6 +37,7 @@ public class CoreLibrarySupportTest { null, ImmutableList.of("java/time/"), ImmutableList.of(), + ImmutableList.of(), ImmutableList.of()); assertThat(support.isRenamedCoreLibrary("java/time/X")).isTrue(); assertThat(support.isRenamedCoreLibrary("java/time/y/X")).isTrue(); @@ -55,6 +56,7 @@ public class CoreLibrarySupportTest { null, ImmutableList.of("java/time/"), ImmutableList.of(), + ImmutableList.of(), ImmutableList.of()); assertThat(support.isRenamedCoreLibrary("__/java/time/X")).isTrue(); assertThat(support.isRenamedCoreLibrary("__/java/time/y/X")).isTrue(); @@ -63,6 +65,7 @@ public class CoreLibrarySupportTest { assertThat(support.isRenamedCoreLibrary("__/java/io/X$$Lambda$17")).isTrue(); assertThat(support.isRenamedCoreLibrary("com/google/X")).isFalse(); } + @Test public void testRenameCoreLibrary() throws Exception { CoreLibrarySupport support = @@ -72,6 +75,7 @@ public class CoreLibrarySupportTest { null, ImmutableList.of(), ImmutableList.of(), + ImmutableList.of(), ImmutableList.of()); assertThat(support.renameCoreLibrary("java/time/X")).isEqualTo("j$/time/X"); assertThat(support.renameCoreLibrary("com/google/X")).isEqualTo("com/google/X"); @@ -86,6 +90,7 @@ public class CoreLibrarySupportTest { null, ImmutableList.of(), ImmutableList.of(), + ImmutableList.of(), ImmutableList.of()); assertThat(support.renameCoreLibrary("__/java/time/X")).isEqualTo("j$/time/X"); assertThat(support.renameCoreLibrary("com/google/X")).isEqualTo("com/google/X"); @@ -100,7 +105,8 @@ public class CoreLibrarySupportTest { null, ImmutableList.of("java/util/Helper"), ImmutableList.of(), - ImmutableList.of("java/util/Existing#match -> java/util/Helper")); + ImmutableList.of("java/util/Existing#match -> java/util/Helper"), + ImmutableList.of()); assertThat(support.getMoveTarget("__/java/util/Existing", "match")).isEqualTo("j$/util/Helper"); assertThat(support.getMoveTarget("java/util/Existing", "match")).isEqualTo("j$/util/Helper"); assertThat(support.getMoveTarget("__/java/util/Existing", "matchesnot")).isNull(); @@ -116,6 +122,7 @@ public class CoreLibrarySupportTest { null, ImmutableList.of("java/util/concurrent/"), ImmutableList.of("java/util/Map"), + ImmutableList.of(), ImmutableList.of()); assertThat(support.isEmulatedCoreClassOrInterface("java/util/Map")).isTrue(); assertThat(support.isEmulatedCoreClassOrInterface("java/util/Map$$Lambda$17")).isFalse(); @@ -135,6 +142,7 @@ public class CoreLibrarySupportTest { null, ImmutableList.of(), ImmutableList.of("java/util/Collection"), + ImmutableList.of(), ImmutableList.of()); assertThat( support.getCoreInterfaceRewritingTarget( @@ -171,6 +179,7 @@ public class CoreLibrarySupportTest { null, ImmutableList.of(), ImmutableList.of("java/util/Collection"), + ImmutableList.of(), ImmutableList.of()); assertThat( support.getCoreInterfaceRewritingTarget( @@ -199,6 +208,7 @@ public class CoreLibrarySupportTest { null, ImmutableList.of(), ImmutableList.of("java/util/Map"), + ImmutableList.of(), ImmutableList.of()); assertThat( support.getCoreInterfaceRewritingTarget( @@ -235,6 +245,7 @@ public class CoreLibrarySupportTest { null, ImmutableList.of(), ImmutableList.of("java/util/Comparator"), + ImmutableList.of(), ImmutableList.of()); assertThat( support.getCoreInterfaceRewritingTarget( @@ -259,6 +270,7 @@ public class CoreLibrarySupportTest { null, ImmutableList.of("java/util/"), ImmutableList.of(), + ImmutableList.of(), ImmutableList.of()); // regular invocations of default methods: ignored @@ -328,6 +340,7 @@ public class CoreLibrarySupportTest { null, ImmutableList.of("java/util/concurrent/"), // should return null for these ImmutableList.of("java/util/Map"), + ImmutableList.of(), ImmutableList.of()); assertThat( support.getCoreInterfaceRewritingTarget( @@ -346,4 +359,33 @@ public class CoreLibrarySupportTest { true)) .isNull(); } + + @Test + public void testGetCoreInterfaceRewritingTarget_excludedMethodIgnored() throws Exception { + CoreLibrarySupport support = + new CoreLibrarySupport( + new CoreLibraryRewriter(""), + Thread.currentThread().getContextClassLoader(), + null, + ImmutableList.of(), + ImmutableList.of("java/util/Collection"), + ImmutableList.of(), + ImmutableList.of("java/util/Collection#removeIf")); + assertThat( + support.getCoreInterfaceRewritingTarget( + Opcodes.INVOKEINTERFACE, + "java/util/List", + "removeIf", + "(Ljava/util/function/Predicate;)Z", + true)) + .isNull(); + assertThat( + support.getCoreInterfaceRewritingTarget( + Opcodes.INVOKEVIRTUAL, + "java/util/ArrayList", + "removeIf", + "(Ljava/util/function/Predicate;)Z", + false)) + .isNull(); + } } diff --git a/test/java/com/google/devtools/build/android/desugar/CorePackageRenamerTest.java b/test/java/com/google/devtools/build/android/desugar/CorePackageRenamerTest.java index d998aa2..95d7b41 100644 --- a/test/java/com/google/devtools/build/android/desugar/CorePackageRenamerTest.java +++ b/test/java/com/google/devtools/build/android/desugar/CorePackageRenamerTest.java @@ -38,7 +38,8 @@ public class CorePackageRenamerTest { null, ImmutableList.of("java/time/"), ImmutableList.of(), - ImmutableList.of("java/util/A#m->java/time/B"))); + ImmutableList.of("java/util/A#m->java/time/B"), + ImmutableList.of())); MethodVisitor mv = renamer.visitMethod(0, "test", "()V", null, null); mv.visitMethodInsn( -- cgit v1.2.3 From 1c433fd1116c4ca655503e7cffa13679c31f0b99 Mon Sep 17 00:00:00 2001 From: kmb Date: Fri, 2 Mar 2018 14:41:23 -0800 Subject: emulate dynamic dispatch of emulated default interface methods RELNOTES: None. PiperOrigin-RevId: 187671513 GitOrigin-RevId: babbfdc6cb98a23fe0dadf02d7dc407504e9cac5 Change-Id: Ie23b521a82464d07f625cefad8418c502f0978f0 --- .../build/android/desugar/CoreLibrarySupport.java | 225 ++++++++++++++------- .../android/desugar/DefaultMethodClassFixer.java | 1 + .../devtools/build/android/desugar/Desugar.java | 4 +- .../android/desugar/CoreLibrarySupportTest.java | 36 ++-- .../android/desugar/CorePackageRenamerTest.java | 1 - 5 files changed, 179 insertions(+), 88 deletions(-) diff --git a/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java b/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java index e22c596..b90222b 100644 --- a/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java +++ b/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java @@ -16,10 +16,16 @@ package com.google.devtools.build.android.desugar; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkState; +import com.google.auto.value.AutoValue; import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import com.google.common.collect.LinkedHashMultimap; +import com.google.common.collect.Multimap; +import com.google.errorprone.annotations.Immutable; import java.lang.reflect.Method; +import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashSet; @@ -39,6 +45,7 @@ import org.objectweb.asm.Type; class CoreLibrarySupport { private static final Object[] EMPTY_FRAME = new Object[0]; + private static final String[] EMPTY_LIST = new String[0]; private final CoreLibraryRewriter rewriter; private final ClassLoader targetLoader; @@ -49,21 +56,20 @@ class CoreLibrarySupport { private final ImmutableSet> emulatedInterfaces; /** Map from {@code owner#name} core library members to their new owners. */ private final ImmutableMap memberMoves; - private final GeneratedClassStore store; - private final HashMap dispatchHelpers = new HashMap<>(); + /** For the collection of definitions of emulated default methods (deterministic iteration). */ + private final Multimap emulatedDefaultMethods = + LinkedHashMultimap.create(); public CoreLibrarySupport( CoreLibraryRewriter rewriter, ClassLoader targetLoader, - GeneratedClassStore store, List renamedPrefixes, List emulatedInterfaces, List memberMoves, List excludeFromEmulation) { this.rewriter = rewriter; this.targetLoader = targetLoader; - this.store = store; checkArgument( renamedPrefixes.stream().allMatch(prefix -> prefix.startsWith("java/")), renamedPrefixes); this.renamedPrefixes = ImmutableSet.copyOf(renamedPrefixes); @@ -146,59 +152,8 @@ class CoreLibrarySupport { access, Opcodes.ACC_ABSTRACT | Opcodes.ACC_NATIVE | Opcodes.ACC_STATIC | Opcodes.ACC_BRIDGE), "Should only be called for default methods: %s.%s", owner, name); - - ClassVisitor helper = dispatchHelper(owner); - String companionDesc = InterfaceDesugaring.companionDefaultMethodDescriptor(owner, desc); - MethodVisitor dispatchMethod = - helper.visitMethod( - access | Opcodes.ACC_STATIC, - name, - companionDesc, - /*signature=*/ null, // signature is invalid due to extra "receiver" argument - exceptions); - - dispatchMethod.visitCode(); - { - // See if the receiver might come with its own implementation of the method, and call it. - // We do this by testing for the interface type created by EmulatedInterfaceRewriter - Label callCompanion = new Label(); - String emulationInterface = renameCoreLibrary(owner); - dispatchMethod.visitVarInsn(Opcodes.ALOAD, 0); // load "receiver" - dispatchMethod.visitTypeInsn(Opcodes.INSTANCEOF, emulationInterface); - dispatchMethod.visitJumpInsn(Opcodes.IFEQ, callCompanion); - dispatchMethod.visitVarInsn(Opcodes.ALOAD, 0); // load "receiver" - dispatchMethod.visitTypeInsn(Opcodes.CHECKCAST, emulationInterface); - - Type neededType = Type.getMethodType(desc); - visitLoadArgs(dispatchMethod, neededType, 1 /* receiver already loaded above*/); - dispatchMethod.visitMethodInsn( - Opcodes.INVOKEINTERFACE, - emulationInterface, - name, - desc, - /*itf=*/ true); - dispatchMethod.visitInsn(neededType.getReturnType().getOpcode(Opcodes.IRETURN)); - - dispatchMethod.visitLabel(callCompanion); - // Trivial frame for the branch target: same empty stack as before - dispatchMethod.visitFrame(Opcodes.F_SAME, 0, EMPTY_FRAME, 0, EMPTY_FRAME); - } - - // Call static type's default implementation in companion class - Type neededType = Type.getMethodType(companionDesc); - visitLoadArgs(dispatchMethod, neededType, 0); - // TODO(b/70681189): Also test emulated subtypes and call their implementations before falling - // back on static type's default implementation - dispatchMethod.visitMethodInsn( - Opcodes.INVOKESTATIC, - InterfaceDesugaring.getCompanionClassName(owner), - name, - companionDesc, - /*itf=*/ false); - dispatchMethod.visitInsn(neededType.getReturnType().getOpcode(Opcodes.IRETURN)); - - dispatchMethod.visitMaxs(0, 0); - dispatchMethod.visitEnd(); + emulatedDefaultMethods.put( + name + ":" + desc, EmulatedMethod.create(access, emulated, name, desc, exceptions)); } /** @@ -303,6 +258,130 @@ class CoreLibrarySupport { return null; } + public void makeDispatchHelpers(GeneratedClassStore store) { + HashMap, ClassVisitor> dispatchHelpers = new HashMap<>(); + for (Collection group : emulatedDefaultMethods.asMap().values()) { + checkState(!group.isEmpty()); + Class root = group + .stream() + .map(EmulatedMethod::owner) + .max(DefaultMethodClassFixer.InterfaceComparator.INSTANCE) + .get(); + checkState(group.stream().map(m -> m.owner()).allMatch(o -> root.isAssignableFrom(o)), + "Not a single unique method: %s", group); + + for (EmulatedMethod methodDefinition : group) { + Class owner = methodDefinition.owner(); + ClassVisitor dispatchHelper = dispatchHelpers.computeIfAbsent(owner, clazz -> { + String className = clazz.getName().replace('.', '/') + "$$Dispatch"; + ClassVisitor result = store.add(className); + result.visit( + Opcodes.V1_7, + // Must be public so dispatch methods can be called from anywhere + Opcodes.ACC_SYNTHETIC | Opcodes.ACC_PUBLIC, + className, + /*signature=*/ null, + "java/lang/Object", + EMPTY_LIST); + return result; + }); + + // Types to check for before calling methodDefinition's companion, sub- before super-types + ImmutableList> typechecks = + group + .stream() + .map(EmulatedMethod::owner) + .filter(o -> o != owner && owner.isAssignableFrom(o)) + .distinct() // should already be but just in case + .sorted(DefaultMethodClassFixer.InterfaceComparator.INSTANCE) + .collect(ImmutableList.toImmutableList()); + makeDispatchHelperMethod(dispatchHelper, methodDefinition, typechecks); + } + } + } + + private void makeDispatchHelperMethod( + ClassVisitor helper, EmulatedMethod method, ImmutableList> typechecks) { + String owner = method.owner().getName().replace('.', '/'); + Type methodType = Type.getMethodType(method.descriptor()); + String companionDesc = + InterfaceDesugaring.companionDefaultMethodDescriptor(owner, method.descriptor()); + MethodVisitor dispatchMethod = + helper.visitMethod( + method.access() | Opcodes.ACC_STATIC, + method.name(), + companionDesc, + /*signature=*/ null, // signature is invalid due to extra "receiver" argument + method.exceptions().toArray(EMPTY_LIST)); + + + dispatchMethod.visitCode(); + { + // See if the receiver might come with its own implementation of the method, and call it. + // We do this by testing for the interface type created by EmulatedInterfaceRewriter + Label fallthrough = new Label(); + String emulationInterface = renameCoreLibrary(owner); + dispatchMethod.visitVarInsn(Opcodes.ALOAD, 0); // load "receiver" + dispatchMethod.visitTypeInsn(Opcodes.INSTANCEOF, emulationInterface); + dispatchMethod.visitJumpInsn(Opcodes.IFEQ, fallthrough); + dispatchMethod.visitVarInsn(Opcodes.ALOAD, 0); // load "receiver" + dispatchMethod.visitTypeInsn(Opcodes.CHECKCAST, emulationInterface); + + visitLoadArgs(dispatchMethod, methodType, 1 /* receiver already loaded above */); + dispatchMethod.visitMethodInsn( + Opcodes.INVOKEINTERFACE, + emulationInterface, + method.name(), + method.descriptor(), + /*itf=*/ true); + dispatchMethod.visitInsn(methodType.getReturnType().getOpcode(Opcodes.IRETURN)); + + dispatchMethod.visitLabel(fallthrough); + // Trivial frame for the branch target: same empty stack as before + dispatchMethod.visitFrame(Opcodes.F_SAME, 0, EMPTY_FRAME, 0, EMPTY_FRAME); + } + + // Next, check for emulated subtypes and call their companion methods + for (Class tested : typechecks) { + checkState(tested.isInterface(), "Dispatch emulation not supported for classes: %s", tested); + Label fallthrough = new Label(); + String emulatedInterface = tested.getName().replace('.', '/'); + dispatchMethod.visitVarInsn(Opcodes.ALOAD, 0); // load "receiver" + dispatchMethod.visitTypeInsn(Opcodes.INSTANCEOF, emulatedInterface); + dispatchMethod.visitJumpInsn(Opcodes.IFEQ, fallthrough); + dispatchMethod.visitVarInsn(Opcodes.ALOAD, 0); // load "receiver" + dispatchMethod.visitTypeInsn(Opcodes.CHECKCAST, emulatedInterface); // make verifier happy + + visitLoadArgs(dispatchMethod, methodType, 1 /* receiver already loaded above */); + dispatchMethod.visitMethodInsn( + Opcodes.INVOKESTATIC, + InterfaceDesugaring.getCompanionClassName(emulatedInterface), + method.name(), + InterfaceDesugaring.companionDefaultMethodDescriptor( + emulatedInterface, method.descriptor()), + /*itf=*/ false); + dispatchMethod.visitInsn(methodType.getReturnType().getOpcode(Opcodes.IRETURN)); + + dispatchMethod.visitLabel(fallthrough); + // Trivial frame for the branch target: same empty stack as before + dispatchMethod.visitFrame(Opcodes.F_SAME, 0, EMPTY_FRAME, 0, EMPTY_FRAME); + } + + // Call static type's default implementation in companion class + dispatchMethod.visitVarInsn(Opcodes.ALOAD, 0); // load "receiver" + visitLoadArgs(dispatchMethod, methodType, 1 /* receiver already loaded above */); + dispatchMethod.visitMethodInsn( + Opcodes.INVOKESTATIC, + InterfaceDesugaring.getCompanionClassName(owner), + method.name(), + companionDesc, + /*itf=*/ false); + dispatchMethod.visitInsn(methodType.getReturnType().getOpcode(Opcodes.IRETURN)); + + dispatchMethod.visitMaxs(0, 0); + dispatchMethod.visitEnd(); + } + private boolean isExcluded(Method method) { String unprefixedOwner = rewriter.unprefix(method.getDeclaringClass().getName().replace('.', '/')); @@ -317,22 +396,6 @@ class CoreLibrarySupport { } } - private ClassVisitor dispatchHelper(String internalName) { - return dispatchHelpers.computeIfAbsent(internalName, className -> { - className += "$$Dispatch"; - ClassVisitor result = store.add(className); - result.visit( - Opcodes.V1_7, - // Must be public so dispatch methods can be called from anywhere - Opcodes.ACC_SYNTHETIC | Opcodes.ACC_PUBLIC, - className, - /*signature=*/ null, - "java/lang/Object", - new String[0]); - return result; - }); - } - private static Method findInterfaceMethod(Class clazz, String name, String desc) { return collectImplementedInterfaces(clazz, new LinkedHashSet<>()) .stream() @@ -383,4 +446,20 @@ class CoreLibrarySupport { private static boolean looksGenerated(String owner) { return owner.contains("$$Lambda$") || owner.endsWith("$$CC") || owner.endsWith("$$Dispatch"); } + + @AutoValue + @Immutable + abstract static class EmulatedMethod { + public static EmulatedMethod create( + int access, Class owner, String name, String desc, @Nullable String[] exceptions) { + return new AutoValue_CoreLibrarySupport_EmulatedMethod(access, owner, name, desc, + exceptions != null ? ImmutableList.copyOf(exceptions) : ImmutableList.of()); + } + + abstract int access(); + abstract Class owner(); + abstract String name(); + abstract String descriptor(); + abstract ImmutableList exceptions(); + } } diff --git a/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java b/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java index 6143940..292e142 100644 --- a/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java +++ b/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java @@ -649,6 +649,7 @@ public class DefaultMethodClassFixer extends ClassVisitor { /** Comparator for interfaces that compares by whether interfaces extend one another. */ enum InterfaceComparator implements Comparator> { + /** Orders subtypes before supertypes and breaks ties lexicographically. */ INSTANCE; @Override diff --git a/java/com/google/devtools/build/android/desugar/Desugar.java b/java/com/google/devtools/build/android/desugar/Desugar.java index 8b8635b..506a380 100644 --- a/java/com/google/devtools/build/android/desugar/Desugar.java +++ b/java/com/google/devtools/build/android/desugar/Desugar.java @@ -417,7 +417,6 @@ class Desugar { ? new CoreLibrarySupport( rewriter, loader, - store, options.rewriteCoreLibraryPrefixes, options.emulateCoreLibraryInterfaces, options.retargetCoreLibraryMembers, @@ -627,6 +626,9 @@ class Desugar { @Nullable CoreLibrarySupport coreLibrarySupport) throws IOException { // Write out any classes we generated along the way + if (coreLibrarySupport != null) { + coreLibrarySupport.makeDispatchHelpers(store); + } ImmutableMap generatedClasses = store.drain(); checkState( generatedClasses.isEmpty() || (allowDefaultMethods && outputJava7), diff --git a/test/java/com/google/devtools/build/android/desugar/CoreLibrarySupportTest.java b/test/java/com/google/devtools/build/android/desugar/CoreLibrarySupportTest.java index e6f34ba..ec4e16d 100644 --- a/test/java/com/google/devtools/build/android/desugar/CoreLibrarySupportTest.java +++ b/test/java/com/google/devtools/build/android/desugar/CoreLibrarySupportTest.java @@ -34,7 +34,6 @@ public class CoreLibrarySupportTest { new CoreLibrarySupport( new CoreLibraryRewriter(""), null, - null, ImmutableList.of("java/time/"), ImmutableList.of(), ImmutableList.of(), @@ -53,7 +52,6 @@ public class CoreLibrarySupportTest { new CoreLibrarySupport( new CoreLibraryRewriter("__/"), null, - null, ImmutableList.of("java/time/"), ImmutableList.of(), ImmutableList.of(), @@ -72,7 +70,6 @@ public class CoreLibrarySupportTest { new CoreLibrarySupport( new CoreLibraryRewriter(""), null, - null, ImmutableList.of(), ImmutableList.of(), ImmutableList.of(), @@ -87,7 +84,6 @@ public class CoreLibrarySupportTest { new CoreLibrarySupport( new CoreLibraryRewriter("__/"), null, - null, ImmutableList.of(), ImmutableList.of(), ImmutableList.of(), @@ -102,7 +98,6 @@ public class CoreLibrarySupportTest { new CoreLibrarySupport( new CoreLibraryRewriter("__/"), null, - null, ImmutableList.of("java/util/Helper"), ImmutableList.of(), ImmutableList.of("java/util/Existing#match -> java/util/Helper"), @@ -119,7 +114,6 @@ public class CoreLibrarySupportTest { new CoreLibrarySupport( new CoreLibraryRewriter(""), Thread.currentThread().getContextClassLoader(), - null, ImmutableList.of("java/util/concurrent/"), ImmutableList.of("java/util/Map"), ImmutableList.of(), @@ -139,7 +133,6 @@ public class CoreLibrarySupportTest { new CoreLibrarySupport( new CoreLibraryRewriter(""), Thread.currentThread().getContextClassLoader(), - null, ImmutableList.of(), ImmutableList.of("java/util/Collection"), ImmutableList.of(), @@ -176,7 +169,6 @@ public class CoreLibrarySupportTest { new CoreLibrarySupport( new CoreLibraryRewriter(""), Thread.currentThread().getContextClassLoader(), - null, ImmutableList.of(), ImmutableList.of("java/util/Collection"), ImmutableList.of(), @@ -205,7 +197,6 @@ public class CoreLibrarySupportTest { new CoreLibrarySupport( new CoreLibraryRewriter(""), Thread.currentThread().getContextClassLoader(), - null, ImmutableList.of(), ImmutableList.of("java/util/Map"), ImmutableList.of(), @@ -242,7 +233,6 @@ public class CoreLibrarySupportTest { new CoreLibrarySupport( new CoreLibraryRewriter(""), Thread.currentThread().getContextClassLoader(), - null, ImmutableList.of(), ImmutableList.of("java/util/Comparator"), ImmutableList.of(), @@ -267,7 +257,6 @@ public class CoreLibrarySupportTest { new CoreLibrarySupport( new CoreLibraryRewriter(""), Thread.currentThread().getContextClassLoader(), - null, ImmutableList.of("java/util/"), ImmutableList.of(), ImmutableList.of(), @@ -337,7 +326,6 @@ public class CoreLibrarySupportTest { new CoreLibrarySupport( new CoreLibraryRewriter(""), Thread.currentThread().getContextClassLoader(), - null, ImmutableList.of("java/util/concurrent/"), // should return null for these ImmutableList.of("java/util/Map"), ImmutableList.of(), @@ -366,7 +354,6 @@ public class CoreLibrarySupportTest { new CoreLibrarySupport( new CoreLibraryRewriter(""), Thread.currentThread().getContextClassLoader(), - null, ImmutableList.of(), ImmutableList.of("java/util/Collection"), ImmutableList.of(), @@ -388,4 +375,27 @@ public class CoreLibrarySupportTest { false)) .isNull(); } + + @Test + public void testEmulatedMethod_nullExceptions() throws Exception { + CoreLibrarySupport.EmulatedMethod m = + CoreLibrarySupport.EmulatedMethod.create(1, Number.class, "a", "()V", null); + assertThat(m.access()).isEqualTo(1); + assertThat(m.owner()).isEqualTo(Number.class); + assertThat(m.name()).isEqualTo("a"); + assertThat(m.descriptor()).isEqualTo("()V"); + assertThat(m.exceptions()).isEmpty(); + } + + @Test + public void testEmulatedMethod_givenExceptions() throws Exception { + CoreLibrarySupport.EmulatedMethod m = + CoreLibrarySupport.EmulatedMethod.create( + 1, Number.class, "a", "()V", new String[] {"b", "c"}); + assertThat(m.access()).isEqualTo(1); + assertThat(m.owner()).isEqualTo(Number.class); + assertThat(m.name()).isEqualTo("a"); + assertThat(m.descriptor()).isEqualTo("()V"); + assertThat(m.exceptions()).containsExactly("b", "c").inOrder(); + } } diff --git a/test/java/com/google/devtools/build/android/desugar/CorePackageRenamerTest.java b/test/java/com/google/devtools/build/android/desugar/CorePackageRenamerTest.java index 95d7b41..2bdd58b 100644 --- a/test/java/com/google/devtools/build/android/desugar/CorePackageRenamerTest.java +++ b/test/java/com/google/devtools/build/android/desugar/CorePackageRenamerTest.java @@ -35,7 +35,6 @@ public class CorePackageRenamerTest { new CoreLibrarySupport( new CoreLibraryRewriter(""), null, - null, ImmutableList.of("java/time/"), ImmutableList.of(), ImmutableList.of("java/util/A#m->java/time/B"), -- cgit v1.2.3 From 4c2556fe7673b48678009f74385a0ec8fe94b4ca Mon Sep 17 00:00:00 2001 From: Laszlo Csomor Date: Thu, 8 Mar 2018 04:27:11 -0800 Subject: tests,windows: enable android.desugar.runtime Add the c.g.d.build.android.desugar.runtime tests to the transitive closure of //src:all_windows_tests, thus running them on CI. See https://github.com/bazelbuild/bazel/issues/4292 Closes #4796. PiperOrigin-RevId: 188312286 GitOrigin-RevId: 63f6e2293fe8e679732d3d180afc0e781ae40241 Change-Id: I0975c9291a5a043d562242e65e6ad5557b958d36 --- .../desugar/runtime/ThrowableExtensionTestUtility.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/test/java/com/google/devtools/build/android/desugar/runtime/ThrowableExtensionTestUtility.java b/test/java/com/google/devtools/build/android/desugar/runtime/ThrowableExtensionTestUtility.java index b65b8bd..489bd7a 100644 --- a/test/java/com/google/devtools/build/android/desugar/runtime/ThrowableExtensionTestUtility.java +++ b/test/java/com/google/devtools/build/android/desugar/runtime/ThrowableExtensionTestUtility.java @@ -27,7 +27,7 @@ public class ThrowableExtensionTestUtility { private static final String SYSTEM_PROPERTY_EXPECTED_STRATEGY = "expected.strategy"; public static String getTwrStrategyClassNameSpecifiedInSystemProperty() { - String className = System.getProperty(SYSTEM_PROPERTY_EXPECTED_STRATEGY); + String className = unquote(System.getProperty(SYSTEM_PROPERTY_EXPECTED_STRATEGY)); assertThat(className).isNotEmpty(); return className; } @@ -61,4 +61,13 @@ public class ThrowableExtensionTestUtility { public static boolean isReuseStrategy() { return isStrategyOfClass(THROWABLE_EXTENSION_CLASS_NAME + "$ReuseDesugaringStrategy"); } + + private static String unquote(String s) { + if (s.startsWith("'") || s.startsWith("\"")) { + assertThat(s).endsWith(s.substring(0, 1)); + return s.substring(1, s.length() - 1); + } else { + return s; + } + } } -- cgit v1.2.3 From 2042d8d9fe7d52111447ac07e45c161d48cb17d7 Mon Sep 17 00:00:00 2001 From: kmb Date: Mon, 12 Mar 2018 12:20:48 -0700 Subject: Support custom implementations of emulated core interface methods RELNOTES: None. PiperOrigin-RevId: 188760099 GitOrigin-RevId: bff3472e4013c053e452fad7948ad68c5cbd5692 Change-Id: I6fe0153afa5bb57d27da9ca43f2a6796c8907e95 --- .../desugar/CoreLibraryInvocationRewriter.java | 24 ++++----- .../build/android/desugar/CoreLibrarySupport.java | 57 ++++++++++++++++------ .../android/desugar/DefaultMethodClassFixer.java | 9 ++-- .../desugar/DefaultMethodClassFixerTest.java | 2 +- 4 files changed, 59 insertions(+), 33 deletions(-) diff --git a/java/com/google/devtools/build/android/desugar/CoreLibraryInvocationRewriter.java b/java/com/google/devtools/build/android/desugar/CoreLibraryInvocationRewriter.java index 0e0610f..77db915 100644 --- a/java/com/google/devtools/build/android/desugar/CoreLibraryInvocationRewriter.java +++ b/java/com/google/devtools/build/android/desugar/CoreLibraryInvocationRewriter.java @@ -52,19 +52,8 @@ public class CoreLibraryInvocationRewriter extends ClassVisitor { public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) { Class coreInterface = support.getCoreInterfaceRewritingTarget(opcode, owner, name, desc, itf); - String newOwner = support.getMoveTarget(owner, name); - if (newOwner != null) { - checkState(coreInterface == null, - "Can't move and use companion: %s.%s : %s", owner, name, desc); - if (opcode != Opcodes.INVOKESTATIC) { - // assuming a static method - desc = InterfaceDesugaring.companionDefaultMethodDescriptor(owner, desc); - opcode = Opcodes.INVOKESTATIC; - } - owner = newOwner; - itf = false; // assuming a class - } else if (coreInterface != null) { + if (coreInterface != null) { String coreInterfaceName = coreInterface.getName().replace('.', '/'); name = InterfaceDesugaring.normalizeInterfaceMethodName( @@ -84,6 +73,17 @@ public class CoreLibraryInvocationRewriter extends ClassVisitor { opcode = Opcodes.INVOKESTATIC; itf = false; + } else { + String newOwner = support.getMoveTarget(owner, name); + if (newOwner != null) { + if (opcode != Opcodes.INVOKESTATIC) { + // assuming a static method + desc = InterfaceDesugaring.companionDefaultMethodDescriptor(owner, desc); + opcode = Opcodes.INVOKESTATIC; + } + owner = newOwner; + itf = false; // assuming a class + } } super.visitMethodInsn(opcode, owner, name, desc, itf); } diff --git a/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java b/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java index b90222b..da23c12 100644 --- a/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java +++ b/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java @@ -14,7 +14,9 @@ package com.google.devtools.build.android.desugar; import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; +import static java.util.stream.Stream.concat; import com.google.auto.value.AutoValue; import com.google.common.base.Splitter; @@ -265,10 +267,13 @@ class CoreLibrarySupport { Class root = group .stream() .map(EmulatedMethod::owner) - .max(DefaultMethodClassFixer.InterfaceComparator.INSTANCE) + .max(DefaultMethodClassFixer.SubtypeComparator.INSTANCE) .get(); checkState(group.stream().map(m -> m.owner()).allMatch(o -> root.isAssignableFrom(o)), "Not a single unique method: %s", group); + String methodName = group.stream().findAny().get().name(); + + ImmutableList> customOverrides = findCustomOverrides(root, methodName); for (EmulatedMethod methodDefinition : group) { Class owner = methodDefinition.owner(); @@ -288,20 +293,39 @@ class CoreLibrarySupport { // Types to check for before calling methodDefinition's companion, sub- before super-types ImmutableList> typechecks = - group - .stream() - .map(EmulatedMethod::owner) + concat(group.stream().map(EmulatedMethod::owner), customOverrides.stream()) .filter(o -> o != owner && owner.isAssignableFrom(o)) - .distinct() // should already be but just in case - .sorted(DefaultMethodClassFixer.InterfaceComparator.INSTANCE) + .distinct() // should already be but just in case + .sorted(DefaultMethodClassFixer.SubtypeComparator.INSTANCE) .collect(ImmutableList.toImmutableList()); makeDispatchHelperMethod(dispatchHelper, methodDefinition, typechecks); } } } + private ImmutableList> findCustomOverrides(Class root, String methodName) { + ImmutableList.Builder> customOverrides = ImmutableList.builder(); + for (ImmutableMap.Entry move : memberMoves.entrySet()) { + // move.getKey is a string # which we validated in the constructor. + // We need to take the string apart here to compare owner and name separately. + if (!methodName.equals(move.getKey().substring(move.getKey().indexOf('#') + 1))) { + continue; + } + Class target = + loadFromInternal( + rewriter.getPrefix() + move.getKey().substring(0, move.getKey().indexOf('#'))); + if (!root.isAssignableFrom(target)) { + continue; + } + checkState(!target.isInterface(), "can't move emulated interface method: %s", move); + customOverrides.add(target); + } + return customOverrides.build(); + } + private void makeDispatchHelperMethod( ClassVisitor helper, EmulatedMethod method, ImmutableList> typechecks) { + checkArgument(method.owner().isInterface()); String owner = method.owner().getName().replace('.', '/'); Type methodType = Type.getMethodType(method.descriptor()); String companionDesc = @@ -341,24 +365,27 @@ class CoreLibrarySupport { dispatchMethod.visitFrame(Opcodes.F_SAME, 0, EMPTY_FRAME, 0, EMPTY_FRAME); } - // Next, check for emulated subtypes and call their companion methods + // Next, check for subtypes with specialized implementations and call them for (Class tested : typechecks) { - checkState(tested.isInterface(), "Dispatch emulation not supported for classes: %s", tested); Label fallthrough = new Label(); - String emulatedInterface = tested.getName().replace('.', '/'); + String testedName = tested.getName().replace('.', '/'); + // In case of a class this must be a member move; for interfaces use the companion. + String target = + tested.isInterface() + ? InterfaceDesugaring.getCompanionClassName(testedName) + : checkNotNull(memberMoves.get(rewriter.unprefix(testedName) + '#' + method.name())); dispatchMethod.visitVarInsn(Opcodes.ALOAD, 0); // load "receiver" - dispatchMethod.visitTypeInsn(Opcodes.INSTANCEOF, emulatedInterface); + dispatchMethod.visitTypeInsn(Opcodes.INSTANCEOF, testedName); dispatchMethod.visitJumpInsn(Opcodes.IFEQ, fallthrough); dispatchMethod.visitVarInsn(Opcodes.ALOAD, 0); // load "receiver" - dispatchMethod.visitTypeInsn(Opcodes.CHECKCAST, emulatedInterface); // make verifier happy + dispatchMethod.visitTypeInsn(Opcodes.CHECKCAST, testedName); // make verifier happy visitLoadArgs(dispatchMethod, methodType, 1 /* receiver already loaded above */); dispatchMethod.visitMethodInsn( Opcodes.INVOKESTATIC, - InterfaceDesugaring.getCompanionClassName(emulatedInterface), + target, method.name(), - InterfaceDesugaring.companionDefaultMethodDescriptor( - emulatedInterface, method.descriptor()), + InterfaceDesugaring.companionDefaultMethodDescriptor(testedName, method.descriptor()), /*itf=*/ false); dispatchMethod.visitInsn(methodType.getReturnType().getOpcode(Opcodes.IRETURN)); @@ -400,7 +427,7 @@ class CoreLibrarySupport { return collectImplementedInterfaces(clazz, new LinkedHashSet<>()) .stream() // search more subtypes before supertypes - .sorted(DefaultMethodClassFixer.InterfaceComparator.INSTANCE) + .sorted(DefaultMethodClassFixer.SubtypeComparator.INSTANCE) .map(itf -> findMethod(itf, name, desc)) .filter(Objects::nonNull) .findFirst() diff --git a/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java b/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java index 292e142..853ed09 100644 --- a/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java +++ b/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java @@ -203,7 +203,7 @@ public class DefaultMethodClassFixer extends ClassVisitor { } private void stubMissingDefaultAndBridgeMethods() { - TreeSet> allInterfaces = new TreeSet<>(InterfaceComparator.INSTANCE); + TreeSet> allInterfaces = new TreeSet<>(SubtypeComparator.INSTANCE); for (String direct : directInterfaces) { // Loading ensures all transitively implemented interfaces can be loaded, which is necessary // to produce correct default method stubs in all cases. We could do without classloading but @@ -647,18 +647,17 @@ public class DefaultMethodClassFixer extends ClassVisitor { } } - /** Comparator for interfaces that compares by whether interfaces extend one another. */ - enum InterfaceComparator implements Comparator> { + /** Comparator for classes and interfaces that compares by whether subtyping relationship. */ + enum SubtypeComparator implements Comparator> { /** Orders subtypes before supertypes and breaks ties lexicographically. */ INSTANCE; @Override public int compare(Class o1, Class o2) { - checkArgument(o1.isInterface()); - checkArgument(o2.isInterface()); if (o1 == o2) { return 0; } + // order subtypes before supertypes if (o1.isAssignableFrom(o2)) { // o1 is supertype of o2 return 1; // we want o1 to come after o2 } diff --git a/test/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixerTest.java b/test/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixerTest.java index 27083db..faa6dda 100644 --- a/test/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixerTest.java +++ b/test/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixerTest.java @@ -15,7 +15,7 @@ package com.google.devtools.build.android.desugar; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.truth.Truth.assertThat; -import static com.google.devtools.build.android.desugar.DefaultMethodClassFixer.InterfaceComparator.INSTANCE; +import static com.google.devtools.build.android.desugar.DefaultMethodClassFixer.SubtypeComparator.INSTANCE; import com.google.common.collect.ImmutableList; import com.google.common.io.Closer; -- cgit v1.2.3 From 46d8182c5c6cb6616586a7ba8466f88ef2fee16a Mon Sep 17 00:00:00 2001 From: kmb Date: Mon, 12 Mar 2018 14:46:18 -0700 Subject: Minor fixes to KeepScanner tool: - use Guava to read zip entries - Fix keep rules emitted for constructors RELNOTES: None. PiperOrigin-RevId: 188781547 GitOrigin-RevId: 8e038b04e068285ba02b7934a7df25803802daff Change-Id: Ifc99978b041f9c1c97ff707aafac90c59187c6c8 --- .../devtools/build/android/desugar/scan/KeepScanner.java | 12 +++++++----- .../devtools/build/android/desugar/scan/testdata_golden.txt | 10 +++++----- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/java/com/google/devtools/build/android/desugar/scan/KeepScanner.java b/java/com/google/devtools/build/android/desugar/scan/KeepScanner.java index 5892bf5..b347c7a 100644 --- a/java/com/google/devtools/build/android/desugar/scan/KeepScanner.java +++ b/java/com/google/devtools/build/android/desugar/scan/KeepScanner.java @@ -15,11 +15,11 @@ package com.google.devtools.build.android.desugar.scan; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.base.Preconditions.checkState; import static java.nio.file.StandardOpenOption.CREATE; import static java.util.Comparator.comparing; import com.google.common.collect.ImmutableSet; +import com.google.common.io.ByteStreams; import com.google.devtools.build.android.Converters.ExistingPathConverter; import com.google.devtools.build.android.Converters.PathConverter; import com.google.devtools.common.options.Option; @@ -140,18 +140,20 @@ class KeepScanner { private static byte[] readFully(ZipFile zip, ZipEntry entry) { byte[] result = new byte[(int) entry.getSize()]; try (InputStream content = zip.getInputStream(entry)) { - checkState(content.read(result) == result.length); - checkState(content.read() == -1); + ByteStreams.readFully(content, result); + return result; } catch (IOException e) { throw new IOError(e); } - return result; } private static CharSequence toKeepDescriptor(KeepReference member) { StringBuilder result = new StringBuilder(); if (member.isMethodReference()) { - result.append("*** ").append(member.name()).append("("); + if (!"".equals(member.name())) { + result.append("*** "); + } + result.append(member.name()).append("("); // Ignore return type as it's unique in the source language boolean first = true; for (Type param : Type.getMethodType(member.desc()).getArgumentTypes()) { diff --git a/test/java/com/google/devtools/build/android/desugar/scan/testdata_golden.txt b/test/java/com/google/devtools/build/android/desugar/scan/testdata_golden.txt index e4509b4..35744ce 100644 --- a/test/java/com/google/devtools/build/android/desugar/scan/testdata_golden.txt +++ b/test/java/com/google/devtools/build/android/desugar/scan/testdata_golden.txt @@ -2,7 +2,7 @@ *** println(java.lang.String); } -keep class java.lang.AssertionError { - *** (); + (); } -keep class java.lang.Class { *** cast(java.lang.Object); @@ -11,7 +11,7 @@ -keep class java.lang.IndexOutOfBoundsException { } -keep class java.lang.Object { - *** (); + (); } -keep class java.lang.String { } @@ -21,8 +21,8 @@ -keep class java.util.AbstractList { } -keep class java.util.ArrayList { - *** (); - *** (int); + (); + (int); *** add(java.lang.Object); *** get(int); *** iterator(); @@ -30,7 +30,7 @@ -keep class java.util.Collection { } -keep class java.util.Date { - *** (long); + (long); *** getTime(); } -keep class java.util.Iterator { -- cgit v1.2.3 From 44b53edaa9c5611c75e08da2fdc5d91c7b0ac6ae Mon Sep 17 00:00:00 2001 From: kmb Date: Mon, 12 Mar 2018 21:37:51 -0700 Subject: Make KeepScanner tool search classpath for nearest definition of each member reference, instead of potentially referring to a subtype. Refactor desugar's class loading machinery and related code into a separate package for easier reuse in this tool. RELNOTES: None. PiperOrigin-RevId: 188825305 GitOrigin-RevId: 2cbeb24a9c41c6b14ecbb26e2e198fbaf79aea64 Change-Id: Ie2969cb1e1c86aa68c5a6dc0be6b42b09dfaee70 --- .../devtools/build/android/desugar/BitFlags.java | 51 ----- .../android/desugar/BytecodeTypeInference.java | 1 + .../build/android/desugar/ClassReaderFactory.java | 3 + .../build/android/desugar/ClassVsInterface.java | 1 + .../build/android/desugar/CoreLibraryRewriter.java | 201 ------------------ .../build/android/desugar/CoreLibrarySupport.java | 2 + .../android/desugar/DefaultMethodClassFixer.java | 1 + .../devtools/build/android/desugar/Desugar.java | 48 +---- .../desugar/DirectoryInputFileProvider.java | 81 ------- .../desugar/DirectoryOutputFileProvider.java | 59 ------ .../android/desugar/EmulatedInterfaceRewriter.java | 1 + .../devtools/build/android/desugar/FieldInfo.java | 29 --- .../build/android/desugar/HeaderClassLoader.java | 234 --------------------- .../build/android/desugar/IndexedInputs.java | 94 --------- .../build/android/desugar/InputFileProvider.java | 36 ---- .../build/android/desugar/InterfaceDesugaring.java | 2 + .../build/android/desugar/Java7Compatibility.java | 1 + .../build/android/desugar/LambdaClassFixer.java | 1 + .../build/android/desugar/LambdaDesugaring.java | 1 + .../android/desugar/LongCompareMethodRewriter.java | 1 + .../ObjectsRequireNonNullMethodRewriter.java | 1 + .../build/android/desugar/OutputFileProvider.java | 32 --- .../android/desugar/TryWithResourcesRewriter.java | 1 + .../android/desugar/ZipInputFileProvider.java | 64 ------ .../android/desugar/ZipOutputFileProvider.java | 78 ------- .../build/android/desugar/io/BitFlags.java | 51 +++++ .../android/desugar/io/CoreLibraryRewriter.java | 201 ++++++++++++++++++ .../desugar/io/DirectoryInputFileProvider.java | 81 +++++++ .../desugar/io/DirectoryOutputFileProvider.java | 59 ++++++ .../build/android/desugar/io/FieldInfo.java | 29 +++ .../android/desugar/io/HeaderClassLoader.java | 234 +++++++++++++++++++++ .../build/android/desugar/io/IndexedInputs.java | 94 +++++++++ .../android/desugar/io/InputFileProvider.java | 49 +++++ .../android/desugar/io/OutputFileProvider.java | 45 ++++ .../android/desugar/io/ThrowingClassLoader.java | 27 +++ .../android/desugar/io/ZipInputFileProvider.java | 64 ++++++ .../android/desugar/io/ZipOutputFileProvider.java | 78 +++++++ .../build/android/desugar/scan/KeepScanner.java | 143 ++++++++++++- .../android/desugar/CoreLibrarySupportTest.java | 1 + .../android/desugar/CorePackageRenamerTest.java | 1 + .../desugar/DefaultMethodClassFixerTest.java | 5 +- .../build/android/desugar/FieldInfoTest.java | 33 --- .../build/android/desugar/IndexedInputsTest.java | 136 ------------ .../android/desugar/Java7CompatibilityTest.java | 1 + .../desugar/TryWithResourcesRewriterTest.java | 1 + .../build/android/desugar/io/FieldInfoTest.java | 33 +++ .../android/desugar/io/IndexedInputsTest.java | 136 ++++++++++++ .../scan/testdata/CollectionReferences.java | 4 + .../build/android/desugar/scan/testdata_golden.txt | 16 ++ 49 files changed, 1371 insertions(+), 1175 deletions(-) delete mode 100644 java/com/google/devtools/build/android/desugar/BitFlags.java delete mode 100644 java/com/google/devtools/build/android/desugar/CoreLibraryRewriter.java delete mode 100644 java/com/google/devtools/build/android/desugar/DirectoryInputFileProvider.java delete mode 100644 java/com/google/devtools/build/android/desugar/DirectoryOutputFileProvider.java delete mode 100644 java/com/google/devtools/build/android/desugar/FieldInfo.java delete mode 100644 java/com/google/devtools/build/android/desugar/HeaderClassLoader.java delete mode 100644 java/com/google/devtools/build/android/desugar/IndexedInputs.java delete mode 100644 java/com/google/devtools/build/android/desugar/InputFileProvider.java delete mode 100644 java/com/google/devtools/build/android/desugar/OutputFileProvider.java delete mode 100644 java/com/google/devtools/build/android/desugar/ZipInputFileProvider.java delete mode 100644 java/com/google/devtools/build/android/desugar/ZipOutputFileProvider.java create mode 100644 java/com/google/devtools/build/android/desugar/io/BitFlags.java create mode 100644 java/com/google/devtools/build/android/desugar/io/CoreLibraryRewriter.java create mode 100644 java/com/google/devtools/build/android/desugar/io/DirectoryInputFileProvider.java create mode 100644 java/com/google/devtools/build/android/desugar/io/DirectoryOutputFileProvider.java create mode 100644 java/com/google/devtools/build/android/desugar/io/FieldInfo.java create mode 100644 java/com/google/devtools/build/android/desugar/io/HeaderClassLoader.java create mode 100644 java/com/google/devtools/build/android/desugar/io/IndexedInputs.java create mode 100644 java/com/google/devtools/build/android/desugar/io/InputFileProvider.java create mode 100644 java/com/google/devtools/build/android/desugar/io/OutputFileProvider.java create mode 100644 java/com/google/devtools/build/android/desugar/io/ThrowingClassLoader.java create mode 100644 java/com/google/devtools/build/android/desugar/io/ZipInputFileProvider.java create mode 100644 java/com/google/devtools/build/android/desugar/io/ZipOutputFileProvider.java delete mode 100644 test/java/com/google/devtools/build/android/desugar/FieldInfoTest.java delete mode 100644 test/java/com/google/devtools/build/android/desugar/IndexedInputsTest.java create mode 100644 test/java/com/google/devtools/build/android/desugar/io/FieldInfoTest.java create mode 100644 test/java/com/google/devtools/build/android/desugar/io/IndexedInputsTest.java diff --git a/java/com/google/devtools/build/android/desugar/BitFlags.java b/java/com/google/devtools/build/android/desugar/BitFlags.java deleted file mode 100644 index 8be2288..0000000 --- a/java/com/google/devtools/build/android/desugar/BitFlags.java +++ /dev/null @@ -1,51 +0,0 @@ -// 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 org.objectweb.asm.Opcodes; - -/** Convenience method for working with {@code int} bitwise flags. */ -class BitFlags { - - /** - * Returns {@code true} iff all bits in {@code bitmask} are set in {@code flags}. Trivially - * returns {@code true} if {@code bitmask} is 0. - */ - public static boolean isSet(int flags, int bitmask) { - return (flags & bitmask) == bitmask; - } - - /** - * Returns {@code true} iff none of the bits in {@code bitmask} are set in {@code flags}. - * Trivially returns {@code true} if {@code bitmask} is 0. - */ - public static boolean noneSet(int flags, int bitmask) { - return (flags & bitmask) == 0; - } - - public static boolean isInterface(int access) { - return isSet(access, Opcodes.ACC_INTERFACE); - } - - public static boolean isStatic(int access) { - return isSet(access, Opcodes.ACC_STATIC); - } - - public static boolean isSynthetic(int access) { - return isSet(access, Opcodes.ACC_SYNTHETIC); - } - - // Static methods only - private BitFlags() {} -} diff --git a/java/com/google/devtools/build/android/desugar/BytecodeTypeInference.java b/java/com/google/devtools/build/android/desugar/BytecodeTypeInference.java index 783069f..ce36071 100644 --- a/java/com/google/devtools/build/android/desugar/BytecodeTypeInference.java +++ b/java/com/google/devtools/build/android/desugar/BytecodeTypeInference.java @@ -19,6 +19,7 @@ import static com.google.common.base.Preconditions.checkState; import com.google.auto.value.AutoValue; import com.google.common.collect.ImmutableList; +import com.google.devtools.build.android.desugar.io.BitFlags; import java.util.ArrayList; import java.util.Optional; import javax.annotation.Nullable; diff --git a/java/com/google/devtools/build/android/desugar/ClassReaderFactory.java b/java/com/google/devtools/build/android/desugar/ClassReaderFactory.java index bae5251..aff9bab 100644 --- a/java/com/google/devtools/build/android/desugar/ClassReaderFactory.java +++ b/java/com/google/devtools/build/android/desugar/ClassReaderFactory.java @@ -13,6 +13,9 @@ // limitations under the License. package com.google.devtools.build.android.desugar; +import com.google.devtools.build.android.desugar.io.CoreLibraryRewriter; +import com.google.devtools.build.android.desugar.io.IndexedInputs; +import com.google.devtools.build.android.desugar.io.InputFileProvider; import java.io.IOException; import java.io.InputStream; import javax.annotation.Nullable; diff --git a/java/com/google/devtools/build/android/desugar/ClassVsInterface.java b/java/com/google/devtools/build/android/desugar/ClassVsInterface.java index cb62deb..2724454 100644 --- a/java/com/google/devtools/build/android/desugar/ClassVsInterface.java +++ b/java/com/google/devtools/build/android/desugar/ClassVsInterface.java @@ -16,6 +16,7 @@ package com.google.devtools.build.android.desugar; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; +import com.google.devtools.build.android.desugar.io.BitFlags; import java.util.HashMap; import javax.annotation.Nullable; import org.objectweb.asm.ClassReader; diff --git a/java/com/google/devtools/build/android/desugar/CoreLibraryRewriter.java b/java/com/google/devtools/build/android/desugar/CoreLibraryRewriter.java deleted file mode 100644 index 698fc53..0000000 --- a/java/com/google/devtools/build/android/desugar/CoreLibraryRewriter.java +++ /dev/null @@ -1,201 +0,0 @@ -// 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 java.io.IOException; -import java.io.InputStream; -import javax.annotation.Nullable; -import org.objectweb.asm.Attribute; -import org.objectweb.asm.ClassReader; -import org.objectweb.asm.ClassVisitor; -import org.objectweb.asm.ClassWriter; -import org.objectweb.asm.Opcodes; -import org.objectweb.asm.commons.ClassRemapper; -import org.objectweb.asm.commons.Remapper; - -/** Utility class to prefix or unprefix class names of core library classes */ -class CoreLibraryRewriter { - private final String prefix; - - public CoreLibraryRewriter(String prefix) { - this.prefix = prefix; - } - - /** - * Factory method that returns either a normal ClassReader if prefix is empty, or a ClassReader - * with a ClassRemapper that prefixes class names of core library classes if prefix is not empty. - */ - public ClassReader reader(InputStream content) throws IOException { - if (prefix.isEmpty()) { - return new ClassReader(content); - } else { - return new PrefixingClassReader(content, prefix); - } - } - - /** - * Factory method that returns a ClassVisitor that delegates to a ClassWriter, removing prefix - * from core library class names if it is not empty. - */ - public UnprefixingClassWriter writer(int flags) { - return new UnprefixingClassWriter(flags); - } - - static boolean shouldPrefix(String typeName) { - return (typeName.startsWith("java/") || typeName.startsWith("sun/")) && !except(typeName); - } - - private static boolean except(String typeName) { - if (typeName.startsWith("java/lang/invoke/")) { - return true; - } - - switch (typeName) { - // Autoboxed types - case "java/lang/Boolean": - case "java/lang/Byte": - case "java/lang/Character": - case "java/lang/Double": - case "java/lang/Float": - case "java/lang/Integer": - case "java/lang/Long": - case "java/lang/Number": - case "java/lang/Short": - - // Special types - case "java/lang/Class": - case "java/lang/Object": - case "java/lang/String": - case "java/lang/Throwable": - return true; - - default: // fall out - } - - return false; - } - - public String getPrefix() { - return prefix; - } - - /** Removes prefix from class names */ - public String unprefix(String typeName) { - if (prefix.isEmpty() || !typeName.startsWith(prefix)) { - return typeName; - } - return typeName.substring(prefix.length()); - } - - /** ClassReader that prefixes core library class names as they are read */ - private static class PrefixingClassReader extends ClassReader { - private final String prefix; - - PrefixingClassReader(InputStream content, String prefix) throws IOException { - super(content); - this.prefix = prefix; - } - - @Override - public void accept(ClassVisitor cv, Attribute[] attrs, int flags) { - cv = - new ClassRemapper( - cv, - new Remapper() { - @Override - public String map(String typeName) { - return prefix(typeName); - } - }); - super.accept(cv, attrs, flags); - } - - @Override - public String getClassName() { - return prefix(super.getClassName()); - } - - @Override - public String getSuperName() { - String result = super.getSuperName(); - return result != null ? prefix(result) : null; - } - - @Override - public String[] getInterfaces() { - String[] result = super.getInterfaces(); - for (int i = 0, len = result.length; i < len; ++i) { - result[i] = prefix(result[i]); - } - return result; - } - - /** Prefixes core library class names with prefix. */ - private String prefix(String typeName) { - if (shouldPrefix(typeName)) { - return prefix + typeName; - } - return typeName; - } - } - - /** - * ClassVisitor that delegates to a ClassWriter, but removes a prefix as each class is written. - * The unprefixing is optimized out if prefix is empty. - */ - public class UnprefixingClassWriter extends ClassVisitor { - private final ClassWriter writer; - - private String finalClassName; - - UnprefixingClassWriter(int flags) { - super(Opcodes.ASM6); - this.writer = new ClassWriter(flags); - this.cv = this.writer; - if (!prefix.isEmpty()) { - this.cv = - new ClassRemapper( - this.writer, - new Remapper() { - @Override - public String map(String typeName) { - return unprefix(typeName); - } - }); - } - } - - /** Returns the (unprefixed) name of the class once written. */ - @Nullable - String getClassName() { - return finalClassName; - } - - byte[] toByteArray() { - return writer.toByteArray(); - } - - @Override - public void visit( - int version, - int access, - String name, - String signature, - String superName, - String[] interfaces) { - finalClassName = unprefix(name); - super.visit(version, access, name, signature, superName, interfaces); - } - } -} diff --git a/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java b/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java index da23c12..fd10e5e 100644 --- a/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java +++ b/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java @@ -25,6 +25,8 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.LinkedHashMultimap; import com.google.common.collect.Multimap; +import com.google.devtools.build.android.desugar.io.BitFlags; +import com.google.devtools.build.android.desugar.io.CoreLibraryRewriter; import com.google.errorprone.annotations.Immutable; import java.lang.reflect.Method; import java.util.Collection; diff --git a/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java b/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java index 853ed09..960cfeb 100644 --- a/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java +++ b/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java @@ -18,6 +18,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import com.google.common.collect.ImmutableList; +import com.google.devtools.build.android.desugar.io.BitFlags; import java.util.Comparator; import java.util.HashSet; import java.util.Iterator; diff --git a/java/com/google/devtools/build/android/desugar/Desugar.java b/java/com/google/devtools/build/android/desugar/Desugar.java index 506a380..c176f9c 100644 --- a/java/com/google/devtools/build/android/desugar/Desugar.java +++ b/java/com/google/devtools/build/android/desugar/Desugar.java @@ -27,14 +27,19 @@ import com.google.common.io.ByteStreams; import com.google.common.io.Closer; import com.google.devtools.build.android.Converters.ExistingPathConverter; import com.google.devtools.build.android.Converters.PathConverter; -import com.google.devtools.build.android.desugar.CoreLibraryRewriter.UnprefixingClassWriter; +import com.google.devtools.build.android.desugar.io.CoreLibraryRewriter; +import com.google.devtools.build.android.desugar.io.CoreLibraryRewriter.UnprefixingClassWriter; +import com.google.devtools.build.android.desugar.io.HeaderClassLoader; +import com.google.devtools.build.android.desugar.io.IndexedInputs; +import com.google.devtools.build.android.desugar.io.InputFileProvider; +import com.google.devtools.build.android.desugar.io.OutputFileProvider; +import com.google.devtools.build.android.desugar.io.ThrowingClassLoader; import com.google.devtools.common.options.Option; import com.google.devtools.common.options.OptionDocumentationCategory; import com.google.devtools.common.options.OptionEffectTag; import com.google.devtools.common.options.OptionsBase; import com.google.devtools.common.options.OptionsParser; import com.google.devtools.common.options.ShellQuotedParamsFilePreProcessor; -import com.google.errorprone.annotations.MustBeClosed; import java.io.IOError; import java.io.IOException; import java.io.InputStream; @@ -386,8 +391,8 @@ class Desugar { Files.isDirectory(inputPath) || !Files.isDirectory(outputPath), "Input jar file requires an output jar file"); - try (OutputFileProvider outputFileProvider = toOutputFileProvider(outputPath); - InputFileProvider inputFiles = toInputFileProvider(inputPath)) { + try (OutputFileProvider outputFileProvider = OutputFileProvider.create(outputPath); + InputFileProvider inputFiles = InputFileProvider.open(inputPath)) { DependencyCollector depsCollector = createDepsCollector(); IndexedInputs indexedInputFiles = new IndexedInputs(ImmutableList.of(inputFiles)); // Prepend classpath with input file itself so LambdaDesugaring can load classes with @@ -942,19 +947,6 @@ class Desugar { return ioPairListbuilder.build(); } - @VisibleForTesting - static class ThrowingClassLoader extends ClassLoader { - @Override - protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { - if (name.startsWith("java.")) { - // Use system class loader for java. classes, since ClassLoader.defineClass gets - // grumpy when those don't come from the standard place. - return super.loadClass(name, resolve); - } - throw new ClassNotFoundException(); - } - } - private static void deleteTreeOnExit(final Path directory) { Thread shutdownHook = new Thread() { @@ -993,26 +985,6 @@ class Desugar { } } - /** Transform a Path to an {@link OutputFileProvider} */ - @MustBeClosed - private static OutputFileProvider toOutputFileProvider(Path path) throws IOException { - if (Files.isDirectory(path)) { - return new DirectoryOutputFileProvider(path); - } else { - return new ZipOutputFileProvider(path); - } - } - - /** Transform a Path to an InputFileProvider that needs to be closed by the caller. */ - @MustBeClosed - private static InputFileProvider toInputFileProvider(Path path) throws IOException { - if (Files.isDirectory(path)) { - return new DirectoryInputFileProvider(path); - } else { - return new ZipInputFileProvider(path); - } - } - /** * Transform a list of Path to a list of InputFileProvider and register them with the given * closer. @@ -1023,7 +995,7 @@ class Desugar { Closer closer, List paths) throws IOException { ImmutableList.Builder builder = new ImmutableList.Builder<>(); for (Path path : paths) { - builder.add(closer.register(toInputFileProvider(path))); + builder.add(closer.register(InputFileProvider.open(path))); } return builder.build(); } diff --git a/java/com/google/devtools/build/android/desugar/DirectoryInputFileProvider.java b/java/com/google/devtools/build/android/desugar/DirectoryInputFileProvider.java deleted file mode 100644 index 1c5abc9..0000000 --- a/java/com/google/devtools/build/android/desugar/DirectoryInputFileProvider.java +++ /dev/null @@ -1,81 +0,0 @@ -// 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 java.io.File; -import java.io.FileInputStream; -import java.io.IOError; -import java.io.IOException; -import java.io.InputStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.function.Consumer; -import java.util.stream.Stream; -import java.util.zip.ZipEntry; - -/** Input provider is a directory. */ -class DirectoryInputFileProvider implements InputFileProvider { - - private final Path root; - - public DirectoryInputFileProvider(Path root) { - this.root = root; - } - - @Override - public String toString() { - return root.getFileName().toString(); - } - - @Override - public InputStream getInputStream(String filename) throws IOException { - return new FileInputStream(root.resolve(filename).toFile()); - } - - @Override - public ZipEntry getZipEntry(String filename) { - ZipEntry destEntry = new ZipEntry(filename); - destEntry.setTime(0L); // Use stable timestamp Jan 1 1980 - return destEntry; - } - - @Override - public void close() throws IOException { - // Nothing to close - } - - @Override - public Iterator iterator() { - final List entries = new ArrayList<>(); - try (Stream paths = Files.walk(root)) { - paths.forEach( - new Consumer() { - @Override - public void accept(Path t) { - if (Files.isRegularFile(t)) { - // Internally, we use '/' as a common package separator in filename to abstract - // that filename can comes from a zip or a directory. - entries.add(root.relativize(t).toString().replace(File.separatorChar, '/')); - } - } - }); - } catch (IOException e) { - throw new IOError(e); - } - return entries.iterator(); - } -} diff --git a/java/com/google/devtools/build/android/desugar/DirectoryOutputFileProvider.java b/java/com/google/devtools/build/android/desugar/DirectoryOutputFileProvider.java deleted file mode 100644 index 782a81e..0000000 --- a/java/com/google/devtools/build/android/desugar/DirectoryOutputFileProvider.java +++ /dev/null @@ -1,59 +0,0 @@ -// 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 com.google.common.io.ByteStreams; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.file.Files; -import java.nio.file.Path; - -/** Output provider is a directory. */ -public class DirectoryOutputFileProvider implements OutputFileProvider { - - private final Path root; - - public DirectoryOutputFileProvider(Path root) { - this.root = root; - } - - @Override - public void copyFrom(String filename, InputFileProvider inputFileProvider) throws IOException { - Path path = root.resolve(filename); - createParentFolder(path); - try (InputStream is = inputFileProvider.getInputStream(filename); - OutputStream os = Files.newOutputStream(path)) { - ByteStreams.copy(is, os); - } - } - - @Override - public void write(String filename, byte[] content) throws IOException { - Path path = root.resolve(filename); - createParentFolder(path); - Files.write(path, content); - } - - @Override - public void close() { - // Nothing to close - } - - private void createParentFolder(Path path) throws IOException { - if (!Files.exists(path.getParent())) { - Files.createDirectories(path.getParent()); - } - } -} diff --git a/java/com/google/devtools/build/android/desugar/EmulatedInterfaceRewriter.java b/java/com/google/devtools/build/android/desugar/EmulatedInterfaceRewriter.java index f066f2a..355dd97 100644 --- a/java/com/google/devtools/build/android/desugar/EmulatedInterfaceRewriter.java +++ b/java/com/google/devtools/build/android/desugar/EmulatedInterfaceRewriter.java @@ -13,6 +13,7 @@ // limitations under the License. package com.google.devtools.build.android.desugar; +import com.google.devtools.build.android.desugar.io.BitFlags; import java.util.Collections; import java.util.LinkedHashSet; import org.objectweb.asm.ClassVisitor; diff --git a/java/com/google/devtools/build/android/desugar/FieldInfo.java b/java/com/google/devtools/build/android/desugar/FieldInfo.java deleted file mode 100644 index c281039..0000000 --- a/java/com/google/devtools/build/android/desugar/FieldInfo.java +++ /dev/null @@ -1,29 +0,0 @@ -// 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 com.google.auto.value.AutoValue; - -/** A value class to store the fields information. */ -@AutoValue -public abstract class FieldInfo { - - static FieldInfo create(String owner, String name, String desc) { - return new AutoValue_FieldInfo(owner, name, desc); - } - - public abstract String owner(); - public abstract String name(); - public abstract String desc(); -} diff --git a/java/com/google/devtools/build/android/desugar/HeaderClassLoader.java b/java/com/google/devtools/build/android/desugar/HeaderClassLoader.java deleted file mode 100644 index 77d99bb..0000000 --- a/java/com/google/devtools/build/android/desugar/HeaderClassLoader.java +++ /dev/null @@ -1,234 +0,0 @@ -// 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 com.google.common.collect.ImmutableList; -import java.io.IOError; -import java.io.IOException; -import java.io.InputStream; -import org.objectweb.asm.ClassReader; -import org.objectweb.asm.ClassVisitor; -import org.objectweb.asm.ClassWriter; -import org.objectweb.asm.FieldVisitor; -import org.objectweb.asm.MethodVisitor; -import org.objectweb.asm.Opcodes; - -/** - * Class loader that can "load" classes from header Jars. This class loader stubs in missing code - * attributes on the fly to make {@link ClassLoader#defineClass} happy. Classes loaded are unusable - * other than to resolve method references, so this class loader should only be used to process or - * inspect classes, not to execute their code. Also note that the resulting classes may be missing - * private members, which header Jars may omit. - * - * @see java.net.URLClassLoader - */ -class HeaderClassLoader extends ClassLoader { - - private final IndexedInputs indexedInputs; - private final CoreLibraryRewriter rewriter; - - public HeaderClassLoader( - IndexedInputs indexedInputs, CoreLibraryRewriter rewriter, ClassLoader parent) { - super(parent); - this.rewriter = rewriter; - this.indexedInputs = indexedInputs; - } - - @Override - protected Class findClass(String name) throws ClassNotFoundException { - String filename = rewriter.unprefix(name.replace('.', '/') + ".class"); - InputFileProvider inputFileProvider = indexedInputs.getInputFileProvider(filename); - if (inputFileProvider == null) { - throw new ClassNotFoundException("Class " + name + " not found"); - } - byte[] bytecode; - try (InputStream content = inputFileProvider.getInputStream(filename)) { - ClassReader reader = rewriter.reader(content); - // Have ASM compute maxs so we don't need to figure out how many formal parameters there are - ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS); - ImmutableList interfaceFieldNames = getFieldsIfReaderIsInterface(reader); - // TODO(kmb): Consider SKIP_CODE and stubbing everything so class loader doesn't verify code - reader.accept(new CodeStubber(writer, interfaceFieldNames), ClassReader.SKIP_DEBUG); - bytecode = writer.toByteArray(); - } catch (IOException e) { - throw new IOError(e); - } - return defineClass(name, bytecode, 0, bytecode.length); - } - - /** - * If the {@code reader} is an interface, then extract all the declared fields in it. Otherwise, - * return an empty list. - */ - private static ImmutableList getFieldsIfReaderIsInterface(ClassReader reader) { - if (BitFlags.isSet(reader.getAccess(), Opcodes.ACC_INTERFACE)) { - NonPrimitiveFieldCollector collector = new NonPrimitiveFieldCollector(); - reader.accept(collector, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG); - return collector.declaredNonPrimitiveFields.build(); - } - return ImmutableList.of(); - } - - /** Collect the fields defined in a class. */ - private static class NonPrimitiveFieldCollector extends ClassVisitor { - - final ImmutableList.Builder declaredNonPrimitiveFields = ImmutableList.builder(); - private String internalName; - - public NonPrimitiveFieldCollector() { - super(Opcodes.ASM6); - } - - @Override - public void visit( - int version, - int access, - String name, - String signature, - String superName, - String[] interfaces) { - super.visit(version, access, name, signature, superName, interfaces); - this.internalName = name; - } - - @Override - public FieldVisitor visitField( - int access, String name, String desc, String signature, Object value) { - if (isNonPrimitiveType(desc)) { - declaredNonPrimitiveFields.add(FieldInfo.create(internalName, name, desc)); - } - return null; - } - - private static boolean isNonPrimitiveType(String type) { - char firstChar = type.charAt(0); - return firstChar == '[' || firstChar == 'L'; - } - } - - - /** - * Class visitor that stubs in missing code attributes, and erases the body of the static - * initializer of functional interfaces if the interfaces have default methods. The erasion of the - * clinit is mainly because when we are desugaring lambdas, we need to load the functional - * interfaces via class loaders, and since the interfaces have default methods, according to the - * JVM spec, these interfaces will be executed. This should be prevented due to security concerns. - */ - private static class CodeStubber extends ClassVisitor { - - private String internalName; - private boolean isInterface; - private final ImmutableList interfaceFields; - - public CodeStubber(ClassVisitor cv, ImmutableList interfaceFields) { - super(Opcodes.ASM6, cv); - this.interfaceFields = interfaceFields; - } - - @Override - public void visit( - int version, - int access, - String name, - String signature, - String superName, - String[] interfaces) { - super.visit(version, access, name, signature, superName, interfaces); - isInterface = BitFlags.isSet(access, Opcodes.ACC_INTERFACE); - internalName = name; - } - - @Override - public MethodVisitor visitMethod( - int access, String name, String desc, String signature, String[] exceptions) { - MethodVisitor dest = super.visitMethod(access, name, desc, signature, exceptions); - if ((access & (Opcodes.ACC_ABSTRACT | Opcodes.ACC_NATIVE)) != 0) { - // No need to stub out abstract or native methods - return dest; - } - if (isInterface && "".equals(name)) { - // Delete class initializers, to avoid code gets executed when we desugar lambdas. - // See b/62184142 - return new InterfaceInitializerEraser(dest, internalName, interfaceFields); - } - return new BodyStubber(dest); - } - } - - /** - * Erase the static initializer of an interface. Given an interface with non-primitive fields, - * this eraser discards the original body of clinit, and initializes each non-primitive field to - * null - */ - private static class InterfaceInitializerEraser extends MethodVisitor { - - private final MethodVisitor dest; - private final ImmutableList interfaceFields; - - public InterfaceInitializerEraser( - MethodVisitor mv, String internalName, ImmutableList interfaceFields) { - super(Opcodes.ASM6); - dest = mv; - this.interfaceFields = interfaceFields; - } - - @Override - public void visitCode() { - dest.visitCode(); - } - - @Override - public void visitEnd() { - for (FieldInfo fieldInfo : interfaceFields) { - dest.visitInsn(Opcodes.ACONST_NULL); - dest.visitFieldInsn( - Opcodes.PUTSTATIC, fieldInfo.owner(), fieldInfo.name(), fieldInfo.desc()); - } - dest.visitInsn(Opcodes.RETURN); - dest.visitMaxs(0, 0); - dest.visitEnd(); - } - } - - /** Method visitor used by {@link CodeStubber} to put code into methods without code. */ - private static class BodyStubber extends MethodVisitor { - - private static final String EXCEPTION_INTERNAL_NAME = "java/lang/UnsupportedOperationException"; - - private boolean hasCode = false; - - public BodyStubber(MethodVisitor mv) { - super(Opcodes.ASM6, mv); - } - - @Override - public void visitCode() { - hasCode = true; - super.visitCode(); - } - - @Override - public void visitEnd() { - if (!hasCode) { - super.visitTypeInsn(Opcodes.NEW, EXCEPTION_INTERNAL_NAME); - super.visitInsn(Opcodes.DUP); - super.visitMethodInsn( - Opcodes.INVOKESPECIAL, EXCEPTION_INTERNAL_NAME, "", "()V", /*itf*/ false); - super.visitInsn(Opcodes.ATHROW); - super.visitMaxs(0, 0); // triggers computation of the actual max's - } - super.visitEnd(); - } - } -} diff --git a/java/com/google/devtools/build/android/desugar/IndexedInputs.java b/java/com/google/devtools/build/android/desugar/IndexedInputs.java deleted file mode 100644 index 33c6132..0000000 --- a/java/com/google/devtools/build/android/desugar/IndexedInputs.java +++ /dev/null @@ -1,94 +0,0 @@ -// 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 static com.google.common.base.Preconditions.checkState; - -import com.google.common.collect.ImmutableMap; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import javax.annotation.CheckReturnValue; -import javax.annotation.Nullable; - -/** - * Opens the given list of input files and compute an index of all classes in them, to avoid - * scanning all inputs over and over for each class to load. An indexed inputs can have a parent - * that is firstly used when a file name is searched. - */ -class IndexedInputs { - - private final ImmutableMap inputFiles; - - /** - * Parent {@link IndexedInputs} to use before to search a file name into this {@link - * IndexedInputs}. - */ - @Nullable - private final IndexedInputs parent; - - /** Index a list of input files without a parent {@link IndexedInputs}. */ - public IndexedInputs(List inputProviders) { - this.parent = null; - this.inputFiles = indexInputs(inputProviders); - } - - /** - * Create a new {@link IndexedInputs} with input files previously indexed and with a parent {@link - * IndexedInputs}. - */ - private IndexedInputs( - ImmutableMap inputFiles, IndexedInputs parentIndexedInputs) { - this.parent = parentIndexedInputs; - this.inputFiles = inputFiles; - } - - /** - * Create a new {@link IndexedInputs} with input files already indexed and with a parent {@link - * IndexedInputs}. - */ - @CheckReturnValue - public IndexedInputs withParent(IndexedInputs parent) { - checkState(this.parent == null); - return new IndexedInputs(this.inputFiles, parent); - } - - @Nullable - public InputFileProvider getInputFileProvider(String filename) { - checkArgument(filename.endsWith(".class")); - - if (parent != null) { - InputFileProvider inputFileProvider = parent.getInputFileProvider(filename); - if (inputFileProvider != null) { - return inputFileProvider; - } - } - - return inputFiles.get(filename); - } - - private ImmutableMap indexInputs( - List inputProviders) { - Map indexedInputs = new HashMap<>(); - for (InputFileProvider inputProvider : inputProviders) { - for (String relativePath : inputProvider) { - if (relativePath.endsWith(".class") && !indexedInputs.containsKey(relativePath)) { - indexedInputs.put(relativePath, inputProvider); - } - } - } - return ImmutableMap.copyOf(indexedInputs); - } -} diff --git a/java/com/google/devtools/build/android/desugar/InputFileProvider.java b/java/com/google/devtools/build/android/desugar/InputFileProvider.java deleted file mode 100644 index c2b6353..0000000 --- a/java/com/google/devtools/build/android/desugar/InputFileProvider.java +++ /dev/null @@ -1,36 +0,0 @@ -// 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 java.io.Closeable; -import java.io.IOException; -import java.io.InputStream; -import java.util.zip.ZipEntry; - -/** Input file provider allows to iterate on relative path filename of a directory or a jar file. */ -interface InputFileProvider extends Closeable, Iterable { - - /** - * Return a ZipEntry for {@code filename}. If the provider is a {@link ZipInputFileProvider}, the - * method returns the existing ZipEntry in order to keep its metadata, otherwise a new one is - * created. - */ - ZipEntry getZipEntry(String filename); - - /** - * This method returns an input stream allowing to read the file {@code filename}, it is the - * responsibility of the caller to close this stream. - */ - InputStream getInputStream(String filename) throws IOException; -} diff --git a/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java b/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java index 0a10df1..e9e3199 100644 --- a/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java +++ b/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java @@ -17,6 +17,8 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; +import com.google.devtools.build.android.desugar.io.BitFlags; +import com.google.devtools.build.android.desugar.io.FieldInfo; import java.lang.reflect.Method; import javax.annotation.Nullable; import org.objectweb.asm.AnnotationVisitor; diff --git a/java/com/google/devtools/build/android/desugar/Java7Compatibility.java b/java/com/google/devtools/build/android/desugar/Java7Compatibility.java index 37a45dd..2090d5c 100644 --- a/java/com/google/devtools/build/android/desugar/Java7Compatibility.java +++ b/java/com/google/devtools/build/android/desugar/Java7Compatibility.java @@ -17,6 +17,7 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; +import com.google.devtools.build.android.desugar.io.BitFlags; import javax.annotation.Nullable; import org.objectweb.asm.AnnotationVisitor; import org.objectweb.asm.Attribute; diff --git a/java/com/google/devtools/build/android/desugar/LambdaClassFixer.java b/java/com/google/devtools/build/android/desugar/LambdaClassFixer.java index fb05bcb..6b0a921 100644 --- a/java/com/google/devtools/build/android/desugar/LambdaClassFixer.java +++ b/java/com/google/devtools/build/android/desugar/LambdaClassFixer.java @@ -19,6 +19,7 @@ import static com.google.common.base.Preconditions.checkState; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; +import com.google.devtools.build.android.desugar.io.BitFlags; import java.util.HashSet; import java.util.LinkedHashSet; import org.objectweb.asm.AnnotationVisitor; diff --git a/java/com/google/devtools/build/android/desugar/LambdaDesugaring.java b/java/com/google/devtools/build/android/desugar/LambdaDesugaring.java index 5f41347..f9b5316 100644 --- a/java/com/google/devtools/build/android/desugar/LambdaDesugaring.java +++ b/java/com/google/devtools/build/android/desugar/LambdaDesugaring.java @@ -21,6 +21,7 @@ import static org.objectweb.asm.Opcodes.ASM6; import com.google.auto.value.AutoValue; import com.google.common.collect.ImmutableSet; +import com.google.devtools.build.android.desugar.io.BitFlags; import java.io.IOException; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles.Lookup; diff --git a/java/com/google/devtools/build/android/desugar/LongCompareMethodRewriter.java b/java/com/google/devtools/build/android/desugar/LongCompareMethodRewriter.java index 6ac415d..7f2f355 100644 --- a/java/com/google/devtools/build/android/desugar/LongCompareMethodRewriter.java +++ b/java/com/google/devtools/build/android/desugar/LongCompareMethodRewriter.java @@ -17,6 +17,7 @@ import static org.objectweb.asm.Opcodes.ASM6; import static org.objectweb.asm.Opcodes.INVOKESTATIC; import static org.objectweb.asm.Opcodes.LCMP; +import com.google.devtools.build.android.desugar.io.CoreLibraryRewriter; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.MethodVisitor; diff --git a/java/com/google/devtools/build/android/desugar/ObjectsRequireNonNullMethodRewriter.java b/java/com/google/devtools/build/android/desugar/ObjectsRequireNonNullMethodRewriter.java index 5e0a344..931459a 100644 --- a/java/com/google/devtools/build/android/desugar/ObjectsRequireNonNullMethodRewriter.java +++ b/java/com/google/devtools/build/android/desugar/ObjectsRequireNonNullMethodRewriter.java @@ -19,6 +19,7 @@ import static org.objectweb.asm.Opcodes.INVOKESTATIC; import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL; import static org.objectweb.asm.Opcodes.POP; +import com.google.devtools.build.android.desugar.io.CoreLibraryRewriter; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.MethodVisitor; diff --git a/java/com/google/devtools/build/android/desugar/OutputFileProvider.java b/java/com/google/devtools/build/android/desugar/OutputFileProvider.java deleted file mode 100644 index 7a590ef..0000000 --- a/java/com/google/devtools/build/android/desugar/OutputFileProvider.java +++ /dev/null @@ -1,32 +0,0 @@ -// 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 java.io.IOException; - -/** Output file provider allows to write files in directory or jar files. */ -interface OutputFileProvider extends AutoCloseable { - - /** Filename to use to write out dependency metadata for later consistency checking. */ - public static final String DESUGAR_DEPS_FILENAME = "META-INF/desugar_deps"; - - /** - * Copy {@code filename} from {@code inputFileProvider} to this output. If input file provider is - * a {@link ZipInputFileProvider} then the metadata of the zip entry are kept. - */ - void copyFrom(String filename, InputFileProvider inputFileProvider) throws IOException; - - /** Write {@code content} in {@code filename} to this output */ - void write(String filename, byte[] content) throws IOException; -} diff --git a/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriter.java b/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriter.java index e8509e7..818585f 100644 --- a/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriter.java +++ b/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriter.java @@ -29,6 +29,7 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.ImmutableSet; import com.google.devtools.build.android.desugar.BytecodeTypeInference.InferredType; +import com.google.devtools.build.android.desugar.io.BitFlags; import java.util.Collections; import java.util.LinkedHashSet; import java.util.Optional; diff --git a/java/com/google/devtools/build/android/desugar/ZipInputFileProvider.java b/java/com/google/devtools/build/android/desugar/ZipInputFileProvider.java deleted file mode 100644 index 307c8b8..0000000 --- a/java/com/google/devtools/build/android/desugar/ZipInputFileProvider.java +++ /dev/null @@ -1,64 +0,0 @@ -// 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 com.google.common.base.Functions; -import com.google.common.collect.Iterators; -import java.io.IOException; -import java.io.InputStream; -import java.nio.file.Path; -import java.util.Iterator; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; - -/** Input provider is a zip file. */ -class ZipInputFileProvider implements InputFileProvider { - - private final Path root; - - private final ZipFile zipFile; - - public ZipInputFileProvider(Path root) throws IOException { - this.root = root; - this.zipFile = new ZipFile(root.toFile()); - } - - @Override - public void close() throws IOException { - zipFile.close(); - } - - @Override - public String toString() { - return root.getFileName().toString(); - } - - @Override - public ZipEntry getZipEntry(String filename) { - ZipEntry zipEntry = zipFile.getEntry(filename); - zipEntry.setCompressedSize(-1); - return zipEntry; - } - - @Override - public InputStream getInputStream(String filename) throws IOException { - return zipFile.getInputStream(zipFile.getEntry(filename)); - } - - @Override - public Iterator iterator() { - return Iterators.transform( - Iterators.forEnumeration(zipFile.entries()), Functions.toStringFunction()); - } -} diff --git a/java/com/google/devtools/build/android/desugar/ZipOutputFileProvider.java b/java/com/google/devtools/build/android/desugar/ZipOutputFileProvider.java deleted file mode 100644 index 8d6501d..0000000 --- a/java/com/google/devtools/build/android/desugar/ZipOutputFileProvider.java +++ /dev/null @@ -1,78 +0,0 @@ -// 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.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.util.zip.CRC32; -import java.util.zip.ZipEntry; -import java.util.zip.ZipOutputStream; - -/** Output provider is a zip file. */ -public class ZipOutputFileProvider implements OutputFileProvider { - - private final ZipOutputStream out; - - public ZipOutputFileProvider(Path root) throws IOException { - out = new ZipOutputStream(new BufferedOutputStream(Files.newOutputStream(root))); - } - - @Override - public void copyFrom(String filename, InputFileProvider inputFileProvider) throws IOException { - // TODO(bazel-team): Avoid de- and re-compressing resource files - out.putNextEntry(inputFileProvider.getZipEntry(filename)); - try (InputStream is = inputFileProvider.getInputStream(filename)) { - ByteStreams.copy(is, out); - } - out.closeEntry(); - } - - @Override - public void write(String filename, byte[] content) throws IOException { - checkArgument(filename.equals(DESUGAR_DEPS_FILENAME) || filename.endsWith(".class"), - "Expect file to be copied: %s", filename); - writeStoredEntry(out, filename, content); - } - - @Override - public void close() throws IOException { - out.close(); - } - - private static void writeStoredEntry(ZipOutputStream out, String filename, byte[] content) - throws IOException { - // Need to pre-compute checksum for STORED (uncompressed) entries) - CRC32 checksum = new CRC32(); - checksum.update(content); - - ZipEntry result = new ZipEntry(filename); - result.setTime(0L); // Use stable timestamp Jan 1 1980 - result.setCrc(checksum.getValue()); - result.setSize(content.length); - result.setCompressedSize(content.length); - // Write uncompressed, since this is just an intermediary artifact that - // we will convert to .dex - result.setMethod(ZipEntry.STORED); - - out.putNextEntry(result); - out.write(content); - out.closeEntry(); - } -} diff --git a/java/com/google/devtools/build/android/desugar/io/BitFlags.java b/java/com/google/devtools/build/android/desugar/io/BitFlags.java new file mode 100644 index 0000000..af6f481 --- /dev/null +++ b/java/com/google/devtools/build/android/desugar/io/BitFlags.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.io; + +import org.objectweb.asm.Opcodes; + +/** Convenience method for working with {@code int} bitwise flags. */ +public class BitFlags { + + /** + * Returns {@code true} iff all bits in {@code bitmask} are set in {@code flags}. Trivially + * returns {@code true} if {@code bitmask} is 0. + */ + public static boolean isSet(int flags, int bitmask) { + return (flags & bitmask) == bitmask; + } + + /** + * Returns {@code true} iff none of the bits in {@code bitmask} are set in {@code flags}. + * Trivially returns {@code true} if {@code bitmask} is 0. + */ + public static boolean noneSet(int flags, int bitmask) { + return (flags & bitmask) == 0; + } + + public static boolean isInterface(int access) { + return isSet(access, Opcodes.ACC_INTERFACE); + } + + public static boolean isStatic(int access) { + return isSet(access, Opcodes.ACC_STATIC); + } + + public static boolean isSynthetic(int access) { + return isSet(access, Opcodes.ACC_SYNTHETIC); + } + + // Static methods only + private BitFlags() {} +} diff --git a/java/com/google/devtools/build/android/desugar/io/CoreLibraryRewriter.java b/java/com/google/devtools/build/android/desugar/io/CoreLibraryRewriter.java new file mode 100644 index 0000000..f3c546c --- /dev/null +++ b/java/com/google/devtools/build/android/desugar/io/CoreLibraryRewriter.java @@ -0,0 +1,201 @@ +// 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.io; + +import java.io.IOException; +import java.io.InputStream; +import javax.annotation.Nullable; +import org.objectweb.asm.Attribute; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.commons.ClassRemapper; +import org.objectweb.asm.commons.Remapper; + +/** Utility class to prefix or unprefix class names of core library classes */ +public class CoreLibraryRewriter { + private final String prefix; + + public CoreLibraryRewriter(String prefix) { + this.prefix = prefix; + } + + /** + * Factory method that returns either a normal ClassReader if prefix is empty, or a ClassReader + * with a ClassRemapper that prefixes class names of core library classes if prefix is not empty. + */ + public ClassReader reader(InputStream content) throws IOException { + if (prefix.isEmpty()) { + return new ClassReader(content); + } else { + return new PrefixingClassReader(content, prefix); + } + } + + /** + * Factory method that returns a ClassVisitor that delegates to a ClassWriter, removing prefix + * from core library class names if it is not empty. + */ + public UnprefixingClassWriter writer(int flags) { + return new UnprefixingClassWriter(flags); + } + + static boolean shouldPrefix(String typeName) { + return (typeName.startsWith("java/") || typeName.startsWith("sun/")) && !except(typeName); + } + + private static boolean except(String typeName) { + if (typeName.startsWith("java/lang/invoke/")) { + return true; + } + + switch (typeName) { + // Autoboxed types + case "java/lang/Boolean": + case "java/lang/Byte": + case "java/lang/Character": + case "java/lang/Double": + case "java/lang/Float": + case "java/lang/Integer": + case "java/lang/Long": + case "java/lang/Number": + case "java/lang/Short": + + // Special types + case "java/lang/Class": + case "java/lang/Object": + case "java/lang/String": + case "java/lang/Throwable": + return true; + + default: // fall out + } + + return false; + } + + public String getPrefix() { + return prefix; + } + + /** Removes prefix from class names */ + public String unprefix(String typeName) { + if (prefix.isEmpty() || !typeName.startsWith(prefix)) { + return typeName; + } + return typeName.substring(prefix.length()); + } + + /** ClassReader that prefixes core library class names as they are read */ + private static class PrefixingClassReader extends ClassReader { + private final String prefix; + + PrefixingClassReader(InputStream content, String prefix) throws IOException { + super(content); + this.prefix = prefix; + } + + @Override + public void accept(ClassVisitor cv, Attribute[] attrs, int flags) { + cv = + new ClassRemapper( + cv, + new Remapper() { + @Override + public String map(String typeName) { + return prefix(typeName); + } + }); + super.accept(cv, attrs, flags); + } + + @Override + public String getClassName() { + return prefix(super.getClassName()); + } + + @Override + public String getSuperName() { + String result = super.getSuperName(); + return result != null ? prefix(result) : null; + } + + @Override + public String[] getInterfaces() { + String[] result = super.getInterfaces(); + for (int i = 0, len = result.length; i < len; ++i) { + result[i] = prefix(result[i]); + } + return result; + } + + /** Prefixes core library class names with prefix. */ + private String prefix(String typeName) { + if (shouldPrefix(typeName)) { + return prefix + typeName; + } + return typeName; + } + } + + /** + * ClassVisitor that delegates to a ClassWriter, but removes a prefix as each class is written. + * The unprefixing is optimized out if prefix is empty. + */ + public class UnprefixingClassWriter extends ClassVisitor { + private final ClassWriter writer; + + private String finalClassName; + + UnprefixingClassWriter(int flags) { + super(Opcodes.ASM6); + this.writer = new ClassWriter(flags); + this.cv = this.writer; + if (!prefix.isEmpty()) { + this.cv = + new ClassRemapper( + this.writer, + new Remapper() { + @Override + public String map(String typeName) { + return unprefix(typeName); + } + }); + } + } + + /** Returns the (unprefixed) name of the class once written. */ + @Nullable + public String getClassName() { + return finalClassName; + } + + public byte[] toByteArray() { + return writer.toByteArray(); + } + + @Override + public void visit( + int version, + int access, + String name, + String signature, + String superName, + String[] interfaces) { + finalClassName = unprefix(name); + super.visit(version, access, name, signature, superName, interfaces); + } + } +} diff --git a/java/com/google/devtools/build/android/desugar/io/DirectoryInputFileProvider.java b/java/com/google/devtools/build/android/desugar/io/DirectoryInputFileProvider.java new file mode 100644 index 0000000..c607b42 --- /dev/null +++ b/java/com/google/devtools/build/android/desugar/io/DirectoryInputFileProvider.java @@ -0,0 +1,81 @@ +// 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.io; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOError; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Stream; +import java.util.zip.ZipEntry; + +/** Input provider is a directory. */ +class DirectoryInputFileProvider implements InputFileProvider { + + private final Path root; + + public DirectoryInputFileProvider(Path root) { + this.root = root; + } + + @Override + public String toString() { + return root.getFileName().toString(); + } + + @Override + public InputStream getInputStream(String filename) throws IOException { + return new FileInputStream(root.resolve(filename).toFile()); + } + + @Override + public ZipEntry getZipEntry(String filename) { + ZipEntry destEntry = new ZipEntry(filename); + destEntry.setTime(0L); // Use stable timestamp Jan 1 1980 + return destEntry; + } + + @Override + public void close() throws IOException { + // Nothing to close + } + + @Override + public Iterator iterator() { + final List entries = new ArrayList<>(); + try (Stream paths = Files.walk(root)) { + paths.forEach( + new Consumer() { + @Override + public void accept(Path t) { + if (Files.isRegularFile(t)) { + // Internally, we use '/' as a common package separator in filename to abstract + // that filename can comes from a zip or a directory. + entries.add(root.relativize(t).toString().replace(File.separatorChar, '/')); + } + } + }); + } catch (IOException e) { + throw new IOError(e); + } + return entries.iterator(); + } +} diff --git a/java/com/google/devtools/build/android/desugar/io/DirectoryOutputFileProvider.java b/java/com/google/devtools/build/android/desugar/io/DirectoryOutputFileProvider.java new file mode 100644 index 0000000..f8e87cb --- /dev/null +++ b/java/com/google/devtools/build/android/desugar/io/DirectoryOutputFileProvider.java @@ -0,0 +1,59 @@ +// 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.io; + +import com.google.common.io.ByteStreams; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; + +/** Output provider is a directory. */ +class DirectoryOutputFileProvider implements OutputFileProvider { + + private final Path root; + + public DirectoryOutputFileProvider(Path root) { + this.root = root; + } + + @Override + public void copyFrom(String filename, InputFileProvider inputFileProvider) throws IOException { + Path path = root.resolve(filename); + createParentFolder(path); + try (InputStream is = inputFileProvider.getInputStream(filename); + OutputStream os = Files.newOutputStream(path)) { + ByteStreams.copy(is, os); + } + } + + @Override + public void write(String filename, byte[] content) throws IOException { + Path path = root.resolve(filename); + createParentFolder(path); + Files.write(path, content); + } + + @Override + public void close() { + // Nothing to close + } + + private void createParentFolder(Path path) throws IOException { + if (!Files.exists(path.getParent())) { + Files.createDirectories(path.getParent()); + } + } +} diff --git a/java/com/google/devtools/build/android/desugar/io/FieldInfo.java b/java/com/google/devtools/build/android/desugar/io/FieldInfo.java new file mode 100644 index 0000000..0b4f634 --- /dev/null +++ b/java/com/google/devtools/build/android/desugar/io/FieldInfo.java @@ -0,0 +1,29 @@ +// Copyright 2016 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package com.google.devtools.build.android.desugar.io; + +import com.google.auto.value.AutoValue; + +/** A value class to store the fields information. */ +@AutoValue +public abstract class FieldInfo { + + public static FieldInfo create(String owner, String name, String desc) { + return new AutoValue_FieldInfo(owner, name, desc); + } + + public abstract String owner(); + public abstract String name(); + public abstract String desc(); +} diff --git a/java/com/google/devtools/build/android/desugar/io/HeaderClassLoader.java b/java/com/google/devtools/build/android/desugar/io/HeaderClassLoader.java new file mode 100644 index 0000000..f70dc0e --- /dev/null +++ b/java/com/google/devtools/build/android/desugar/io/HeaderClassLoader.java @@ -0,0 +1,234 @@ +// Copyright 2016 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package com.google.devtools.build.android.desugar.io; + +import com.google.common.collect.ImmutableList; +import java.io.IOError; +import java.io.IOException; +import java.io.InputStream; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +/** + * Class loader that can "load" classes from header Jars. This class loader stubs in missing code + * attributes on the fly to make {@link ClassLoader#defineClass} happy. Classes loaded are unusable + * other than to resolve method references, so this class loader should only be used to process or + * inspect classes, not to execute their code. Also note that the resulting classes may be missing + * private members, which header Jars may omit. + * + * @see java.net.URLClassLoader + */ +public class HeaderClassLoader extends ClassLoader { + + private final IndexedInputs indexedInputs; + private final CoreLibraryRewriter rewriter; + + public HeaderClassLoader( + IndexedInputs indexedInputs, CoreLibraryRewriter rewriter, ClassLoader parent) { + super(parent); + this.rewriter = rewriter; + this.indexedInputs = indexedInputs; + } + + @Override + protected Class findClass(String name) throws ClassNotFoundException { + String filename = rewriter.unprefix(name.replace('.', '/') + ".class"); + InputFileProvider inputFileProvider = indexedInputs.getInputFileProvider(filename); + if (inputFileProvider == null) { + throw new ClassNotFoundException("Class " + name + " not found"); + } + byte[] bytecode; + try (InputStream content = inputFileProvider.getInputStream(filename)) { + ClassReader reader = rewriter.reader(content); + // Have ASM compute maxs so we don't need to figure out how many formal parameters there are + ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS); + ImmutableList interfaceFieldNames = getFieldsIfReaderIsInterface(reader); + // TODO(kmb): Consider SKIP_CODE and stubbing everything so class loader doesn't verify code + reader.accept(new CodeStubber(writer, interfaceFieldNames), ClassReader.SKIP_DEBUG); + bytecode = writer.toByteArray(); + } catch (IOException e) { + throw new IOError(e); + } + return defineClass(name, bytecode, 0, bytecode.length); + } + + /** + * If the {@code reader} is an interface, then extract all the declared fields in it. Otherwise, + * return an empty list. + */ + private static ImmutableList getFieldsIfReaderIsInterface(ClassReader reader) { + if (BitFlags.isSet(reader.getAccess(), Opcodes.ACC_INTERFACE)) { + NonPrimitiveFieldCollector collector = new NonPrimitiveFieldCollector(); + reader.accept(collector, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG); + return collector.declaredNonPrimitiveFields.build(); + } + return ImmutableList.of(); + } + + /** Collect the fields defined in a class. */ + private static class NonPrimitiveFieldCollector extends ClassVisitor { + + final ImmutableList.Builder declaredNonPrimitiveFields = ImmutableList.builder(); + private String internalName; + + public NonPrimitiveFieldCollector() { + super(Opcodes.ASM6); + } + + @Override + public void visit( + int version, + int access, + String name, + String signature, + String superName, + String[] interfaces) { + super.visit(version, access, name, signature, superName, interfaces); + this.internalName = name; + } + + @Override + public FieldVisitor visitField( + int access, String name, String desc, String signature, Object value) { + if (isNonPrimitiveType(desc)) { + declaredNonPrimitiveFields.add(FieldInfo.create(internalName, name, desc)); + } + return null; + } + + private static boolean isNonPrimitiveType(String type) { + char firstChar = type.charAt(0); + return firstChar == '[' || firstChar == 'L'; + } + } + + + /** + * Class visitor that stubs in missing code attributes, and erases the body of the static + * initializer of functional interfaces if the interfaces have default methods. The erasion of the + * clinit is mainly because when we are desugaring lambdas, we need to load the functional + * interfaces via class loaders, and since the interfaces have default methods, according to the + * JVM spec, these interfaces will be executed. This should be prevented due to security concerns. + */ + private static class CodeStubber extends ClassVisitor { + + private String internalName; + private boolean isInterface; + private final ImmutableList interfaceFields; + + public CodeStubber(ClassVisitor cv, ImmutableList interfaceFields) { + super(Opcodes.ASM6, cv); + this.interfaceFields = interfaceFields; + } + + @Override + public void visit( + int version, + int access, + String name, + String signature, + String superName, + String[] interfaces) { + super.visit(version, access, name, signature, superName, interfaces); + isInterface = BitFlags.isSet(access, Opcodes.ACC_INTERFACE); + internalName = name; + } + + @Override + public MethodVisitor visitMethod( + int access, String name, String desc, String signature, String[] exceptions) { + MethodVisitor dest = super.visitMethod(access, name, desc, signature, exceptions); + if ((access & (Opcodes.ACC_ABSTRACT | Opcodes.ACC_NATIVE)) != 0) { + // No need to stub out abstract or native methods + return dest; + } + if (isInterface && "".equals(name)) { + // Delete class initializers, to avoid code gets executed when we desugar lambdas. + // See b/62184142 + return new InterfaceInitializerEraser(dest, internalName, interfaceFields); + } + return new BodyStubber(dest); + } + } + + /** + * Erase the static initializer of an interface. Given an interface with non-primitive fields, + * this eraser discards the original body of clinit, and initializes each non-primitive field to + * null + */ + private static class InterfaceInitializerEraser extends MethodVisitor { + + private final MethodVisitor dest; + private final ImmutableList interfaceFields; + + public InterfaceInitializerEraser( + MethodVisitor mv, String internalName, ImmutableList interfaceFields) { + super(Opcodes.ASM6); + dest = mv; + this.interfaceFields = interfaceFields; + } + + @Override + public void visitCode() { + dest.visitCode(); + } + + @Override + public void visitEnd() { + for (FieldInfo fieldInfo : interfaceFields) { + dest.visitInsn(Opcodes.ACONST_NULL); + dest.visitFieldInsn( + Opcodes.PUTSTATIC, fieldInfo.owner(), fieldInfo.name(), fieldInfo.desc()); + } + dest.visitInsn(Opcodes.RETURN); + dest.visitMaxs(0, 0); + dest.visitEnd(); + } + } + + /** Method visitor used by {@link CodeStubber} to put code into methods without code. */ + private static class BodyStubber extends MethodVisitor { + + private static final String EXCEPTION_INTERNAL_NAME = "java/lang/UnsupportedOperationException"; + + private boolean hasCode = false; + + public BodyStubber(MethodVisitor mv) { + super(Opcodes.ASM6, mv); + } + + @Override + public void visitCode() { + hasCode = true; + super.visitCode(); + } + + @Override + public void visitEnd() { + if (!hasCode) { + super.visitTypeInsn(Opcodes.NEW, EXCEPTION_INTERNAL_NAME); + super.visitInsn(Opcodes.DUP); + super.visitMethodInsn( + Opcodes.INVOKESPECIAL, EXCEPTION_INTERNAL_NAME, "", "()V", /*itf*/ false); + super.visitInsn(Opcodes.ATHROW); + super.visitMaxs(0, 0); // triggers computation of the actual max's + } + super.visitEnd(); + } + } +} diff --git a/java/com/google/devtools/build/android/desugar/io/IndexedInputs.java b/java/com/google/devtools/build/android/desugar/io/IndexedInputs.java new file mode 100644 index 0000000..8ce4b62 --- /dev/null +++ b/java/com/google/devtools/build/android/desugar/io/IndexedInputs.java @@ -0,0 +1,94 @@ +// 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.io; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; + +import com.google.common.collect.ImmutableMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.annotation.CheckReturnValue; +import javax.annotation.Nullable; + +/** + * Opens the given list of input files and compute an index of all classes in them, to avoid + * scanning all inputs over and over for each class to load. An indexed inputs can have a parent + * that is firstly used when a file name is searched. + */ +public class IndexedInputs { + + private final ImmutableMap inputFiles; + + /** + * Parent {@link IndexedInputs} to use before to search a file name into this {@link + * IndexedInputs}. + */ + @Nullable + private final IndexedInputs parent; + + /** Index a list of input files without a parent {@link IndexedInputs}. */ + public IndexedInputs(List inputProviders) { + this.parent = null; + this.inputFiles = indexInputs(inputProviders); + } + + /** + * Create a new {@link IndexedInputs} with input files previously indexed and with a parent {@link + * IndexedInputs}. + */ + private IndexedInputs( + ImmutableMap inputFiles, IndexedInputs parentIndexedInputs) { + this.parent = parentIndexedInputs; + this.inputFiles = inputFiles; + } + + /** + * Create a new {@link IndexedInputs} with input files already indexed and with a parent {@link + * IndexedInputs}. + */ + @CheckReturnValue + public IndexedInputs withParent(IndexedInputs parent) { + checkState(this.parent == null); + return new IndexedInputs(this.inputFiles, parent); + } + + @Nullable + public InputFileProvider getInputFileProvider(String filename) { + checkArgument(filename.endsWith(".class")); + + if (parent != null) { + InputFileProvider inputFileProvider = parent.getInputFileProvider(filename); + if (inputFileProvider != null) { + return inputFileProvider; + } + } + + return inputFiles.get(filename); + } + + private ImmutableMap indexInputs( + List inputProviders) { + Map indexedInputs = new HashMap<>(); + for (InputFileProvider inputProvider : inputProviders) { + for (String relativePath : inputProvider) { + if (relativePath.endsWith(".class") && !indexedInputs.containsKey(relativePath)) { + indexedInputs.put(relativePath, inputProvider); + } + } + } + return ImmutableMap.copyOf(indexedInputs); + } +} diff --git a/java/com/google/devtools/build/android/desugar/io/InputFileProvider.java b/java/com/google/devtools/build/android/desugar/io/InputFileProvider.java new file mode 100644 index 0000000..c41d018 --- /dev/null +++ b/java/com/google/devtools/build/android/desugar/io/InputFileProvider.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.io; + +import com.google.errorprone.annotations.MustBeClosed; +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.zip.ZipEntry; + +/** Input file provider allows to iterate on relative path filename of a directory or a jar file. */ +public interface InputFileProvider extends Closeable, Iterable { + + /** + * Return a ZipEntry for {@code filename}. If the provider is a {@link ZipInputFileProvider}, the + * method returns the existing ZipEntry in order to keep its metadata, otherwise a new one is + * created. + */ + ZipEntry getZipEntry(String filename); + + /** + * This method returns an input stream allowing to read the file {@code filename}, it is the + * responsibility of the caller to close this stream. + */ + InputStream getInputStream(String filename) throws IOException; + + /** Transform a Path to an InputFileProvider that needs to be closed by the caller. */ + @MustBeClosed + public static InputFileProvider open(Path path) throws IOException { + if (Files.isDirectory(path)) { + return new DirectoryInputFileProvider(path); + } else { + return new ZipInputFileProvider(path); + } + } +} diff --git a/java/com/google/devtools/build/android/desugar/io/OutputFileProvider.java b/java/com/google/devtools/build/android/desugar/io/OutputFileProvider.java new file mode 100644 index 0000000..e693786 --- /dev/null +++ b/java/com/google/devtools/build/android/desugar/io/OutputFileProvider.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.io; + +import com.google.errorprone.annotations.MustBeClosed; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +/** Output file provider allows to write files in directory or jar files. */ +public interface OutputFileProvider extends AutoCloseable { + + /** Filename to use to write out dependency metadata for later consistency checking. */ + public static final String DESUGAR_DEPS_FILENAME = "META-INF/desugar_deps"; + + /** + * Copy {@code filename} from {@code inputFileProvider} to this output. If input file provider is + * a {@link ZipInputFileProvider} then the metadata of the zip entry are kept. + */ + void copyFrom(String filename, InputFileProvider inputFileProvider) throws IOException; + + /** Write {@code content} in {@code filename} to this output */ + void write(String filename, byte[] content) throws IOException; + + /** Transform a Path to an {@link OutputFileProvider} */ + @MustBeClosed + public static OutputFileProvider create(Path path) throws IOException { + if (Files.isDirectory(path)) { + return new DirectoryOutputFileProvider(path); + } else { + return new ZipOutputFileProvider(path); + } + } +} diff --git a/java/com/google/devtools/build/android/desugar/io/ThrowingClassLoader.java b/java/com/google/devtools/build/android/desugar/io/ThrowingClassLoader.java new file mode 100644 index 0000000..16f83f2 --- /dev/null +++ b/java/com/google/devtools/build/android/desugar/io/ThrowingClassLoader.java @@ -0,0 +1,27 @@ +// Copyright 2016 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package com.google.devtools.build.android.desugar.io; + +/** Class loader that throws whenever it can, for use the parent of a class loader hierarchy. */ +public class ThrowingClassLoader extends ClassLoader { + @Override + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + if (name.startsWith("java.")) { + // Use system class loader for java. classes, since ClassLoader.defineClass gets + // grumpy when those don't come from the standard place. + return super.loadClass(name, resolve); + } + throw new ClassNotFoundException(); + } +} \ No newline at end of file diff --git a/java/com/google/devtools/build/android/desugar/io/ZipInputFileProvider.java b/java/com/google/devtools/build/android/desugar/io/ZipInputFileProvider.java new file mode 100644 index 0000000..9bd7758 --- /dev/null +++ b/java/com/google/devtools/build/android/desugar/io/ZipInputFileProvider.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.io; + +import com.google.common.base.Functions; +import com.google.common.collect.Iterators; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Path; +import java.util.Iterator; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +/** Input provider is a zip file. */ +class ZipInputFileProvider implements InputFileProvider { + + private final Path root; + + private final ZipFile zipFile; + + public ZipInputFileProvider(Path root) throws IOException { + this.root = root; + this.zipFile = new ZipFile(root.toFile()); + } + + @Override + public void close() throws IOException { + zipFile.close(); + } + + @Override + public String toString() { + return root.getFileName().toString(); + } + + @Override + public ZipEntry getZipEntry(String filename) { + ZipEntry zipEntry = zipFile.getEntry(filename); + zipEntry.setCompressedSize(-1); + return zipEntry; + } + + @Override + public InputStream getInputStream(String filename) throws IOException { + return zipFile.getInputStream(zipFile.getEntry(filename)); + } + + @Override + public Iterator iterator() { + return Iterators.transform( + Iterators.forEnumeration(zipFile.entries()), Functions.toStringFunction()); + } +} diff --git a/java/com/google/devtools/build/android/desugar/io/ZipOutputFileProvider.java b/java/com/google/devtools/build/android/desugar/io/ZipOutputFileProvider.java new file mode 100644 index 0000000..36cb26d --- /dev/null +++ b/java/com/google/devtools/build/android/desugar/io/ZipOutputFileProvider.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.io; + +import static com.google.common.base.Preconditions.checkArgument; + +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.util.zip.CRC32; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +/** Output provider is a zip file. */ +class ZipOutputFileProvider implements OutputFileProvider { + + private final ZipOutputStream out; + + public ZipOutputFileProvider(Path root) throws IOException { + out = new ZipOutputStream(new BufferedOutputStream(Files.newOutputStream(root))); + } + + @Override + public void copyFrom(String filename, InputFileProvider inputFileProvider) throws IOException { + // TODO(bazel-team): Avoid de- and re-compressing resource files + out.putNextEntry(inputFileProvider.getZipEntry(filename)); + try (InputStream is = inputFileProvider.getInputStream(filename)) { + ByteStreams.copy(is, out); + } + out.closeEntry(); + } + + @Override + public void write(String filename, byte[] content) throws IOException { + checkArgument(filename.equals(DESUGAR_DEPS_FILENAME) || filename.endsWith(".class"), + "Expect file to be copied: %s", filename); + writeStoredEntry(out, filename, content); + } + + @Override + public void close() throws IOException { + out.close(); + } + + private static void writeStoredEntry(ZipOutputStream out, String filename, byte[] content) + throws IOException { + // Need to pre-compute checksum for STORED (uncompressed) entries) + CRC32 checksum = new CRC32(); + checksum.update(content); + + ZipEntry result = new ZipEntry(filename); + result.setTime(0L); // Use stable timestamp Jan 1 1980 + result.setCrc(checksum.getValue()); + result.setSize(content.length); + result.setCompressedSize(content.length); + // Write uncompressed, since this is just an intermediary artifact that + // we will convert to .dex + result.setMethod(ZipEntry.STORED); + + out.putNextEntry(result); + out.write(content); + out.closeEntry(); + } +} diff --git a/java/com/google/devtools/build/android/desugar/scan/KeepScanner.java b/java/com/google/devtools/build/android/desugar/scan/KeepScanner.java index b347c7a..4924f7c 100644 --- a/java/com/google/devtools/build/android/desugar/scan/KeepScanner.java +++ b/java/com/google/devtools/build/android/desugar/scan/KeepScanner.java @@ -15,13 +15,21 @@ package com.google.devtools.build.android.desugar.scan; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; import static java.nio.file.StandardOpenOption.CREATE; import static java.util.Comparator.comparing; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.io.ByteStreams; +import com.google.common.io.Closer; import com.google.devtools.build.android.Converters.ExistingPathConverter; import com.google.devtools.build.android.Converters.PathConverter; +import com.google.devtools.build.android.desugar.io.CoreLibraryRewriter; +import com.google.devtools.build.android.desugar.io.HeaderClassLoader; +import com.google.devtools.build.android.desugar.io.IndexedInputs; +import com.google.devtools.build.android.desugar.io.InputFileProvider; +import com.google.devtools.build.android.desugar.io.ThrowingClassLoader; import com.google.devtools.common.options.Option; import com.google.devtools.common.options.OptionDocumentationCategory; import com.google.devtools.common.options.OptionEffectTag; @@ -32,9 +40,11 @@ import java.io.IOError; import java.io.IOException; import java.io.InputStream; import java.io.PrintStream; +import java.lang.reflect.Method; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; +import java.util.List; import java.util.Map; import java.util.stream.Collectors; import java.util.zip.ZipEntry; @@ -56,6 +66,32 @@ class KeepScanner { ) public Path inputJars; + @Option( + name = "classpath_entry", + allowMultiple = true, + defaultValue = "", + documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, + effectTags = {OptionEffectTag.UNKNOWN}, + converter = ExistingPathConverter.class, + help = + "Ordered classpath (Jar or directory) to resolve symbols in the --input Jar, like " + + "javac's -cp flag." + ) + public List classpath; + + @Option( + name = "bootclasspath_entry", + allowMultiple = true, + defaultValue = "", + documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, + effectTags = {OptionEffectTag.UNKNOWN}, + converter = ExistingPathConverter.class, + help = + "Bootclasspath that was used to compile the --input Jar with, like javac's " + + "-bootclasspath flag (required)." + ) + public List bootclasspath; + @Option( name = "keep_file", defaultValue = "null", @@ -81,10 +117,25 @@ class KeepScanner { parser.setAllowResidue(false); parser.enableParamsFileSupport(new ShellQuotedParamsFilePreProcessor(FileSystems.getDefault())); parser.parseAndExitUponError(args); - KeepScannerOptions options = parser.getOptions(KeepScannerOptions.class); - Map> seeds = - scan(checkNotNull(options.inputJars), options.prefix); + + Map> seeds; + try (Closer closer = Closer.create()) { + // TODO(kmb): Try to share more of this code with Desugar binary + IndexedInputs classpath = + new IndexedInputs(toRegisteredInputFileProvider(closer, options.classpath)); + IndexedInputs bootclasspath = + new IndexedInputs(toRegisteredInputFileProvider(closer, options.bootclasspath)); + + // Construct classloader from classpath. Since we're assuming the prefix we're looking for + // isn't part of the input itself we shouldn't need to include the input in the classloader. + CoreLibraryRewriter noopRewriter = new CoreLibraryRewriter(""); + ClassLoader classloader = + new HeaderClassLoader(classpath, noopRewriter, + new HeaderClassLoader(bootclasspath, noopRewriter, + new ThrowingClassLoader())); + seeds = scan(checkNotNull(options.inputJars), options.prefix, classloader); + } try (PrintStream out = new PrintStream( @@ -117,11 +168,9 @@ class KeepScanner { }); } - /** - * Scans for and returns references with owners matching the given prefix grouped by owner. - */ - private static Map> scan(Path jarFile, String prefix) - throws IOException { + /** Scans for and returns references with owners matching the given prefix grouped by owner. */ + private static Map> scan( + Path jarFile, String prefix, ClassLoader classpath) throws IOException { // We read the Jar sequentially since ZipFile uses locks anyway but then allow scanning each // class in parallel. try (ZipFile zip = new ZipFile(jarFile.toFile())) { @@ -131,6 +180,8 @@ class KeepScanner { .parallel() .flatMap( content -> PrefixReferenceScanner.scan(new ClassReader(content), prefix).stream()) + .distinct() // so we don't process the same reference multiple times next + .map(ref -> nearestDeclaration(ref, classpath)) .collect( Collectors.groupingByConcurrent( KeepReference::internalName, ImmutableSet.toImmutableSet())); @@ -147,6 +198,68 @@ class KeepScanner { } } + /** + * Find the nearest definition of the given reference in the class hierarchy and return the + * modified reference. This is needed b/c bytecode sometimes refers to a method or field using + * an owner type that inherits the method or field instead of defining the member itself. + * In that case we need to find and keep the inherited definition. + */ + private static KeepReference nearestDeclaration(KeepReference ref, ClassLoader classpath) { + if (!ref.isMemberReference() || "".equals(ref.name())) { + return ref; // class and constructor references don't need any further work + } + + Class clazz; + try { + clazz = classpath.loadClass(ref.internalName().replace('/', '.')); + } catch (ClassNotFoundException e) { + throw (NoClassDefFoundError) new NoClassDefFoundError("Couldn't load " + ref).initCause(e); + } + + Class owner = findDeclaringClass(clazz, ref); + if (owner == clazz) { + return ref; + } + String parent = checkNotNull(owner, "Can't resolve: %s", ref).getName().replace('.', '/'); + return KeepReference.memberReference(parent, ref.name(), ref.desc()); + } + + private static Class findDeclaringClass(Class clazz, KeepReference ref) { + if (ref.isFieldReference()) { + try { + return clazz.getField(ref.name()).getDeclaringClass(); + } catch (NoSuchFieldException e) { + // field must be non-public, so search class hierarchy + do { + try { + return clazz.getDeclaredField(ref.name()).getDeclaringClass(); + } catch (NoSuchFieldException ignored) { + // fall through for clarity + } + clazz = clazz.getSuperclass(); + } while (clazz != null); + } + } else { + checkState(ref.isMethodReference()); + Type descriptor = Type.getMethodType(ref.desc()); + for (Method m : clazz.getMethods()) { + if (m.getName().equals(ref.name()) && Type.getType(m).equals(descriptor)) { + return m.getDeclaringClass(); + } + } + do { + // Method must be non-public, so search class hierarchy + for (Method m : clazz.getDeclaredMethods()) { + if (m.getName().equals(ref.name()) && Type.getType(m).equals(descriptor)) { + return m.getDeclaringClass(); + } + } + clazz = clazz.getSuperclass(); + } while (clazz != null); + } + return null; + } + private static CharSequence toKeepDescriptor(KeepReference member) { StringBuilder result = new StringBuilder(); if (member.isMethodReference()) { @@ -172,5 +285,19 @@ class KeepScanner { return result; } + /** + * Transform a list of Path to a list of InputFileProvider and register them with the given + * closer. + */ + @SuppressWarnings("MustBeClosedChecker") + private static ImmutableList toRegisteredInputFileProvider( + Closer closer, List paths) throws IOException { + ImmutableList.Builder builder = new ImmutableList.Builder<>(); + for (Path path : paths) { + builder.add(closer.register(InputFileProvider.open(path))); + } + return builder.build(); + } + private KeepScanner() {} } diff --git a/test/java/com/google/devtools/build/android/desugar/CoreLibrarySupportTest.java b/test/java/com/google/devtools/build/android/desugar/CoreLibrarySupportTest.java index ec4e16d..42f1f78 100644 --- a/test/java/com/google/devtools/build/android/desugar/CoreLibrarySupportTest.java +++ b/test/java/com/google/devtools/build/android/desugar/CoreLibrarySupportTest.java @@ -16,6 +16,7 @@ package com.google.devtools.build.android.desugar; import static com.google.common.truth.Truth.assertThat; import com.google.common.collect.ImmutableList; +import com.google.devtools.build.android.desugar.io.CoreLibraryRewriter; import java.util.Collection; import java.util.Comparator; import java.util.Map; diff --git a/test/java/com/google/devtools/build/android/desugar/CorePackageRenamerTest.java b/test/java/com/google/devtools/build/android/desugar/CorePackageRenamerTest.java index 2bdd58b..5220ed6 100644 --- a/test/java/com/google/devtools/build/android/desugar/CorePackageRenamerTest.java +++ b/test/java/com/google/devtools/build/android/desugar/CorePackageRenamerTest.java @@ -16,6 +16,7 @@ package com.google.devtools.build.android.desugar; import static com.google.common.truth.Truth.assertThat; import com.google.common.collect.ImmutableList; +import com.google.devtools.build.android.desugar.io.CoreLibraryRewriter; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; diff --git a/test/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixerTest.java b/test/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixerTest.java index faa6dda..406a36f 100644 --- a/test/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixerTest.java +++ b/test/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixerTest.java @@ -19,7 +19,10 @@ import static com.google.devtools.build.android.desugar.DefaultMethodClassFixer. import com.google.common.collect.ImmutableList; import com.google.common.io.Closer; -import com.google.devtools.build.android.desugar.Desugar.ThrowingClassLoader; +import com.google.devtools.build.android.desugar.io.CoreLibraryRewriter; +import com.google.devtools.build.android.desugar.io.HeaderClassLoader; +import com.google.devtools.build.android.desugar.io.IndexedInputs; +import com.google.devtools.build.android.desugar.io.ThrowingClassLoader; import java.io.File; import java.io.IOException; import java.io.Serializable; diff --git a/test/java/com/google/devtools/build/android/desugar/FieldInfoTest.java b/test/java/com/google/devtools/build/android/desugar/FieldInfoTest.java deleted file mode 100644 index afb2bea..0000000 --- a/test/java/com/google/devtools/build/android/desugar/FieldInfoTest.java +++ /dev/null @@ -1,33 +0,0 @@ -// 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 deleted file mode 100644 index bac3fc9..0000000 --- a/test/java/com/google/devtools/build/android/desugar/IndexedInputsTest.java +++ /dev/null @@ -1,136 +0,0 @@ -// 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 index 2eab943..99e51c1 100644 --- a/test/java/com/google/devtools/build/android/desugar/Java7CompatibilityTest.java +++ b/test/java/com/google/devtools/build/android/desugar/Java7CompatibilityTest.java @@ -16,6 +16,7 @@ package com.google.devtools.build.android.desugar; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; +import com.google.devtools.build.android.desugar.io.BitFlags; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; diff --git a/test/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriterTest.java b/test/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriterTest.java index 37afae7..dc0da22 100644 --- a/test/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriterTest.java +++ b/test/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriterTest.java @@ -25,6 +25,7 @@ import static org.objectweb.asm.Opcodes.ASM5; import static org.objectweb.asm.Opcodes.INVOKESTATIC; import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL; +import com.google.devtools.build.android.desugar.io.BitFlags; import com.google.devtools.build.android.desugar.runtime.ThrowableExtension; import com.google.devtools.build.android.desugar.testdata.ClassUsingTryWithResources; import java.io.IOException; diff --git a/test/java/com/google/devtools/build/android/desugar/io/FieldInfoTest.java b/test/java/com/google/devtools/build/android/desugar/io/FieldInfoTest.java new file mode 100644 index 0000000..0579822 --- /dev/null +++ b/test/java/com/google/devtools/build/android/desugar/io/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.io; + +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/io/IndexedInputsTest.java b/test/java/com/google/devtools/build/android/desugar/io/IndexedInputsTest.java new file mode 100644 index 0000000..81a4b31 --- /dev/null +++ b/test/java/com/google/devtools/build/android/desugar/io/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.io; + +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/scan/testdata/CollectionReferences.java b/test/java/com/google/devtools/build/android/desugar/scan/testdata/CollectionReferences.java index 482c32a..830364c 100644 --- a/test/java/com/google/devtools/build/android/desugar/scan/testdata/CollectionReferences.java +++ b/test/java/com/google/devtools/build/android/desugar/scan/testdata/CollectionReferences.java @@ -54,6 +54,10 @@ public class CollectionReferences { return result; } + public void expire(long before) { + dates.removeIf(d -> d.getTime() < before); + } + static { System.out.println("Hello!"); } diff --git a/test/java/com/google/devtools/build/android/desugar/scan/testdata_golden.txt b/test/java/com/google/devtools/build/android/desugar/scan/testdata_golden.txt index 35744ce..6082576 100644 --- a/test/java/com/google/devtools/build/android/desugar/scan/testdata_golden.txt +++ b/test/java/com/google/devtools/build/android/desugar/scan/testdata_golden.txt @@ -18,6 +18,19 @@ -keep class java.lang.System { *** out; } +-keep class java.lang.invoke.CallSite { +} +-keep class java.lang.invoke.LambdaMetafactory { + *** metafactory(java.lang.invoke.MethodHandles$Lookup, java.lang.String, java.lang.invoke.MethodType, java.lang.invoke.MethodType, java.lang.invoke.MethodHandle, java.lang.invoke.MethodType); +} +-keep class java.lang.invoke.MethodHandle { +} +-keep class java.lang.invoke.MethodHandles { +} +-keep class java.lang.invoke.MethodHandles$Lookup { +} +-keep class java.lang.invoke.MethodType { +} -keep class java.util.AbstractList { } -keep class java.util.ArrayList { @@ -28,6 +41,7 @@ *** iterator(); } -keep class java.util.Collection { + *** removeIf(java.util.function.Predicate); } -keep class java.util.Date { (long); @@ -44,3 +58,5 @@ *** get(int); *** iterator(); } +-keep class java.util.function.Predicate { +} -- cgit v1.2.3 From faa0690d2152c090607a8db136ba9104bc22bb4e Mon Sep 17 00:00:00 2001 From: kmb Date: Fri, 16 Mar 2018 18:52:15 -0700 Subject: Reflect core library moves in super calls, even in default method stubs. Always generate default method stubs for emulated methods. RELNOTES: None. PiperOrigin-RevId: 189423933 GitOrigin-RevId: 44a26afb091f2d23d68bcad53e45a319b299867a Change-Id: I8eaecb5a1a29051a14d0529005a56a225b2f4d8b --- .../desugar/CoreLibraryInvocationRewriter.java | 9 +++- .../build/android/desugar/CoreLibrarySupport.java | 45 +++++++++++++---- .../android/desugar/DefaultMethodClassFixer.java | 59 ++++++++++++++++++---- .../android/desugar/CoreLibrarySupportTest.java | 53 +++++++++++++++++++ 4 files changed, 145 insertions(+), 21 deletions(-) diff --git a/java/com/google/devtools/build/android/desugar/CoreLibraryInvocationRewriter.java b/java/com/google/devtools/build/android/desugar/CoreLibraryInvocationRewriter.java index 77db915..381a344 100644 --- a/java/com/google/devtools/build/android/desugar/CoreLibraryInvocationRewriter.java +++ b/java/com/google/devtools/build/android/desugar/CoreLibraryInvocationRewriter.java @@ -14,6 +14,7 @@ package com.google.devtools.build.android.desugar; import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import org.objectweb.asm.ClassVisitor; @@ -65,9 +66,13 @@ public class CoreLibraryInvocationRewriter extends ClassVisitor { } if (opcode == Opcodes.INVOKESTATIC || opcode == Opcodes.INVOKESPECIAL) { - checkArgument(itf, "Expected interface to rewrite %s.%s : %s", owner, name, desc); - owner = InterfaceDesugaring.getCompanionClassName(coreInterfaceName); + checkArgument(itf || opcode == Opcodes.INVOKESPECIAL, + "Expected interface to rewrite %s.%s : %s", owner, name, desc); + owner = coreInterface.isInterface() + ? InterfaceDesugaring.getCompanionClassName(coreInterfaceName) + : checkNotNull(support.getMoveTarget(coreInterfaceName, name)); } else { + checkState(coreInterface.isInterface()); owner = coreInterfaceName + "$$Dispatch"; } diff --git a/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java b/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java index fd10e5e..f247074 100644 --- a/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java +++ b/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java @@ -32,6 +32,7 @@ import java.lang.reflect.Method; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Objects; @@ -89,7 +90,8 @@ class CoreLibrarySupport { this.emulatedInterfaces = classBuilder.build(); // We can call isRenamed and rename below b/c we initialized the necessary fields above - ImmutableMap.Builder movesBuilder = ImmutableMap.builder(); + // Use LinkedHashMap to tolerate identical duplicates + LinkedHashMap movesBuilder = new LinkedHashMap<>(); Splitter splitter = Splitter.on("->").trimResults().omitEmptyStrings(); for (String move : memberMoves) { List pair = splitter.splitToList(move); @@ -103,9 +105,12 @@ class CoreLibrarySupport { checkArgument(!this.excludeFromEmulation.contains(pair.get(0)), "Retargeted invocation %s shouldn't overlap with excluded", move); - movesBuilder.put(pair.get(0), renameCoreLibrary(pair.get(1))); + String value = renameCoreLibrary(pair.get(1)); + String existing = movesBuilder.put(pair.get(0), value); + checkArgument(existing == null || existing.equals(value), + "Two move destinations %s and %s configured for %s", existing, value, pair.get(0)); } - this.memberMoves = movesBuilder.build(); + this.memberMoves = ImmutableMap.copyOf(movesBuilder); } public boolean isRenamedCoreLibrary(String internalName) { @@ -165,9 +170,13 @@ class CoreLibrarySupport { * core interface, this methods returns that interface. This is a helper method for * {@link CoreLibraryInvocationRewriter}. * - *

Always returns an interface (or {@code null}), even if {@code owner} is a class. Can only - * return non-{@code null} if {@code owner} is a core library type. + *

This method can only return non-{@code null} if {@code owner} is a core library type. + * It usually returns an emulated interface, unless the given invocation is a super-call to a + * core class's implementation of an emulated method that's being moved (other implementations + * of emulated methods in core classes are ignored). In that case the class is returned and the + * caller can use {@link #getMoveTarget} to find out where to redirect the invokespecial to. */ + // TODO(kmb): Rethink this API and consider combining it with getMoveTarget(). @Nullable public Class getCoreInterfaceRewritingTarget( int opcode, String owner, String name, String desc, boolean itf) { @@ -175,15 +184,20 @@ class CoreLibrarySupport { // Regular desugaring handles generated classes, no emulation is needed return null; } - if (!itf && (opcode == Opcodes.INVOKESTATIC || opcode == Opcodes.INVOKESPECIAL)) { - // Ignore staticly dispatched invocations on classes--they never need rewriting + if (!itf && opcode == Opcodes.INVOKESTATIC) { + // Ignore static invocations on classes--they never need rewriting (unless moved but that's + // handled separately). return null; } + if ("".equals(name)) { + return null; // Constructors aren't rewritten + } + Class clazz; if (isRenamedCoreLibrary(owner)) { // For renamed invocation targets we just need to do what InterfaceDesugaring does, that is, // only worry about invokestatic and invokespecial interface invocations; nothing to do for - // invokevirtual and invokeinterface. InterfaceDesugaring ignores bootclasspath interfaces, + // classes and invokeinterface. InterfaceDesugaring ignores bootclasspath interfaces, // so we have to do its work here for renamed interfaces. if (itf && (opcode == Opcodes.INVOKESTATIC || opcode == Opcodes.INVOKESPECIAL)) { @@ -213,6 +227,19 @@ class CoreLibrarySupport { if (isExcluded(callee)) { return null; } + + if (!itf && opcode == Opcodes.INVOKESPECIAL) { + // See if the invoked implementation is moved; note we ignore all other overrides in classes + Class impl = clazz; // we know clazz is not an interface because !itf + while (impl != null) { + String implName = impl.getName().replace('.', '/'); + if (getMoveTarget(implName, name) != null) { + return impl; + } + impl = impl.getSuperclass(); + } + } + Class result = callee.getDeclaringClass(); if (isRenamedCoreLibrary(result.getName().replace('.', '/')) || emulatedInterfaces.stream().anyMatch(emulated -> emulated.isAssignableFrom(result))) { @@ -232,7 +259,7 @@ class CoreLibrarySupport { checkState(!roots.hasNext(), "Ambiguous emulation substitute: %s", callee); return substitute; } else { - checkArgument(opcode != Opcodes.INVOKESPECIAL, + checkArgument(!itf || opcode != Opcodes.INVOKESPECIAL, "Couldn't resolve interface super call %s.super.%s : %s", owner, name, desc); } return null; diff --git a/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java b/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java index 960cfeb..f0fc6d1 100644 --- a/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java +++ b/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java @@ -233,11 +233,13 @@ public class DefaultMethodClassFixer extends ClassVisitor { // superclass is also rewritten and already implements this interface, so we _must_ skip it. continue; } - stubMissingDefaultAndBridgeMethods(interfaceToVisit.getName().replace('.', '/')); + stubMissingDefaultAndBridgeMethods( + interfaceToVisit.getName().replace('.', '/'), mayNeedStubsForSuperclass); } } - private void stubMissingDefaultAndBridgeMethods(String implemented) { + private void stubMissingDefaultAndBridgeMethods( + String implemented, boolean mayNeedStubsForSuperclass) { ClassReader bytecode; boolean isBootclasspath; if (bootclasspath.isKnown(implemented)) { @@ -258,7 +260,9 @@ public class DefaultMethodClassFixer extends ClassVisitor { "Couldn't find interface %s implemented by %s", implemented, internalName); isBootclasspath = false; } - bytecode.accept(new DefaultMethodStubber(isBootclasspath), ClassReader.SKIP_DEBUG); + bytecode.accept( + new DefaultMethodStubber(isBootclasspath, mayNeedStubsForSuperclass), + ClassReader.SKIP_DEBUG); } private Class loadFromInternal(String internalName) { @@ -281,7 +285,8 @@ public class DefaultMethodClassFixer extends ClassVisitor { } private void recordInheritedMethods() { - InstanceMethodRecorder recorder = new InstanceMethodRecorder(); + InstanceMethodRecorder recorder = + new InstanceMethodRecorder(mayNeedInterfaceStubsForEmulatedSuperclass()); String internalName = superName; while (internalName != null) { ClassReader bytecode = bootclasspath.readIfKnown(internalName); @@ -429,11 +434,15 @@ public class DefaultMethodClassFixer extends ClassVisitor { private class DefaultMethodStubber extends ClassVisitor { private final boolean isBootclasspathInterface; + private final boolean mayNeedStubsForSuperclass; + private String stubbedInterfaceName; - public DefaultMethodStubber(boolean isBootclasspathInterface) { + public DefaultMethodStubber( + boolean isBootclasspathInterface, boolean mayNeedStubsForSuperclass) { super(Opcodes.ASM6); this.isBootclasspathInterface = isBootclasspathInterface; + this.mayNeedStubsForSuperclass = mayNeedStubsForSuperclass; } @Override @@ -472,6 +481,21 @@ public class DefaultMethodClassFixer extends ClassVisitor { MethodVisitor stubMethod = DefaultMethodClassFixer.this.visitMethod(access, name, desc, (String) null, exceptions); + String receiverName = stubbedInterfaceName; + String owner = InterfaceDesugaring.getCompanionClassName(stubbedInterfaceName); + if (mayNeedStubsForSuperclass) { + // Reflect what CoreLibraryInvocationRewriter would do if it encountered a super-call to a + // moved implementation of an emulated method. Equivalent to emitting the invokespecial + // super call here and relying on CoreLibraryInvocationRewriter for the rest + Class emulatedImplementation = + coreLibrarySupport.getCoreInterfaceRewritingTarget( + Opcodes.INVOKESPECIAL, superName, name, desc, /*itf=*/ false); + if (emulatedImplementation != null && !emulatedImplementation.isInterface()) { + receiverName = emulatedImplementation.getName().replace('.', '/'); + owner = checkNotNull(coreLibrarySupport.getMoveTarget(receiverName, name)); + } + } + int slot = 0; stubMethod.visitVarInsn(Opcodes.ALOAD, slot++); // load the receiver Type neededType = Type.getMethodType(desc); @@ -481,10 +505,10 @@ public class DefaultMethodClassFixer extends ClassVisitor { } stubMethod.visitMethodInsn( Opcodes.INVOKESTATIC, - InterfaceDesugaring.getCompanionClassName(stubbedInterfaceName), + owner, name, - InterfaceDesugaring.companionDefaultMethodDescriptor(stubbedInterfaceName, desc), - /*itf*/ false); + InterfaceDesugaring.companionDefaultMethodDescriptor(receiverName, desc), + /*itf=*/ false); stubMethod.visitInsn(neededType.getReturnType().getOpcode(Opcodes.IRETURN)); stubMethod.visitMaxs(0, 0); // rely on class writer to compute these @@ -563,8 +587,13 @@ public class DefaultMethodClassFixer extends ClassVisitor { private class InstanceMethodRecorder extends ClassVisitor { - public InstanceMethodRecorder() { + private final boolean ignoreEmulatedMethods; + + private String className; + + public InstanceMethodRecorder(boolean ignoreEmulatedMethods) { super(Opcodes.ASM6); + this.ignoreEmulatedMethods = ignoreEmulatedMethods; } @Override @@ -576,13 +605,23 @@ public class DefaultMethodClassFixer extends ClassVisitor { String superName, String[] interfaces) { checkArgument(BitFlags.noneSet(access, Opcodes.ACC_INTERFACE)); + className = name; // updated every time we start visiting another superclass super.visit(version, access, name, signature, superName, interfaces); } @Override public MethodVisitor visitMethod( int access, String name, String desc, String signature, String[] exceptions) { - // TODO(kmb): what if this method only exists on some devices, e.g., ArrayList.spliterator? + if (ignoreEmulatedMethods + && BitFlags.noneSet(access, Opcodes.ACC_STATIC) // short-circuit + && coreLibrarySupport.getCoreInterfaceRewritingTarget( + Opcodes.INVOKEVIRTUAL, className, name, desc, /*itf=*/ false) + != null) { + // *don't* record emulated core library method implementations in immediate subclasses of + // emulated core library clasess so that they can be stubbed (since the inherited + // implementation may be missing at runtime). + return null; + } recordIfInstanceMethod(access, name, desc); return null; } diff --git a/test/java/com/google/devtools/build/android/desugar/CoreLibrarySupportTest.java b/test/java/com/google/devtools/build/android/desugar/CoreLibrarySupportTest.java index 42f1f78..9b43207 100644 --- a/test/java/com/google/devtools/build/android/desugar/CoreLibrarySupportTest.java +++ b/test/java/com/google/devtools/build/android/desugar/CoreLibrarySupportTest.java @@ -19,6 +19,7 @@ import com.google.common.collect.ImmutableList; import com.google.devtools.build.android.desugar.io.CoreLibraryRewriter; import java.util.Collection; import java.util.Comparator; +import java.util.LinkedHashMap; import java.util.Map; import java.util.concurrent.ConcurrentMap; import org.junit.Test; @@ -164,6 +165,58 @@ public class CoreLibrarySupportTest { .isNull(); } + @Test + public void testGetCoreInterfaceRewritingTarget_emulatedImplementationMoved() throws Exception { + CoreLibrarySupport support = + new CoreLibrarySupport( + new CoreLibraryRewriter(""), + Thread.currentThread().getContextClassLoader(), + ImmutableList.of("java/util/Moved"), + ImmutableList.of("java/util/Map"), + ImmutableList.of("java/util/LinkedHashMap#forEach->java/util/Moved"), + ImmutableList.of()); + assertThat( + support.getCoreInterfaceRewritingTarget( + Opcodes.INVOKEINTERFACE, + "java/util/Map", + "forEach", + "(Ljava/util/function/BiConsumer;)V", + true)) + .isEqualTo(Map.class); + assertThat( + support.getCoreInterfaceRewritingTarget( + Opcodes.INVOKESPECIAL, + "java/util/Map", + "forEach", + "(Ljava/util/function/BiConsumer;)V", + true)) + .isEqualTo(Map.class); + assertThat( + support.getCoreInterfaceRewritingTarget( + Opcodes.INVOKEVIRTUAL, + "java/util/LinkedHashMap", + "forEach", + "(Ljava/util/function/BiConsumer;)V", + false)) + .isEqualTo(Map.class); + assertThat( + support.getCoreInterfaceRewritingTarget( + Opcodes.INVOKESPECIAL, + "java/util/LinkedHashMap", + "forEach", + "(Ljava/util/function/BiConsumer;)V", + false)) + .isEqualTo(LinkedHashMap.class); + assertThat( + support.getCoreInterfaceRewritingTarget( + Opcodes.INVOKESPECIAL, + "java/util/HashMap", + "forEach", + "(Ljava/util/function/BiConsumer;)V", + false)) + .isEqualTo(Map.class); + } + @Test public void testGetCoreInterfaceRewritingTarget_abstractMethod() throws Exception { CoreLibrarySupport support = -- cgit v1.2.3 From 6227f0cb24cbe58be9847a356e97205a5ba17f61 Mon Sep 17 00:00:00 2001 From: kmb Date: Mon, 26 Mar 2018 18:38:07 -0700 Subject: stub simple core library bridge methods that only differ in return type RELNOTES: None. PiperOrigin-RevId: 190559240 GitOrigin-RevId: 327c74df7c3b4820a0620bf9696c3f88bffebda3 Change-Id: I0f9a4718ff0e8714e3133ecf0ef528bb7a039bba --- .../android/desugar/DefaultMethodClassFixer.java | 85 ++++++++++++++++++---- 1 file changed, 72 insertions(+), 13 deletions(-) diff --git a/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java b/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java index f0fc6d1..1de48bf 100644 --- a/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java +++ b/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java @@ -19,6 +19,8 @@ import static com.google.common.base.Preconditions.checkState; import com.google.common.collect.ImmutableList; import com.google.devtools.build.android.desugar.io.BitFlags; +import java.lang.reflect.Method; +import java.util.Arrays; import java.util.Comparator; import java.util.HashSet; import java.util.Iterator; @@ -513,27 +515,84 @@ public class DefaultMethodClassFixer extends ClassVisitor { stubMethod.visitMaxs(0, 0); // rely on class writer to compute these stubMethod.visitEnd(); - return null; + return null; // don't visit the visited interface's default method } else if (shouldStubAsBridgeDefaultMethod(access, name, desc)) { recordIfInstanceMethod(access, name, desc); + MethodVisitor stubMethod = + DefaultMethodClassFixer.this.visitMethod(access, name, desc, (String) null, exceptions); // If we're visiting a bootclasspath interface then we most likely don't have the code. // That means we can't just copy the method bodies as we're trying to do below. - checkState(!isBootclasspathInterface, - "TODO stub core interface %s bridge methods in %s", stubbedInterfaceName, internalName); - // For bridges we just copy their bodies instead of going through the companion class. - // Meanwhile, we also need to desugar the copied method bodies, so that any calls to - // interface methods are correctly handled. - return new InterfaceDesugaring.InterfaceInvocationRewriter( - DefaultMethodClassFixer.this.visitMethod(access, name, desc, (String) null, exceptions), - stubbedInterfaceName, - bootclasspath, - targetLoader, - depsCollector, - internalName); + if (isBootclasspathInterface) { + // Synthesize a "bridge" method that calls the true implementation + Method bridged = findBridgedMethod(name, desc); + checkState(bridged != null, + "TODO: Can't stub core interface bridge method %s.%s %s in %s", + stubbedInterfaceName, name, desc, internalName); + + int slot = 0; + stubMethod.visitVarInsn(Opcodes.ALOAD, slot++); // load the receiver + Type neededType = Type.getType(bridged); + for (Type arg : neededType.getArgumentTypes()) { + // TODO(b/73586397): insert downcasts if necessary + stubMethod.visitVarInsn(arg.getOpcode(Opcodes.ILOAD), slot); + slot += arg.getSize(); + } + // Just call the bridged method directly on the visited class using invokevirtual + stubMethod.visitMethodInsn( + Opcodes.INVOKEVIRTUAL, + internalName, + name, + neededType.getDescriptor(), + /*itf=*/ false); + stubMethod.visitInsn(neededType.getReturnType().getOpcode(Opcodes.IRETURN)); + + stubMethod.visitMaxs(0, 0); // rely on class writer to compute these + stubMethod.visitEnd(); + return null; // don't visit the visited interface's bridge method + } else { + // For bridges we just copy their bodies instead of going through the companion class. + // Meanwhile, we also need to desugar the copied method bodies, so that any calls to + // interface methods are correctly handled. + return new InterfaceDesugaring.InterfaceInvocationRewriter( + stubMethod, + stubbedInterfaceName, + bootclasspath, + targetLoader, + depsCollector, + internalName); + } } else { return null; // not a default or bridge method or the class already defines this method. } } + + /** + * Returns a non-bridge interface method with given name that a method with the given descriptor + * can bridge to, if any such method can be found. + */ + @Nullable + private Method findBridgedMethod(String name, String desc) { + Type[] paramTypes = Type.getArgumentTypes(desc); + Class itf = loadFromInternal(stubbedInterfaceName); + checkArgument(itf.isInterface(), "Should be an interface: %s", stubbedInterfaceName); + Method result = null; + for (Method m : itf.getDeclaredMethods()) { + if (m.isBridge()) { + continue; + } + if (!m.getName().equals(name)) { + continue; + } + // For now, only support specialized return types (which don't require casts) + // TODO(b/73586397): Make this work for other kinds of bridges in core library interfaces + if (Arrays.equals(paramTypes, Type.getArgumentTypes(m))) { + checkState(result == null, + "Found multiple bridge target %s and %s for descriptor %s", result, m, desc); + return result = m; + } + } + return result; + } } /** -- cgit v1.2.3 From 84e656e907d9ec9cc152359c56cdd676de581dfa Mon Sep 17 00:00:00 2001 From: cushon Date: Thu, 29 Mar 2018 11:21:27 -0700 Subject: Support source versions newer than 8 in Bazel's annotation processors This quiets some build warnings. PiperOrigin-RevId: 190958692 GitOrigin-RevId: eef80048e2c59e3be974144ce9cd90b9f90294fb Change-Id: Ibf4e681bfc1ef540c2012df32d2970ed71240e65 --- .../google/devtools/common/options/processor/OptionProcessor.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/java/com/google/devtools/common/options/processor/OptionProcessor.java b/java/com/google/devtools/common/options/processor/OptionProcessor.java index 76b9640..5190053 100644 --- a/java/com/google/devtools/common/options/processor/OptionProcessor.java +++ b/java/com/google/devtools/common/options/processor/OptionProcessor.java @@ -36,7 +36,6 @@ import javax.annotation.processing.Messager; import javax.annotation.processing.ProcessingEnvironment; import javax.annotation.processing.RoundEnvironment; import javax.annotation.processing.SupportedAnnotationTypes; -import javax.annotation.processing.SupportedSourceVersion; import javax.lang.model.SourceVersion; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.Element; @@ -74,7 +73,6 @@ import javax.tools.Diagnostic; *

These properties can be relied upon at runtime without additional checks. */ @SupportedAnnotationTypes({"com.google.devtools.common.options.Option"}) -@SupportedSourceVersion(SourceVersion.RELEASE_8) public final class OptionProcessor extends AbstractProcessor { private Types typeUtils; @@ -83,6 +81,11 @@ public final class OptionProcessor extends AbstractProcessor { private ImmutableMap> defaultConverters; private ImmutableMap, PrimitiveType> primitiveTypeMap; + @Override + public SourceVersion getSupportedSourceVersion() { + return SourceVersion.latestSupported(); + } + @Override public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); -- cgit v1.2.3 From 6530fbc66c699ebbbdde505d6f694cace7db728b Mon Sep 17 00:00:00 2001 From: ccalvarin Date: Fri, 30 Mar 2018 08:40:44 -0700 Subject: Remove category checking from incompatible changes. String categories are deprecated, replace this special-cased value with a specific OptionMetadata tag, TRIGGERED_BY_ALL_INCOMPATIBLE_CHANGES. RELNOTES: None. PiperOrigin-RevId: 191069412 GitOrigin-RevId: 78a5fcff8a311c71cfe163a40856f7413e346409 Change-Id: I1be6e8a8c592e0fa8ec29a631957d840f34a2113 --- .../com/google/devtools/common/options/OptionDefinition.java | 2 +- .../devtools/common/options/OptionFilterDescriptions.java | 3 +++ .../google/devtools/common/options/OptionMetadataTag.java | 12 +++++++++++- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/java/com/google/devtools/common/options/OptionDefinition.java b/java/com/google/devtools/common/options/OptionDefinition.java index 7b87744..e89234b 100644 --- a/java/com/google/devtools/common/options/OptionDefinition.java +++ b/java/com/google/devtools/common/options/OptionDefinition.java @@ -137,7 +137,7 @@ public class OptionDefinition implements Comparable { } /** {@link Option#expansionFunction()} ()} */ - Class getExpansionFunction() { + public Class getExpansionFunction() { return optionAnnotation.expansionFunction(); } diff --git a/java/com/google/devtools/common/options/OptionFilterDescriptions.java b/java/com/google/devtools/common/options/OptionFilterDescriptions.java index 2a7999d..4b4373a 100644 --- a/java/com/google/devtools/common/options/OptionFilterDescriptions.java +++ b/java/com/google/devtools/common/options/OptionFilterDescriptions.java @@ -169,6 +169,9 @@ public class OptionFilterDescriptions { OptionMetadataTag.DEPRECATED, "This option is deprecated. It might be that the feature it affects is deprecated, " + "or that another method of supplying the information is preferred.") + .put( + OptionMetadataTag.TRIGGERED_BY_ALL_INCOMPATIBLE_CHANGES, + "This option is triggered by the expansion option --all_incompatible_changes.") .put( OptionMetadataTag.HIDDEN, // Here for completeness, these options are UNDOCUMENTED. "This option should not be used by a user, and should not be logged.") diff --git a/java/com/google/devtools/common/options/OptionMetadataTag.java b/java/com/google/devtools/common/options/OptionMetadataTag.java index c511fa6..563aa3e 100644 --- a/java/com/google/devtools/common/options/OptionMetadataTag.java +++ b/java/com/google/devtools/common/options/OptionMetadataTag.java @@ -55,7 +55,17 @@ public enum OptionMetadataTag { * *

These should be in category {@code OptionDocumentationCategory.UNDOCUMENTED}. */ - INTERNAL(4); + INTERNAL(4), + + /** + * Options that are triggered by --all_incompatible_changes. + * + *

These must also be labelled {@link OptionMetadataTag#INCOMPATIBLE_CHANGE} and have the + * prefix --incompatible_. Note that the option name prefix is also a triggering case for the + * --all_incompatible_changes expansion, and so all options that start with the "incompatible_" + * prefix must have this tag. + */ + TRIGGERED_BY_ALL_INCOMPATIBLE_CHANGES(5); private final int value; -- cgit v1.2.3 From 2aa5142dbc0b8bf8981ad6d5038895ff203e2d6f Mon Sep 17 00:00:00 2001 From: ajmichael Date: Wed, 4 Apr 2018 11:54:28 -0700 Subject: Remove some deprecated resources flags. RELNOTES: None PiperOrigin-RevId: 191624839 GitOrigin-RevId: c4987159509cd8de3f0c4070b53ea1bf3b8278cd Change-Id: Iaf2947340b544491d975d64d19b5337be25a9ac6 --- .../google/devtools/build/android/Converters.java | 50 ---------------------- 1 file changed, 50 deletions(-) diff --git a/java/com/google/devtools/build/android/Converters.java b/java/com/google/devtools/build/android/Converters.java index e58dd2d..13911f9 100644 --- a/java/com/google/devtools/build/android/Converters.java +++ b/java/com/google/devtools/build/android/Converters.java @@ -21,7 +21,6 @@ import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList.Builder; import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Iterables; import com.google.devtools.build.android.aapt2.CompiledResources; import com.google.devtools.build.android.aapt2.StaticLibrary; import com.google.devtools.common.options.Converter; @@ -40,7 +39,6 @@ import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; -import javax.annotation.Nullable; /** * Some convenient converters used by android actions. Note: These are specific to android actions. @@ -205,42 +203,6 @@ public final class Converters { } } - /** - * Converter for a list of {@link DependencySymbolFileProvider}. Relies on {@code - * DependencySymbolFileProvider#valueOf(String)} to perform conversion and validation. - * - * @deprecated use multi-value flags and {@link DependencySymbolFileProviderConverter} instead. - */ - @Deprecated - public static class DependencySymbolFileProviderListConverter - implements Converter> { - - @Override - public List convert(String input) throws OptionsParsingException { - if (input.isEmpty()) { - return ImmutableList.of(); - } - try { - ImmutableList.Builder builder = ImmutableList.builder(); - for (String item : input.split(",")) { - builder.add(DependencySymbolFileProvider.valueOf(item)); - } - return builder.build(); - } catch (IllegalArgumentException e) { - throw new OptionsParsingException( - String.format("invalid DependencyAndroidData: %s", e.getMessage()), e); - } - } - - @Override - public String getTypeDescription() { - return String.format( - "a list of dependency android data in the format: %s[%s]", - DependencySymbolFileProvider.commandlineFormat("1"), - DependencySymbolFileProvider.commandlineFormat("2")); - } - } - /** * Converter for {@link Revision}. Relies on {@code Revision#parseRevision(String)} to perform * conversion and validation. @@ -319,18 +281,6 @@ public final class Converters { } } - public static List concatLists( - @Nullable List a, @Nullable List b) { - @SuppressWarnings("unchecked") - List la = (List) a; - @SuppressWarnings("unchecked") - List lb = (List) b; - if (la == null || la.isEmpty()) { - return (lb == null || lb.isEmpty()) ? ImmutableList.of() : lb; - } - return (lb == null || lb.isEmpty()) ? la : ImmutableList.copyOf(Iterables.concat(la, lb)); - } - /** * Validating converter for a list of Paths. A Path is considered valid if it resolves to a file. */ -- cgit v1.2.3 From 9174b52bca2837a8a472403f332a0501980fa54e Mon Sep 17 00:00:00 2001 From: ccalvarin Date: Mon, 9 Apr 2018 08:44:02 -0700 Subject: Remove alphabetical sorting of options in the canonical list. This was broken for --config. Doing this properly requires keeping the order in which the options were given, which could be done either by filtering the ordered list according to which values affect the final outcome or by tracking the order correctly. I picked the later: the option order was not explicitly tracked for expansions before but now it is. RELNOTES: canonicalize-flags no longer reorders the flags PiperOrigin-RevId: 192132260 GitOrigin-RevId: aa98bc29dae14119797febd447302842f4ac68af Change-Id: I82fb65d38569d4e5a9808f032da1ccc2304e2f18 --- .../devtools/common/options/OptionPriority.java | 77 +++++++++++++++------- .../devtools/common/options/OptionsParser.java | 4 +- .../devtools/common/options/OptionsParserImpl.java | 54 +++++++-------- .../devtools/common/options/OptionsProvider.java | 12 ++-- 4 files changed, 90 insertions(+), 57 deletions(-) diff --git a/java/com/google/devtools/common/options/OptionPriority.java b/java/com/google/devtools/common/options/OptionPriority.java index ec5d0d8..53f0d75 100644 --- a/java/com/google/devtools/common/options/OptionPriority.java +++ b/java/com/google/devtools/common/options/OptionPriority.java @@ -13,6 +13,7 @@ // limitations under the License. package com.google.devtools.common.options; +import com.google.common.collect.ImmutableList; import java.util.Objects; /** @@ -25,43 +26,63 @@ import java.util.Objects; */ public class OptionPriority implements Comparable { private final PriorityCategory priorityCategory; - private final int index; - private final boolean locked; + /** + * Each option that is passed explicitly has 0 ancestors, so it only has its command line index + * (or rc index, etc., depending on the category), but expanded options have the command line + * index of its parent and then its position within the options that were expanded at that point. + * Since options can expand to expanding options, and --config can expand to expansion options as + * well, this can technically go arbitrarily deep, but in practice this is very short, of length < + * 5, most commonly of length 1. + */ + private final ImmutableList priorityIndices; + + private boolean alreadyExpanded = false; - private OptionPriority(PriorityCategory priorityCategory, int index, boolean locked) { + private OptionPriority( + PriorityCategory priorityCategory, ImmutableList priorityIndices) { this.priorityCategory = priorityCategory; - this.index = index; - this.locked = locked; + this.priorityIndices = priorityIndices; } /** Get the first OptionPriority for that category. */ static OptionPriority lowestOptionPriorityAtCategory(PriorityCategory category) { - return new OptionPriority(category, 0, false); + return new OptionPriority(category, ImmutableList.of(0)); } /** * Get the priority for the option following this one. In normal, incremental option parsing, the - * returned priority would compareTo as after the current one. Does not increment locked + * returned priority would compareTo as after the current one. Does not increment ancestor * priorities. */ static OptionPriority nextOptionPriority(OptionPriority priority) { - if (priority.locked) { - return priority; - } - return new OptionPriority(priority.priorityCategory, priority.index + 1, false); + int lastElementPosition = priority.priorityIndices.size() - 1; + return new OptionPriority( + priority.priorityCategory, + ImmutableList.builder() + .addAll(priority.priorityIndices.subList(0, lastElementPosition)) + .add(priority.priorityIndices.get(lastElementPosition) + 1) + .build()); } /** - * Return a priority for this option that will avoid priority increases by calls to - * nextOptionPriority. + * Some options are expanded to other options, and the children options need to have their order + * preserved while maintaining their position between the options that flank the parent option. * - *

Some options are expanded in-place, and need to be all parsed at the priority of the - * original option. In this case, parsing one of these after another should not cause the option - * to be considered as higher priority than the ones before it (this would cause overlap between - * the expansion of --expansion_flag and a option following it in the same list of options). + * @return the priority for the first child of the passed priority. This child's ordering can be + * tracked the same way that the parent's was. */ - public static OptionPriority getLockedPriority(OptionPriority priority) { - return new OptionPriority(priority.priorityCategory, priority.index, true); + public static OptionPriority getChildPriority(OptionPriority parentPriority) + throws OptionsParsingException { + if (parentPriority.alreadyExpanded) { + throw new OptionsParsingException("Tried to expand option too many times"); + } + // Prevent this option from being re-expanded. + parentPriority.alreadyExpanded = true; + + // The child priority has 1 more level of nesting than its parent. + return new OptionPriority( + parentPriority.priorityCategory, + ImmutableList.builder().addAll(parentPriority.priorityIndices).add(0).build()); } public PriorityCategory getPriorityCategory() { @@ -71,28 +92,36 @@ public class OptionPriority implements Comparable { @Override public int compareTo(OptionPriority o) { if (priorityCategory.equals(o.priorityCategory)) { - return index - o.index; + for (int i = 0; i < priorityIndices.size() && i < o.priorityIndices.size(); ++i) { + if (!priorityIndices.get(i).equals(o.priorityIndices.get(i))) { + return priorityIndices.get(i).compareTo(o.priorityIndices.get(i)); + } + } + // The values are up to the shorter one's length are the same, so the shorter one is a direct + // ancestor and comes first. + return Integer.compare(priorityIndices.size(), o.priorityIndices.size()); } - return priorityCategory.ordinal() - o.priorityCategory.ordinal(); + return Integer.compare(priorityCategory.ordinal(), o.priorityCategory.ordinal()); } @Override public boolean equals(Object o) { if (o instanceof OptionPriority) { OptionPriority other = (OptionPriority) o; - return other.priorityCategory.equals(priorityCategory) && other.index == index; + return priorityCategory.equals(other.priorityCategory) + && priorityIndices.equals(other.priorityIndices); } return false; } @Override public int hashCode() { - return Objects.hash(priorityCategory, index); + return Objects.hash(priorityCategory, priorityIndices); } @Override public String toString() { - return String.format("OptionPriority(%s,%s)", priorityCategory, index); + return String.format("OptionPriority(%s,%s)", priorityCategory, priorityIndices); } /** diff --git a/java/com/google/devtools/common/options/OptionsParser.java b/java/com/google/devtools/common/options/OptionsParser.java index b7da004..e75f0e1 100644 --- a/java/com/google/devtools/common/options/OptionsParser.java +++ b/java/com/google/devtools/common/options/OptionsParser.java @@ -629,7 +629,7 @@ public class OptionsParser implements OptionsProvider { * @param args the arguments to parse as the expansion. Order matters, as the value of a flag may * be in the following argument. */ - public void parseArgsFixedAsExpansionOfOption( + public void parseArgsAsExpansionOfOption( ParsedOptionDescription optionToExpand, String source, List args) throws OptionsParsingException { Preconditions.checkNotNull( @@ -638,7 +638,7 @@ public class OptionsParser implements OptionsProvider { optionToExpand.getPriority().getPriorityCategory() != OptionPriority.PriorityCategory.DEFAULT, "Priority cannot be default, which was specified for arglist " + args); - residue.addAll(impl.parseArgsFixedAsExpansionOfOption(optionToExpand, o -> source, args)); + residue.addAll(impl.parseArgsAsExpansionOfOption(optionToExpand, o -> source, args)); if (!allowResidue && !residue.isEmpty()) { String errorMsg = "Unrecognized arguments: " + Joiner.on(' ').join(residue); throw new OptionsParsingException(errorMsg); diff --git a/java/com/google/devtools/common/options/OptionsParserImpl.java b/java/com/google/devtools/common/options/OptionsParserImpl.java index bc66cc3..2c15430 100644 --- a/java/com/google/devtools/common/options/OptionsParserImpl.java +++ b/java/com/google/devtools/common/options/OptionsParserImpl.java @@ -142,9 +142,10 @@ class OptionsParserImpl { return optionValues .keySet() .stream() - .sorted() .map(optionDefinition -> optionValues.get(optionDefinition).getCanonicalInstances()) .flatMap(Collection::stream) + // Return the effective (canonical) options in the order they were applied. + .sorted(comparing(ParsedOptionDescription::getPriority)) .collect(ImmutableList.toImmutableList()); } @@ -207,30 +208,30 @@ class OptionsParserImpl { OptionDefinition expansionFlagDef, OptionInstanceOrigin originOfExpansionFlag) throws OptionsParsingException { ImmutableList.Builder builder = ImmutableList.builder(); - OptionInstanceOrigin originOfSubflags; + + // Values needed to correctly track the origin of the expanded options. + OptionPriority nextOptionPriority = + OptionPriority.getChildPriority(originOfExpansionFlag.getPriority()); + String source; + ParsedOptionDescription implicitDependent = null; + ParsedOptionDescription expandedFrom = null; + ImmutableList options; ParsedOptionDescription expansionFlagParsedDummy = ParsedOptionDescription.newDummyInstance(expansionFlagDef, originOfExpansionFlag); if (expansionFlagDef.hasImplicitRequirements()) { options = ImmutableList.copyOf(expansionFlagDef.getImplicitRequirements()); - originOfSubflags = - new OptionInstanceOrigin( - originOfExpansionFlag.getPriority(), - String.format( - "implicitly required by %s (source: %s)", - expansionFlagDef, originOfExpansionFlag.getSource()), - expansionFlagParsedDummy, - null); + source = + String.format( + "implicitly required by %s (source: %s)", + expansionFlagDef, originOfExpansionFlag.getSource()); + implicitDependent = expansionFlagParsedDummy; } else if (expansionFlagDef.isExpansionOption()) { options = optionsData.getEvaluatedExpansion(expansionFlagDef); - originOfSubflags = - new OptionInstanceOrigin( - originOfExpansionFlag.getPriority(), - String.format( - "expanded by %s (source: %s)", - expansionFlagDef, originOfExpansionFlag.getSource()), - null, - expansionFlagParsedDummy); + source = + String.format( + "expanded by %s (source: %s)", expansionFlagDef, originOfExpansionFlag.getSource()); + expandedFrom = expansionFlagParsedDummy; } else { return ImmutableList.of(); } @@ -242,11 +243,12 @@ class OptionsParserImpl { identifyOptionAndPossibleArgument( unparsedFlagExpression, optionsIterator, - originOfSubflags.getPriority(), - o -> originOfSubflags.getSource(), - originOfSubflags.getImplicitDependent(), - originOfSubflags.getExpandedFrom()); + nextOptionPriority, + o -> source, + implicitDependent, + expandedFrom); builder.add(parsedOption); + nextOptionPriority = OptionPriority.nextOptionPriority(nextOptionPriority); } return builder.build(); } @@ -287,15 +289,15 @@ class OptionsParserImpl { } } - /** Implements {@link OptionsParser#parseArgsFixedAsExpansionOfOption} */ - List parseArgsFixedAsExpansionOfOption( + /** Implements {@link OptionsParser#parseArgsAsExpansionOfOption} */ + List parseArgsAsExpansionOfOption( ParsedOptionDescription optionToExpand, Function sourceFunction, List args) throws OptionsParsingException { ResidueAndPriority residueAndPriority = parse( - OptionPriority.getLockedPriority(optionToExpand.getPriority()), + OptionPriority.getChildPriority(optionToExpand.getPriority()), sourceFunction, null, optionToExpand, @@ -419,7 +421,7 @@ class OptionsParserImpl { if (expansionBundle != null) { ResidueAndPriority residueAndPriority = parse( - OptionPriority.getLockedPriority(parsedOption.getPriority()), + OptionPriority.getChildPriority(parsedOption.getPriority()), o -> expansionBundle.sourceOfExpansionArgs, optionDefinition.hasImplicitRequirements() ? parsedOption : null, optionDefinition.isExpansionOption() ? parsedOption : null, diff --git a/java/com/google/devtools/common/options/OptionsProvider.java b/java/com/google/devtools/common/options/OptionsProvider.java index d467fe5..ece5d5d 100644 --- a/java/com/google/devtools/common/options/OptionsProvider.java +++ b/java/com/google/devtools/common/options/OptionsProvider.java @@ -73,11 +73,13 @@ public interface OptionsProvider extends OptionsClassProvider { List asListOfOptionValues(); /** - * Canonicalizes the list of options that this OptionsParser has parsed. The - * contract is that if the returned set of options is passed to an options - * parser with the same options classes, then that will have the same effect - * as using the original args (which are passed in here), except for cosmetic - * differences. + * Canonicalizes the list of options that this OptionsParser has parsed. + * + *

The contract is that if the returned set of options is passed to an options parser with the + * same options classes, then that will have the same effect as using the original args (which are + * passed in here), except for cosmetic differences. We do not guarantee that the 'canonical' list + * is unique, since some flags may have effects unknown to the parser (--config, for Bazel), so we + * do not reorder flags to further simplify the list. */ List canonicalize(); } -- cgit v1.2.3 From ce0be66cf75cd2c13644542b8ecbb18406ac4c32 Mon Sep 17 00:00:00 2001 From: ccalvarin Date: Tue, 17 Apr 2018 07:48:38 -0700 Subject: Make attempting to change --config in invocation policy an error. It will not work as expected, since config is already expanded by this point in options processing. RELNOTES: None. PiperOrigin-RevId: 193196664 GitOrigin-RevId: 9c8c77502ff52907a327e6bdc9ac282da0af6b44 Change-Id: I5fa3aaec852b2d16bb8974291735ba4da1709243 --- .../google/devtools/common/options/InvocationPolicyEnforcer.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/java/com/google/devtools/common/options/InvocationPolicyEnforcer.java b/java/com/google/devtools/common/options/InvocationPolicyEnforcer.java index 88deb46..3b42a29 100644 --- a/java/com/google/devtools/common/options/InvocationPolicyEnforcer.java +++ b/java/com/google/devtools/common/options/InvocationPolicyEnforcer.java @@ -248,6 +248,15 @@ public final class InvocationPolicyEnforcer { OptionPriority nextPriority = OptionPriority.lowestOptionPriorityAtCategory(PriorityCategory.INVOCATION_POLICY); for (FlagPolicy policy : invocationPolicy.getFlagPoliciesList()) { + // Explicitly disallow --config in invocation policy. + if (policy.getFlagName().equals("config")) { + throw new OptionsParsingException( + "Invocation policy is applied after --config expansion, changing config values now " + + "would have no effect and is disallowed to prevent confusion. Please remove the " + + "following policy : " + + policy); + } + // These policies are high-level, before expansion, and so are not the implicitDependents or // expansions of any other flag, other than in an obtuse sense from --invocation_policy. OptionPriority currentPriority = nextPriority; -- cgit v1.2.3 From 59370640f451cdb9b18472324f4234c0dc184755 Mon Sep 17 00:00:00 2001 From: cnsun Date: Tue, 17 Apr 2018 15:04:01 -0700 Subject: Relax the assertion in Desugar for checking the calls to $closeResource(...). It is possible that $closeResource(...) is not used as the calls to it might be eliminated by some optimization tools, such as Proguard. RELNOTES: n/a. PiperOrigin-RevId: 193262552 GitOrigin-RevId: 1a2ab6d54e2a8749549f41055cd66f3f6dfea4cc Change-Id: Ifdbd7b47132b541ecfd831d2a7b83d76853ec206 --- .../build/android/desugar/TryWithResourcesRewriter.java | 12 +++++------- ...ugar_unused_synthetic_close_resource_method_golden.txt | 8 ++++++++ .../build/android/desugar/unused_closed_resource.jar | Bin 0 -> 739 bytes 3 files changed, 13 insertions(+), 7 deletions(-) create mode 100644 test/java/com/google/devtools/build/android/desugar/desugar_unused_synthetic_close_resource_method_golden.txt create mode 100644 test/java/com/google/devtools/build/android/desugar/unused_closed_resource.jar diff --git a/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriter.java b/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriter.java index 818585f..98eef45 100644 --- a/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriter.java +++ b/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriter.java @@ -166,14 +166,12 @@ public class TryWithResourcesRewriter extends ClassVisitor { new CloseResourceMethodSpecializer(cv, resourceInternalName, isInterface)); } } else { + // It is possible that all calls to $closeResources(...) are in dead code regions, and the + // calls are eliminated, which leaving the method $closeResources() unused. (b/78030676). + // In this case, we just discard the method body. checkState( - closeResourceMethod == null, - "The field resourceTypeInternalNames is empty. " - + "But the class has the $closeResource method."); - checkState( - !hasCloseResourceMethod, - "The class %s has close resource method, but resourceTypeInternalNames is empty.", - internalName); + !hasCloseResourceMethod || closeResourceMethod != null, + "There should be $closeResources(...) in the class file."); } super.visitEnd(); } diff --git a/test/java/com/google/devtools/build/android/desugar/desugar_unused_synthetic_close_resource_method_golden.txt b/test/java/com/google/devtools/build/android/desugar/desugar_unused_synthetic_close_resource_method_golden.txt new file mode 100644 index 0000000..4be60b6 --- /dev/null +++ b/test/java/com/google/devtools/build/android/desugar/desugar_unused_synthetic_close_resource_method_golden.txt @@ -0,0 +1,8 @@ +Compiled from "UnusedSyntheticCloseResourceMethod.java" +public class com.google.devtools.build.android.desugar.testdata.UnusedSyntheticCloseResourceMethod { + public com.google.devtools.build.android.desugar.testdata.UnusedSyntheticCloseResourceMethod(); + Code: + 0: aload_0 + 1: invokespecial #9 // Method java/lang/Object."":()V + 4: return +} diff --git a/test/java/com/google/devtools/build/android/desugar/unused_closed_resource.jar b/test/java/com/google/devtools/build/android/desugar/unused_closed_resource.jar new file mode 100644 index 0000000..6a451eb Binary files /dev/null and b/test/java/com/google/devtools/build/android/desugar/unused_closed_resource.jar differ -- cgit v1.2.3 From 72ad6666b99ce7f2bc604cb25515fd0f41b56d60 Mon Sep 17 00:00:00 2001 From: jcater Date: Fri, 20 Apr 2018 04:04:06 -0700 Subject: Remove use of bare Immutable{List,Map,Set} Builder classes. Always use the more-qualified class name for clarity at the site of use. There are too many classes named Builder. PiperOrigin-RevId: 193649193 GitOrigin-RevId: 96d3c91c714544584c9174759bedebf2a6be5e71 Change-Id: I0c9cf0ab619bc743cd15ba63ad7355e008c0f1d1 --- java/com/google/devtools/build/android/Converters.java | 3 +-- .../google/devtools/common/options/processor/OptionProcessor.java | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/java/com/google/devtools/build/android/Converters.java b/java/com/google/devtools/build/android/Converters.java index 13911f9..5e89db2 100644 --- a/java/com/google/devtools/build/android/Converters.java +++ b/java/com/google/devtools/build/android/Converters.java @@ -19,7 +19,6 @@ import com.android.manifmerger.ManifestMerger2.MergeType; import com.android.repository.Revision; import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableList.Builder; import com.google.common.collect.ImmutableMap; import com.google.devtools.build.android.aapt2.CompiledResources; import com.google.devtools.build.android.aapt2.StaticLibrary; @@ -423,7 +422,7 @@ public final class Converters { @Override public List convert(String input) throws OptionsParsingException { - final Builder builder = ImmutableList.builder(); + final ImmutableList.Builder builder = ImmutableList.builder(); for (String path : SPLITTER.splitToList(input)) { builder.add(libraryConverter.convert(path)); } diff --git a/java/com/google/devtools/common/options/processor/OptionProcessor.java b/java/com/google/devtools/common/options/processor/OptionProcessor.java index 5190053..0f1989c 100644 --- a/java/com/google/devtools/common/options/processor/OptionProcessor.java +++ b/java/com/google/devtools/common/options/processor/OptionProcessor.java @@ -15,7 +15,6 @@ package com.google.devtools.common.options.processor; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableMap.Builder; import com.google.devtools.common.options.Converter; import com.google.devtools.common.options.Converters; import com.google.devtools.common.options.ExpansionFunction; @@ -96,11 +95,12 @@ public final class OptionProcessor extends AbstractProcessor { // Because of the discrepancies between the java.lang and javax.lang type models, we can't // directly use the get() method for the default converter map. Instead, we'll convert it once, // to be more usable, and with the boxed type return values of convert() as the keys. - ImmutableMap.Builder> converterMapBuilder = new Builder<>(); + ImmutableMap.Builder> converterMapBuilder = + new ImmutableMap.Builder<>(); // Create a link from the primitive Classes to their primitive types. This intentionally // only contains the types in the DEFAULT_CONVERTERS map. - ImmutableMap.Builder, PrimitiveType> builder = new Builder<>(); + ImmutableMap.Builder, PrimitiveType> builder = new ImmutableMap.Builder<>(); builder.put(int.class, typeUtils.getPrimitiveType(TypeKind.INT)); builder.put(double.class, typeUtils.getPrimitiveType(TypeKind.DOUBLE)); builder.put(boolean.class, typeUtils.getPrimitiveType(TypeKind.BOOLEAN)); -- cgit v1.2.3 From a228dd20929b9b510b25df7a0b1b087cff0e1274 Mon Sep 17 00:00:00 2001 From: jcater Date: Tue, 1 May 2018 13:19:16 -0700 Subject: Clean up code that directly imports nested classes like Builder, Entry, etc. PiperOrigin-RevId: 194985157 GitOrigin-RevId: 26ff4b3e3997aab79e39caf62c0d123a315d9478 Change-Id: Ibdf69191b559399f4775d82a52a26ce93567707c --- .../build/android/desugar/dependencies/MetadataCollectorTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/java/com/google/devtools/build/android/desugar/dependencies/MetadataCollectorTest.java b/test/java/com/google/devtools/build/android/desugar/dependencies/MetadataCollectorTest.java index e99abc4..64dd871 100644 --- a/test/java/com/google/devtools/build/android/desugar/dependencies/MetadataCollectorTest.java +++ b/test/java/com/google/devtools/build/android/desugar/dependencies/MetadataCollectorTest.java @@ -16,11 +16,11 @@ package com.google.devtools.build.android.desugar.dependencies; import static com.google.common.truth.Truth.assertThat; import com.google.common.collect.ImmutableList; +import com.google.devtools.build.android.desugar.proto.DesugarDeps; import com.google.devtools.build.android.desugar.proto.DesugarDeps.Dependency; import com.google.devtools.build.android.desugar.proto.DesugarDeps.DesugarDepsInfo; import com.google.devtools.build.android.desugar.proto.DesugarDeps.InterfaceDetails; import com.google.devtools.build.android.desugar.proto.DesugarDeps.InterfaceWithCompanion; -import com.google.devtools.build.android.desugar.proto.DesugarDeps.Type; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -103,8 +103,8 @@ public class MetadataCollectorTest { .build()); } - private static Type wrapType(String name) { - return Type.newBuilder().setBinaryName(name).build(); + private static DesugarDeps.Type wrapType(String name) { + return DesugarDeps.Type.newBuilder().setBinaryName(name).build(); } private DesugarDepsInfo extractProto(MetadataCollector collector) throws Exception { -- cgit v1.2.3 From ac50274742dc9431da2669348102ad5f0cf200aa Mon Sep 17 00:00:00 2001 From: jcater Date: Tue, 1 May 2018 20:33:18 -0700 Subject: Clean up code that directly imports nested classes like Builder, Entry, etc. PiperOrigin-RevId: 195040539 GitOrigin-RevId: 0a57d3dcb1cc014d65dbeb604035bb34a7191e29 Change-Id: I78ff7b0f225fbdcdeed44145fe0e28ffc0e4c197 --- .../build/android/desugar/dependencies/MetadataCollector.java | 5 ++--- .../google/devtools/common/options/processor/OptionProcessor.java | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/java/com/google/devtools/build/android/desugar/dependencies/MetadataCollector.java b/java/com/google/devtools/build/android/desugar/dependencies/MetadataCollector.java index 732ef50..448ccab 100644 --- a/java/com/google/devtools/build/android/desugar/dependencies/MetadataCollector.java +++ b/java/com/google/devtools/build/android/desugar/dependencies/MetadataCollector.java @@ -22,7 +22,6 @@ import com.google.devtools.build.android.desugar.proto.DesugarDeps.Dependency; import com.google.devtools.build.android.desugar.proto.DesugarDeps.DesugarDepsInfo; import com.google.devtools.build.android.desugar.proto.DesugarDeps.InterfaceDetails; import com.google.devtools.build.android.desugar.proto.DesugarDeps.InterfaceWithCompanion; -import com.google.devtools.build.android.desugar.proto.DesugarDeps.Type; import javax.annotation.Nullable; /** Dependency collector that emits collected metadata as a {@link DesugarDepsInfo} proto. */ @@ -82,7 +81,7 @@ public final class MetadataCollector implements DependencyCollector { return DesugarDepsInfo.getDefaultInstance().equals(result) ? null : result.toByteArray(); } - private static Type wrapType(String internalName) { - return Type.newBuilder().setBinaryName(internalName).build(); + private static DesugarDeps.Type wrapType(String internalName) { + return DesugarDeps.Type.newBuilder().setBinaryName(internalName).build(); } } \ No newline at end of file diff --git a/java/com/google/devtools/common/options/processor/OptionProcessor.java b/java/com/google/devtools/common/options/processor/OptionProcessor.java index 0f1989c..485efcd 100644 --- a/java/com/google/devtools/common/options/processor/OptionProcessor.java +++ b/java/com/google/devtools/common/options/processor/OptionProcessor.java @@ -26,7 +26,7 @@ import com.google.devtools.common.options.OptionsBase; import com.google.devtools.common.options.OptionsParser; import com.google.devtools.common.options.OptionsParsingException; import java.util.List; -import java.util.Map.Entry; +import java.util.Map; import java.util.Set; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -107,7 +107,7 @@ public final class OptionProcessor extends AbstractProcessor { builder.put(long.class, typeUtils.getPrimitiveType(TypeKind.LONG)); primitiveTypeMap = builder.build(); - for (Entry, Converter> entry : Converters.DEFAULT_CONVERTERS.entrySet()) { + for (Map.Entry, Converter> entry : Converters.DEFAULT_CONVERTERS.entrySet()) { Class converterClass = entry.getKey(); String typeName = converterClass.getCanonicalName(); TypeElement typeElement = elementUtils.getTypeElement(typeName); -- cgit v1.2.3 From 6c7f67fe112e0eae9a41b74bf37ced67a88516fc Mon Sep 17 00:00:00 2001 From: jcater Date: Wed, 2 May 2018 09:08:52 -0700 Subject: Clean up code that directly imports nested classes like Builder, Entry, etc. PiperOrigin-RevId: 195100670 GitOrigin-RevId: 94b8702db5f9a905337aca74bfc2e7c436bf33ec Change-Id: Iea45a0d018d49a43181c1e357721d0b552bea777 --- .../devtools/common/options/OptionValueDescription.java | 14 +++++++------- java/com/google/devtools/common/options/OptionsBase.java | 3 +-- java/com/google/devtools/common/options/OptionsParser.java | 5 ++--- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/java/com/google/devtools/common/options/OptionValueDescription.java b/java/com/google/devtools/common/options/OptionValueDescription.java index f52e8a5..11ad7fe 100644 --- a/java/com/google/devtools/common/options/OptionValueDescription.java +++ b/java/com/google/devtools/common/options/OptionValueDescription.java @@ -22,7 +22,7 @@ import com.google.devtools.common.options.OptionsParser.ConstructionException; import java.util.Collection; import java.util.Comparator; import java.util.List; -import java.util.Map.Entry; +import java.util.Map; import java.util.stream.Collectors; import javax.annotation.Nullable; @@ -276,8 +276,8 @@ public abstract class OptionValueDescription { .asMap() .entrySet() .stream() - .sorted(Comparator.comparing(Entry::getKey)) - .map(Entry::getValue) + .sorted(Comparator.comparing(Map.Entry::getKey)) + .map(Map.Entry::getValue) .flatMap(Collection::stream) .map(ParsedOptionDescription::getSource) .distinct() @@ -292,8 +292,8 @@ public abstract class OptionValueDescription { .asMap() .entrySet() .stream() - .sorted(Comparator.comparing(Entry::getKey)) - .map(Entry::getValue) + .sorted(Comparator.comparing(Map.Entry::getKey)) + .map(Map.Entry::getValue) .flatMap(Collection::stream) .collect(Collectors.toList()); } @@ -320,8 +320,8 @@ public abstract class OptionValueDescription { .asMap() .entrySet() .stream() - .sorted(Comparator.comparing(Entry::getKey)) - .map(Entry::getValue) + .sorted(Comparator.comparing(Map.Entry::getKey)) + .map(Map.Entry::getValue) .flatMap(Collection::stream) // Only provide the options that aren't implied elsewhere. .filter(optionDesc -> optionDesc.getImplicitDependent() == null) diff --git a/java/com/google/devtools/common/options/OptionsBase.java b/java/com/google/devtools/common/options/OptionsBase.java index 6b9f2f1..9496c65 100644 --- a/java/com/google/devtools/common/options/OptionsBase.java +++ b/java/com/google/devtools/common/options/OptionsBase.java @@ -20,7 +20,6 @@ import java.lang.reflect.Field; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Map.Entry; /** * Base class for all options classes. Extend this class, adding public instance fields annotated @@ -93,7 +92,7 @@ public abstract class OptionsBase { public final String cacheKey() { StringBuilder result = new StringBuilder(getClass().getName()).append("{"); - for (Entry entry : asMap().entrySet()) { + for (Map.Entry entry : asMap().entrySet()) { result.append(entry.getKey()).append("="); Object value = entry.getValue(); diff --git a/java/com/google/devtools/common/options/OptionsParser.java b/java/com/google/devtools/common/options/OptionsParser.java index e75f0e1..6f1d7b6 100644 --- a/java/com/google/devtools/common/options/OptionsParser.java +++ b/java/com/google/devtools/common/options/OptionsParser.java @@ -33,7 +33,6 @@ import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; -import java.util.Map.Entry; import java.util.Set; import java.util.function.Consumer; import java.util.function.Function; @@ -299,7 +298,7 @@ public class OptionsParser implements OptionsProvider { getOptionsSortedByCategory(); ImmutableMap optionCategoryDescriptions = OptionFilterDescriptions.getOptionCategoriesEnumDescription(productName); - for (Entry> e : + for (Map.Entry> e : optionsByCategory.entrySet()) { String categoryDescription = optionCategoryDescriptions.get(e.getKey()); List categorizedOptionList = e.getValue(); @@ -463,7 +462,7 @@ public class OptionsParser implements OptionsProvider { ImmutableMap optionCategoryDescriptions = OptionFilterDescriptions.getOptionCategoriesEnumDescription(productName); - for (Entry> e : + for (Map.Entry> e : optionsByCategory.entrySet()) { desc.append("

"); String categoryDescription = optionCategoryDescriptions.get(e.getKey()); -- cgit v1.2.3 From bc0139bb823083b8322a3a58d3fa8eb390c154de Mon Sep 17 00:00:00 2001 From: Googler Date: Thu, 3 May 2018 06:43:51 -0700 Subject: Allow --worker_max_instances to take MnemonicName=value to specify max for each named worker. RELNOTES: Allow --worker_max_instances to take MnemonicName=value to specify max for each worker. PiperOrigin-RevId: 195244295 GitOrigin-RevId: 0c12603bedd4a270094137269b910a8587d3f93c Change-Id: I1ab6bf78b0101c7fbe842d18c62ce844869e4eec --- .../google/devtools/common/options/Converters.java | 129 +++++++++++---------- 1 file changed, 69 insertions(+), 60 deletions(-) diff --git a/java/com/google/devtools/common/options/Converters.java b/java/com/google/devtools/common/options/Converters.java index 35f2da4..fb3bbfa 100644 --- a/java/com/google/devtools/common/options/Converters.java +++ b/java/com/google/devtools/common/options/Converters.java @@ -26,10 +26,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; -/** - * Some convenient converters used by blaze. Note: These are specific to - * blaze. - */ +/** Some convenient converters used by blaze. Note: These are specific to blaze. */ public final class Converters { /** Standard converter for booleans. Accepts common shorthands/synonyms. */ @@ -180,9 +177,7 @@ public final class Converters { } } - /** - * Standard converter for the {@link java.time.Duration} type. - */ + /** Standard converter for the {@link java.time.Duration} type. */ public static class DurationConverter implements Converter { private final Pattern durationRegex = Pattern.compile("^([0-9]+)(d|h|m|s|ms)$"); @@ -198,7 +193,7 @@ public final class Converters { } long duration = Long.parseLong(m.group(1)); String unit = m.group(2); - switch(unit) { + switch (unit) { case "d": return Duration.ofDays(duration); case "h": @@ -210,8 +205,8 @@ public final class Converters { case "ms": return Duration.ofMillis(duration); default: - throw new IllegalStateException("This must not happen. Did you update the regex without " - + "the switch case?"); + throw new IllegalStateException( + "This must not happen. Did you update the regex without the switch case?"); } } @@ -240,14 +235,8 @@ public final class Converters { .build(); /** - * Join a list of words as in English. Examples: - * "nothing" - * "one" - * "one or two" - * "one and two" - * "one, two or three". - * "one, two and three". - * The toString method of each element is used. + * Join a list of words as in English. Examples: "nothing" "one" "one or two" "one and two" "one, + * two or three". "one, two and three". The toString method of each element is used. */ static String joinEnglishList(Iterable choices) { StringBuilder buf = new StringBuilder(); @@ -261,14 +250,12 @@ public final class Converters { return buf.length() == 0 ? "nothing" : buf.toString(); } - public static class SeparatedOptionListConverter - implements Converter> { + public static class SeparatedOptionListConverter implements Converter> { private final String separatorDescription; private final Splitter splitter; - protected SeparatedOptionListConverter(char separator, - String separatorDescription) { + protected SeparatedOptionListConverter(char separator, String separatorDescription) { this.separatorDescription = separatorDescription; this.splitter = Splitter.on(separator); } @@ -284,8 +271,7 @@ public final class Converters { } } - public static class CommaSeparatedOptionListConverter - extends SeparatedOptionListConverter { + public static class CommaSeparatedOptionListConverter extends SeparatedOptionListConverter { public CommaSeparatedOptionListConverter() { super(',', "comma"); } @@ -299,10 +285,10 @@ public final class Converters { public static class LogLevelConverter implements Converter { - public static final Level[] LEVELS = new Level[] { - Level.OFF, Level.SEVERE, Level.WARNING, Level.INFO, Level.FINE, - Level.FINER, Level.FINEST - }; + public static final Level[] LEVELS = + new Level[] { + Level.OFF, Level.SEVERE, Level.WARNING, Level.INFO, Level.FINE, Level.FINER, Level.FINEST + }; @Override public Level convert(String input) throws OptionsParsingException { @@ -318,12 +304,9 @@ public final class Converters { public String getTypeDescription() { return "0 <= an integer <= " + (LEVELS.length - 1); } - } - /** - * Checks whether a string is part of a set of strings. - */ + /** Checks whether a string is part of a set of strings. */ public static class StringSetConverter implements Converter { // TODO(bazel-team): if this class never actually contains duplicates, we could s/List/Set/ @@ -349,9 +332,7 @@ public final class Converters { } } - /** - * Checks whether a string is a valid regex pattern and compiles it. - */ + /** Checks whether a string is a valid regex pattern and compiles it. */ public static class RegexPatternConverter implements Converter { @Override @@ -369,9 +350,7 @@ public final class Converters { } } - /** - * Limits the length of a string argument. - */ + /** Limits the length of a string argument. */ public static class LengthLimitingConverter implements Converter { private final int maxSize; @@ -393,9 +372,7 @@ public final class Converters { } } - /** - * Checks whether an integer is in the given range. - */ + /** Checks whether an integer is in the given range. */ public static class RangeConverter implements Converter { final int minValue; final int maxValue; @@ -432,25 +409,27 @@ public final class Converters { return "an integer, >= " + minValue; } else { return "an integer in " - + (minValue < 0 ? "(" + minValue + ")" : minValue) + "-" + maxValue + " range"; + + (minValue < 0 ? "(" + minValue + ")" : minValue) + + "-" + + maxValue + + " range"; } } } /** - * A converter for variable assignments from the parameter list of a blaze - * command invocation. Assignments are expected to have the form "name=value", - * where names and values are defined to be as permissive as possible. + * A converter for variable assignments from the parameter list of a blaze command invocation. + * Assignments are expected to have the form "name=value", where names and values are defined to + * be as permissive as possible. */ public static class AssignmentConverter implements Converter> { @Override - public Map.Entry convert(String input) - throws OptionsParsingException { + public Map.Entry convert(String input) throws OptionsParsingException { int pos = input.indexOf("="); if (pos <= 0) { - throw new OptionsParsingException("Variable definitions must be in the form of a " - + "'name=value' assignment"); + throw new OptionsParsingException( + "Variable definitions must be in the form of a 'name=value' assignment"); } String name = input.substring(0, pos); String value = input.substring(pos + 1); @@ -461,24 +440,22 @@ public final class Converters { public String getTypeDescription() { return "a 'name=value' assignment"; } - } /** - * A converter for variable assignments from the parameter list of a blaze - * command invocation. Assignments are expected to have the form "name[=value]", - * where names and values are defined to be as permissive as possible and value - * part can be optional (in which case it is considered to be null). + * A converter for variable assignments from the parameter list of a blaze command invocation. + * Assignments are expected to have the form "name[=value]", where names and values are defined to + * be as permissive as possible and value part can be optional (in which case it is considered to + * be null). */ public static class OptionalAssignmentConverter implements Converter> { @Override - public Map.Entry convert(String input) - throws OptionsParsingException { - int pos = input.indexOf("="); + public Map.Entry convert(String input) throws OptionsParsingException { + int pos = input.indexOf('='); if (pos == 0 || input.length() == 0) { - throw new OptionsParsingException("Variable definitions must be in the form of a " - + "'name=value' or 'name' assignment"); + throw new OptionsParsingException( + "Variable definitions must be in the form of a 'name=value' or 'name' assignment"); } else if (pos < 0) { return Maps.immutableEntry(input, null); } @@ -491,7 +468,40 @@ public final class Converters { public String getTypeDescription() { return "a 'name=value' assignment with an optional value part"; } + } + + /** + * A converter for named integers of the form "[name=]value". When no name is specified, an empty + * string is used for the key. + */ + public static class NamedIntegersConverter implements Converter> { + @Override + public Map.Entry convert(String input) throws OptionsParsingException { + int pos = input.indexOf('='); + if (pos == 0 || input.length() == 0) { + throw new OptionsParsingException( + "Specify either 'value' or 'name=value', where 'value' is an integer"); + } else if (pos < 0) { + try { + return Maps.immutableEntry("", Integer.parseInt(input)); + } catch (NumberFormatException e) { + throw new OptionsParsingException("'" + input + "' is not an int"); + } + } + String name = input.substring(0, pos); + String value = input.substring(pos + 1); + try { + return Maps.immutableEntry(name, Integer.parseInt(value)); + } catch (NumberFormatException e) { + throw new OptionsParsingException("'" + value + "' is not an int"); + } + } + + @Override + public String getTypeDescription() { + return "an integer or a named integer, 'name=value'"; + } } public static class HelpVerbosityConverter extends EnumConverter { @@ -508,5 +518,4 @@ public final class Converters { super(0, 100); } } - } -- cgit v1.2.3