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