From 47bb3bfbc969ea3ac98381cb67c3a6b8821012f5 Mon Sep 17 00:00:00 2001 From: kmb Date: Mon, 5 Feb 2018 18:18:15 -0800 Subject: Basic tooling to desugar select core libraries RELNOTES: None. PiperOrigin-RevId: 184619885 GitOrigin-RevId: 1324318ea0fe60350c0a5179818fc1c97d4ec854 Change-Id: I2d9bc87180067959b618641a188d83a8d7c24b3b --- .../desugar/CoreLibraryInvocationRewriter.java | 81 +++++++ .../build/android/desugar/CoreLibraryRewriter.java | 6 +- .../build/android/desugar/CoreLibrarySupport.java | 152 ++++++++++++++ .../build/android/desugar/CorePackageRenamer.java | 54 +++++ .../devtools/build/android/desugar/Desugar.java | 72 ++++++- .../build/android/desugar/InterfaceDesugaring.java | 3 +- .../android/desugar/CoreLibrarySupportTest.java | 232 +++++++++++++++++++++ .../android/desugar/CorePackageRenamerTest.java | 90 ++++++++ 8 files changed, 681 insertions(+), 9 deletions(-) create mode 100644 java/com/google/devtools/build/android/desugar/CoreLibraryInvocationRewriter.java create mode 100644 java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java create mode 100644 java/com/google/devtools/build/android/desugar/CorePackageRenamer.java create mode 100644 test/java/com/google/devtools/build/android/desugar/CoreLibrarySupportTest.java create mode 100644 test/java/com/google/devtools/build/android/desugar/CorePackageRenamerTest.java diff --git a/java/com/google/devtools/build/android/desugar/CoreLibraryInvocationRewriter.java b/java/com/google/devtools/build/android/desugar/CoreLibraryInvocationRewriter.java new file mode 100644 index 0000000..417248b --- /dev/null +++ b/java/com/google/devtools/build/android/desugar/CoreLibraryInvocationRewriter.java @@ -0,0 +1,81 @@ +// Copyright 2018 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package com.google.devtools.build.android.desugar; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; + +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +/** + * Rewriter of default and static interface methods defined in some core libraries. + * + *

This is conceptually similar to call site rewriting in {@link InterfaceDesugaring} but here + * we're doing it for certain bootclasspath methods and in particular for invokeinterface and + * invokevirtual, which are ignored in regular {@link InterfaceDesugaring}. + */ +public class CoreLibraryInvocationRewriter extends ClassVisitor { + + private final CoreLibrarySupport support; + + public CoreLibraryInvocationRewriter(ClassVisitor cv, CoreLibrarySupport support) { + super(Opcodes.ASM6, cv); + this.support = support; + } + + @Override + public MethodVisitor visitMethod( + int access, String name, String desc, String signature, String[] exceptions) { + MethodVisitor result = super.visitMethod(access, name, desc, signature, exceptions); + return result != null ? new CoreLibraryMethodInvocationRewriter(result) : null; + } + + private class CoreLibraryMethodInvocationRewriter extends MethodVisitor { + public CoreLibraryMethodInvocationRewriter(MethodVisitor mv) { + super(Opcodes.ASM6, mv); + } + + @Override + public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) { + Class coreInterface = + support.getEmulatedCoreLibraryInvocationTarget(opcode, owner, name, desc, itf); + if (coreInterface != null) { + String coreInterfaceName = coreInterface.getName().replace('.', '/'); + name = + InterfaceDesugaring.normalizeInterfaceMethodName( + name, name.startsWith("lambda$"), opcode == Opcodes.INVOKESTATIC); + if (opcode == Opcodes.INVOKESTATIC) { + checkState(owner.equals(coreInterfaceName)); + } else { + desc = + InterfaceDesugaring.companionDefaultMethodDescriptor( + opcode == Opcodes.INVOKESPECIAL ? owner : coreInterfaceName, desc); + } + + if (opcode == Opcodes.INVOKESTATIC || opcode == Opcodes.INVOKESPECIAL) { + checkArgument(itf, "Expected interface to rewrite %s.%s : %s", owner, name, desc); + owner = InterfaceDesugaring.getCompanionClassName(owner); + } else { + // TODO(kmb): Simulate dynamic dispatch instead of calling most general default method + owner = InterfaceDesugaring.getCompanionClassName(coreInterfaceName); + } + opcode = Opcodes.INVOKESTATIC; + itf = false; + } + super.visitMethodInsn(opcode, owner, name, desc, itf); + } + } +} diff --git a/java/com/google/devtools/build/android/desugar/CoreLibraryRewriter.java b/java/com/google/devtools/build/android/desugar/CoreLibraryRewriter.java index 7f1591b..456fdb5 100644 --- a/java/com/google/devtools/build/android/desugar/CoreLibraryRewriter.java +++ b/java/com/google/devtools/build/android/desugar/CoreLibraryRewriter.java @@ -85,6 +85,10 @@ class CoreLibraryRewriter { return false; } + public String getPrefix() { + return prefix; + } + /** Removes prefix from class names */ public String unprefix(String typeName) { if (prefix.isEmpty() || !typeName.startsWith(prefix)) { @@ -159,7 +163,7 @@ class CoreLibraryRewriter { if (!prefix.isEmpty()) { this.cv = new ClassRemapper( - this.cv, + this.writer, new Remapper() { @Override public String map(String typeName) { diff --git a/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java b/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java new file mode 100644 index 0000000..56e5f18 --- /dev/null +++ b/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java @@ -0,0 +1,152 @@ +// Copyright 2018 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package com.google.devtools.build.android.desugar; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.common.collect.ImmutableList; +import java.lang.reflect.Method; +import java.util.LinkedHashSet; +import java.util.Objects; +import java.util.Set; +import javax.annotation.Nullable; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; + +/** + * Helper that keeps track of which core library classes and methods we want to rewrite. + */ +class CoreLibrarySupport { + + private final CoreLibraryRewriter rewriter; + private final ClassLoader targetLoader; + /** Internal name prefixes that we want to move to a custom package. */ + private final ImmutableList renamedPrefixes; + /** Internal names of interfaces whose default and static interface methods we'll emulate. */ + private final ImmutableList> emulatedInterfaces; + + public CoreLibrarySupport(CoreLibraryRewriter rewriter, ClassLoader targetLoader, + ImmutableList renamedPrefixes, ImmutableList emulatedInterfaces) + throws ClassNotFoundException { + this.rewriter = rewriter; + this.targetLoader = targetLoader; + checkArgument( + renamedPrefixes.stream().allMatch(prefix -> prefix.startsWith("java/")), renamedPrefixes); + this.renamedPrefixes = renamedPrefixes; + ImmutableList.Builder> classBuilder = ImmutableList.builder(); + for (String itf : emulatedInterfaces) { + checkArgument(itf.startsWith("java/util/"), itf); + Class clazz = targetLoader.loadClass((rewriter.getPrefix() + itf).replace('/', '.')); + checkArgument(clazz.isInterface(), itf); + classBuilder.add(clazz); + } + this.emulatedInterfaces = classBuilder.build(); + } + + public boolean isRenamedCoreLibrary(String internalName) { + String unprefixedName = rewriter.unprefix(internalName); + return renamedPrefixes.stream().anyMatch(prefix -> unprefixedName.startsWith(prefix)); + } + + public String renameCoreLibrary(String internalName) { + internalName = rewriter.unprefix(internalName); + return (internalName.startsWith("java/")) + ? "j$/" + internalName.substring(/* cut away "java/" prefix */ 5) + : internalName; + } + + public boolean isEmulatedCoreLibraryInvocation( + int opcode, String owner, String name, String desc, boolean itf) { + return getEmulatedCoreLibraryInvocationTarget(opcode, owner, name, desc, itf) != null; + } + + @Nullable + public Class getEmulatedCoreLibraryInvocationTarget( + int opcode, String owner, String name, String desc, boolean itf) { + if (owner.contains("$$Lambda$") || owner.endsWith("$$CC")) { + return null; // regular desugaring handles invocations on generated classes, no emulation + } + Class clazz = getEmulatedCoreClassOrInterface(owner); + if (clazz == null) { + return null; + } + + if (itf && opcode == Opcodes.INVOKESTATIC) { + return clazz; // static interface method + } + + Method callee = findInterfaceMethod(clazz, name, desc); + if (callee != null && callee.isDefault()) { + return callee.getDeclaringClass(); + } + return null; + } + + private Class getEmulatedCoreClassOrInterface(String internalName) { + { + String unprefixedOwner = rewriter.unprefix(internalName); + if (!unprefixedOwner.startsWith("java/util/") || isRenamedCoreLibrary(unprefixedOwner)) { + return null; + } + } + + Class clazz; + try { + clazz = targetLoader.loadClass(internalName.replace('/', '.')); + } catch (ClassNotFoundException e) { + throw (NoClassDefFoundError) new NoClassDefFoundError().initCause(e); + } + + if (emulatedInterfaces.stream().anyMatch(itf -> itf.isAssignableFrom(clazz))) { + return clazz; + } + return null; + } + + private static Method findInterfaceMethod(Class clazz, String name, String desc) { + return collectImplementedInterfaces(clazz, new LinkedHashSet<>()) + .stream() + // search more subtypes before supertypes + .sorted(DefaultMethodClassFixer.InterfaceComparator.INSTANCE) + .map(itf -> findMethod(itf, name, desc)) + .filter(Objects::nonNull) + .findFirst() + .orElse((Method) null); + } + + + private static Method findMethod(Class clazz, String name, String desc) { + for (Method m : clazz.getMethods()) { + if (m.getName().equals(name) && Type.getMethodDescriptor(m).equals(desc)) { + return m; + } + } + return null; + } + + private static Set> collectImplementedInterfaces(Class clazz, Set> dest) { + if (clazz.isInterface()) { + if (!dest.add(clazz)) { + return dest; + } + } else if (clazz.getSuperclass() != null) { + collectImplementedInterfaces(clazz.getSuperclass(), dest); + } + + for (Class itf : clazz.getInterfaces()) { + collectImplementedInterfaces(itf, dest); + } + return dest; + } +} diff --git a/java/com/google/devtools/build/android/desugar/CorePackageRenamer.java b/java/com/google/devtools/build/android/desugar/CorePackageRenamer.java new file mode 100644 index 0000000..3d58ef6 --- /dev/null +++ b/java/com/google/devtools/build/android/desugar/CorePackageRenamer.java @@ -0,0 +1,54 @@ +// Copyright 2018 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package com.google.devtools.build.android.desugar; + +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.Handle; +import org.objectweb.asm.commons.ClassRemapper; +import org.objectweb.asm.commons.Remapper; + +/** + * A visitor that renames some type names and only when the owner is also renamed. + */ +class CorePackageRenamer extends ClassRemapper { + + public CorePackageRenamer(ClassVisitor cv, CoreLibrarySupport support) { + super(cv, new CorePackageRemapper(support)); + } + + private static final class CorePackageRemapper extends Remapper { + private final CoreLibrarySupport support; + + private CorePackageRemapper(CoreLibrarySupport support) { + this.support = support; + } + + public boolean isRenamed(String owner) { + return support.isRenamedCoreLibrary(owner); + } + + @Override + public String map(String typeName) { + return isRenamed(typeName) ? support.renameCoreLibrary(typeName) : typeName; + } + + @Override + public Object mapValue(Object value) { + if (value instanceof Handle && !isRenamed(((Handle) value).getOwner())) { + return value; + } + return super.mapValue(value); + } + } +} diff --git a/java/com/google/devtools/build/android/desugar/Desugar.java b/java/com/google/devtools/build/android/desugar/Desugar.java index 5d3df4a..c86b406 100644 --- a/java/com/google/devtools/build/android/desugar/Desugar.java +++ b/java/com/google/devtools/build/android/desugar/Desugar.java @@ -248,6 +248,28 @@ class Desugar { ) public boolean coreLibrary; + /** Type prefixes that we'll move to a custom package. */ + @Option( + name = "rewrite_core_library_prefix", + defaultValue = "", // ignored + allowMultiple = true, + documentationCategory = OptionDocumentationCategory.UNDOCUMENTED, + effectTags = {OptionEffectTag.UNKNOWN}, + help = "Assume the given java.* prefixes are desugared." + ) + public List rewriteCoreLibraryPrefixes; + + /** Interfaces whose default and static interface methods we'll emulate. */ + @Option( + name = "emulate_core_library_interface", + defaultValue = "", // ignored + allowMultiple = true, + documentationCategory = OptionDocumentationCategory.UNDOCUMENTED, + effectTags = {OptionEffectTag.UNKNOWN}, + help = "Assume the given java.* interfaces are emulated." + ) + public List emulateCoreLibraryInterfaces; + /** Set to work around b/62623509 with JaCoCo versions prior to 0.7.9. */ // TODO(kmb): Remove when Android Studio doesn't need it anymore (see b/37116789) @Option( @@ -265,7 +287,7 @@ class Desugar { private final DesugarOptions options; private final CoreLibraryRewriter rewriter; private final LambdaClassMaker lambdas; - private final GeneratedClassStore store; + private final GeneratedClassStore store = new GeneratedClassStore(); private final Set visitedExceptionTypes = new HashSet<>(); /** The counter to record the times of try-with-resources desugaring is invoked. */ private final AtomicInteger numOfTryWithResourcesInvoked = new AtomicInteger(); @@ -282,7 +304,6 @@ class Desugar { this.options = options; this.rewriter = new CoreLibraryRewriter(options.coreLibrary ? "__desugar__/" : ""); this.lambdas = new LambdaClassMaker(dumpDirectory); - this.store = new GeneratedClassStore(); this.outputJava7 = options.minSdkVersion < 24; this.allowDefaultMethods = options.desugarInterfaceMethodBodiesIfNeeded || options.minSdkVersion >= 24; @@ -358,6 +379,16 @@ class Desugar { ImmutableSet.Builder interfaceLambdaMethodCollector = ImmutableSet.builder(); ClassVsInterface interfaceCache = new ClassVsInterface(classpathReader); + CoreLibrarySupport coreLibrarySupport = + options.rewriteCoreLibraryPrefixes.isEmpty() + && options.emulateCoreLibraryInterfaces.isEmpty() + ? null + : new CoreLibrarySupport( + rewriter, + loader, + ImmutableList.copyOf(options.rewriteCoreLibraryPrefixes), + ImmutableList.copyOf(options.emulateCoreLibraryInterfaces)); + desugarClassesInInput( inputFiles, outputFileProvider, @@ -365,6 +396,7 @@ class Desugar { classpathReader, depsCollector, bootclasspathReader, + coreLibrarySupport, interfaceCache, interfaceLambdaMethodCollector); @@ -374,11 +406,12 @@ class Desugar { classpathReader, depsCollector, bootclasspathReader, + coreLibrarySupport, interfaceCache, interfaceLambdaMethodCollector.build(), bridgeMethodReader); - desugarAndWriteGeneratedClasses(outputFileProvider, bootclasspathReader); + desugarAndWriteGeneratedClasses(outputFileProvider, bootclasspathReader, coreLibrarySupport); copyThrowableExtensionClass(outputFileProvider); byte[] depsInfo = depsCollector.toByteArray(); @@ -445,6 +478,7 @@ class Desugar { @Nullable ClassReaderFactory classpathReader, DependencyCollector depsCollector, ClassReaderFactory bootclasspathReader, + @Nullable CoreLibrarySupport coreLibrarySupport, ClassVsInterface interfaceCache, ImmutableSet.Builder interfaceLambdaMethodCollector) throws IOException { @@ -466,6 +500,7 @@ class Desugar { classpathReader, depsCollector, bootclasspathReader, + coreLibrarySupport, interfaceCache, interfaceLambdaMethodCollector, writer, @@ -494,6 +529,7 @@ class Desugar { @Nullable ClassReaderFactory classpathReader, DependencyCollector depsCollector, ClassReaderFactory bootclasspathReader, + @Nullable CoreLibrarySupport coreLibrarySupport, ClassVsInterface interfaceCache, ImmutableSet interfaceLambdaMethods, @Nullable ClassReaderFactory bridgeMethodReader) @@ -525,6 +561,7 @@ class Desugar { classpathReader, depsCollector, bootclasspathReader, + coreLibrarySupport, interfaceCache, interfaceLambdaMethods, bridgeMethodReader, @@ -540,7 +577,9 @@ class Desugar { } private void desugarAndWriteGeneratedClasses( - OutputFileProvider outputFileProvider, ClassReaderFactory bootclasspathReader) + OutputFileProvider outputFileProvider, + ClassReaderFactory bootclasspathReader, + @Nullable CoreLibrarySupport coreLibrarySupport) throws IOException { // Write out any classes we generated along the way ImmutableMap generatedClasses = store.drain(); @@ -552,8 +591,13 @@ class Desugar { UnprefixingClassWriter writer = rewriter.writer(ClassWriter.COMPUTE_MAXS); // checkState above implies that we want Java 7 .class files, so send through that visitor. // Don't need a ClassReaderFactory b/c static interface methods should've been moved. - ClassVisitor visitor = - new Java7Compatibility(writer, (ClassReaderFactory) null, bootclasspathReader); + ClassVisitor visitor = writer; + if (coreLibrarySupport != null) { + visitor = new CorePackageRenamer(visitor, coreLibrarySupport); + visitor = new CoreLibraryInvocationRewriter(visitor, coreLibrarySupport); + } + + visitor = new Java7Compatibility(visitor, (ClassReaderFactory) null, bootclasspathReader); generated.getValue().accept(visitor); String filename = rewriter.unprefix(generated.getKey()) + ".class"; outputFileProvider.write(filename, writer.toByteArray()); @@ -569,6 +613,7 @@ class Desugar { @Nullable ClassReaderFactory classpathReader, DependencyCollector depsCollector, ClassReaderFactory bootclasspathReader, + @Nullable CoreLibrarySupport coreLibrarySupport, ClassVsInterface interfaceCache, ImmutableSet interfaceLambdaMethods, @Nullable ClassReaderFactory bridgeMethodReader, @@ -576,6 +621,12 @@ class Desugar { UnprefixingClassWriter writer, ClassReader input) { ClassVisitor visitor = checkNotNull(writer); + + if (coreLibrarySupport != null) { + visitor = new CorePackageRenamer(visitor, coreLibrarySupport); + visitor = new CoreLibraryInvocationRewriter(visitor, coreLibrarySupport); + } + if (!allowTryWithResources) { CloseResourceMethodScanner closeResourceMethodScanner = new CloseResourceMethodScanner(); input.accept(closeResourceMethodScanner, ClassReader.SKIP_DEBUG); @@ -612,6 +663,7 @@ class Desugar { options.legacyJacocoFix); } } + visitor = new LambdaClassFixer( visitor, @@ -639,11 +691,18 @@ class Desugar { @Nullable ClassReaderFactory classpathReader, DependencyCollector depsCollector, ClassReaderFactory bootclasspathReader, + @Nullable CoreLibrarySupport coreLibrarySupport, ClassVsInterface interfaceCache, ImmutableSet.Builder interfaceLambdaMethodCollector, UnprefixingClassWriter writer, ClassReader input) { ClassVisitor visitor = checkNotNull(writer); + + if (coreLibrarySupport != null) { + visitor = new CorePackageRenamer(visitor, coreLibrarySupport); + visitor = new CoreLibraryInvocationRewriter(visitor, coreLibrarySupport); + } + if (!allowTryWithResources) { CloseResourceMethodScanner closeResourceMethodScanner = new CloseResourceMethodScanner(); input.accept(closeResourceMethodScanner, ClassReader.SKIP_DEBUG); @@ -678,6 +737,7 @@ class Desugar { options.legacyJacocoFix); } } + // LambdaDesugaring is relatively expensive, so check first whether we need it. Additionally, // we need to collect lambda methods referenced by invokedynamic instructions up-front anyway. // TODO(kmb): Scan constant pool instead of visiting the class to find bootstrap methods etc. diff --git a/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java b/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java index b8b3ead..3524fae 100644 --- a/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java +++ b/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java @@ -265,8 +265,7 @@ class InterfaceDesugaring extends ClassVisitor { return "".equals(methodName); } - private static String normalizeInterfaceMethodName( - String name, boolean isLambda, boolean isStatic) { + static String normalizeInterfaceMethodName(String name, boolean isLambda, boolean isStatic) { if (isLambda) { // Rename lambda method to reflect the new owner. Not doing so confuses LambdaDesugaring // if it's run over this class again. LambdaDesugaring has already renamed the method from diff --git a/test/java/com/google/devtools/build/android/desugar/CoreLibrarySupportTest.java b/test/java/com/google/devtools/build/android/desugar/CoreLibrarySupportTest.java new file mode 100644 index 0000000..d7fcad4 --- /dev/null +++ b/test/java/com/google/devtools/build/android/desugar/CoreLibrarySupportTest.java @@ -0,0 +1,232 @@ +// Copyright 2018 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package com.google.devtools.build.android.desugar; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.collect.ImmutableList; +import java.util.Collection; +import java.util.Comparator; +import java.util.Map; +import java.util.concurrent.ConcurrentMap; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.objectweb.asm.Opcodes; + +@RunWith(JUnit4.class) +public class CoreLibrarySupportTest { + + @Test + public void testIsRenamedCoreLibrary() throws Exception { + CoreLibrarySupport support = + new CoreLibrarySupport( + new CoreLibraryRewriter(""), null, ImmutableList.of("java/time/"), ImmutableList.of()); + assertThat(support.isRenamedCoreLibrary("java/time/X")).isTrue(); + assertThat(support.isRenamedCoreLibrary("java/time/y/X")).isTrue(); + assertThat(support.isRenamedCoreLibrary("java/io/X")).isFalse(); + assertThat(support.isRenamedCoreLibrary("com/google/X")).isFalse(); + } + + @Test + public void testIsRenamedCoreLibrary_prefixedLoader() throws Exception { + CoreLibrarySupport support = + new CoreLibrarySupport( + new CoreLibraryRewriter("__/"), + null, + ImmutableList.of("java/time/"), + ImmutableList.of()); + assertThat(support.isRenamedCoreLibrary("__/java/time/X")).isTrue(); + assertThat(support.isRenamedCoreLibrary("__/java/time/y/X")).isTrue(); + assertThat(support.isRenamedCoreLibrary("__/java/io/X")).isFalse(); + assertThat(support.isRenamedCoreLibrary("com/google/X")).isFalse(); + } + @Test + public void testRenameCoreLibrary() throws Exception { + CoreLibrarySupport support = + new CoreLibrarySupport( + new CoreLibraryRewriter(""), null, ImmutableList.of(), ImmutableList.of()); + assertThat(support.renameCoreLibrary("java/time/X")).isEqualTo("j$/time/X"); + assertThat(support.renameCoreLibrary("com/google/X")).isEqualTo("com/google/X"); + } + + @Test + public void testRenameCoreLibrary_prefixedLoader() throws Exception { + CoreLibrarySupport support = + new CoreLibrarySupport( + new CoreLibraryRewriter("__/"), null, ImmutableList.of(), ImmutableList.of()); + assertThat(support.renameCoreLibrary("__/java/time/X")).isEqualTo("j$/time/X"); + assertThat(support.renameCoreLibrary("com/google/X")).isEqualTo("com/google/X"); + } + + @Test + public void testIsEmulatedCoreLibraryInvocation() throws Exception { + CoreLibrarySupport support = + new CoreLibrarySupport( + new CoreLibraryRewriter(""), + Thread.currentThread().getContextClassLoader(), + ImmutableList.of(), + ImmutableList.of("java/util/Collection")); + assertThat( + support.isEmulatedCoreLibraryInvocation( + Opcodes.INVOKEINTERFACE, + "java/util/Collection", + "removeIf", + "(Ljava/util/function/Predicate;)Z", + true)) + .isTrue(); // true for default method + assertThat( + support.isEmulatedCoreLibraryInvocation( + Opcodes.INVOKEINTERFACE, "java/util/Collection", "size", "()I", true)) + .isFalse(); // false for abstract method + } + + @Test + public void testGetEmulatedCoreLibraryInvocationTarget_defaultMethod() throws Exception { + CoreLibrarySupport support = + new CoreLibrarySupport( + new CoreLibraryRewriter(""), + Thread.currentThread().getContextClassLoader(), + ImmutableList.of(), + ImmutableList.of("java/util/Collection")); + assertThat( + support.getEmulatedCoreLibraryInvocationTarget( + Opcodes.INVOKEINTERFACE, + "java/util/Collection", + "removeIf", + "(Ljava/util/function/Predicate;)Z", + true)) + .isEqualTo(Collection.class); + assertThat( + support.getEmulatedCoreLibraryInvocationTarget( + Opcodes.INVOKEVIRTUAL, + "java/util/ArrayList", + "removeIf", + "(Ljava/util/function/Predicate;)Z", + false)) + .isEqualTo(Collection.class); + assertThat( + support.getEmulatedCoreLibraryInvocationTarget( + Opcodes.INVOKEINTERFACE, + "com/google/common/collect/ImmutableList", + "removeIf", + "(Ljava/util/function/Predicate;)Z", + true)) + .isNull(); + } + + @Test + public void testGetEmulatedCoreLibraryInvocationTarget_abstractMethod() throws Exception { + CoreLibrarySupport support = + new CoreLibrarySupport( + new CoreLibraryRewriter(""), + Thread.currentThread().getContextClassLoader(), + ImmutableList.of(), + ImmutableList.of("java/util/Collection")); + assertThat( + support.getEmulatedCoreLibraryInvocationTarget( + Opcodes.INVOKEINTERFACE, + "java/util/Collection", + "size", + "()I", + true)) + .isNull(); + assertThat( + support.getEmulatedCoreLibraryInvocationTarget( + Opcodes.INVOKEVIRTUAL, + "java/util/ArrayList", + "size", + "()I", + false)) + .isNull(); + } + + @Test + public void testGetEmulatedCoreLibraryInvocationTarget_defaultOverride() throws Exception { + CoreLibrarySupport support = + new CoreLibrarySupport( + new CoreLibraryRewriter(""), + Thread.currentThread().getContextClassLoader(), + ImmutableList.of(), + ImmutableList.of("java/util/Map")); + assertThat( + support.getEmulatedCoreLibraryInvocationTarget( + Opcodes.INVOKEINTERFACE, + "java/util/Map", + "putIfAbsent", + "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", + true)) + .isEqualTo(Map.class); + assertThat( + support.getEmulatedCoreLibraryInvocationTarget( + Opcodes.INVOKEINTERFACE, + "java/util/concurrent/ConcurrentMap", + "putIfAbsent", + "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", + true)) + .isNull(); // putIfAbsent is default in Map but abstract in ConcurrentMap + assertThat( + support.getEmulatedCoreLibraryInvocationTarget( + Opcodes.INVOKEINTERFACE, + "java/util/concurrent/ConcurrentMap", + "getOrDefault", + "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", + true)) + .isEqualTo(ConcurrentMap.class); // conversely, getOrDefault is overridden as default method + } + + @Test + public void testGetEmulatedCoreLibraryInvocationTarget_staticInterfaceMethod() throws Exception { + CoreLibrarySupport support = + new CoreLibrarySupport( + new CoreLibraryRewriter(""), + Thread.currentThread().getContextClassLoader(), + ImmutableList.of(), + ImmutableList.of("java/util/Comparator")); + assertThat( + support.getEmulatedCoreLibraryInvocationTarget( + Opcodes.INVOKESTATIC, + "java/util/Comparator", + "reverseOrder", + "()Ljava/util/Comparator;", + true)) + .isEqualTo(Comparator.class); + } + + @Test + public void testGetEmulatedCoreLibraryInvocationTarget_ignoreRenamed() throws Exception { + CoreLibrarySupport support = + new CoreLibrarySupport( + new CoreLibraryRewriter(""), + Thread.currentThread().getContextClassLoader(), + ImmutableList.of("java/util/concurrent/"), // should return null for these + ImmutableList.of("java/util/Map")); + assertThat( + support.getEmulatedCoreLibraryInvocationTarget( + Opcodes.INVOKEINTERFACE, + "java/util/Map", + "getOrDefault", + "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", + true)) + .isEqualTo(Map.class); + assertThat( + support.getEmulatedCoreLibraryInvocationTarget( + Opcodes.INVOKEINTERFACE, + "java/util/concurrent/ConcurrentMap", + "getOrDefault", + "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", + true)) + .isNull(); + } +} diff --git a/test/java/com/google/devtools/build/android/desugar/CorePackageRenamerTest.java b/test/java/com/google/devtools/build/android/desugar/CorePackageRenamerTest.java new file mode 100644 index 0000000..0626b46 --- /dev/null +++ b/test/java/com/google/devtools/build/android/desugar/CorePackageRenamerTest.java @@ -0,0 +1,90 @@ +// Copyright 2018 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package com.google.devtools.build.android.desugar; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.collect.ImmutableList; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +@RunWith(JUnit4.class) +public class CorePackageRenamerTest { + + @Test + public void testSymbolRewrite() throws Exception { + MockClassVisitor out = new MockClassVisitor(); + CorePackageRenamer renamer = new CorePackageRenamer( + out, + new CoreLibrarySupport( + new CoreLibraryRewriter(""), null, ImmutableList.of("java/time/"), ImmutableList.of())); + MethodVisitor mv = renamer.visitMethod(0, "test", "()V", null, null); + + mv.visitMethodInsn( + Opcodes.INVOKESTATIC, "java/time/Instant", "now", "()Ljava/time/Instant;", false); + assertThat(out.mv.owner).isEqualTo("j$/time/Instant"); + assertThat(out.mv.desc).isEqualTo("()Lj$/time/Instant;"); + + mv.visitMethodInsn( + Opcodes.INVOKESTATIC, "other/time/Instant", "now", "()Ljava/time/Instant;", false); + assertThat(out.mv.owner).isEqualTo("other/time/Instant"); + assertThat(out.mv.desc).isEqualTo("()Lj$/time/Instant;"); + + mv.visitFieldInsn( + Opcodes.GETFIELD, "other/time/Instant", "now", "Ljava/time/Instant;"); + assertThat(out.mv.owner).isEqualTo("other/time/Instant"); + assertThat(out.mv.desc).isEqualTo("Lj$/time/Instant;"); + } + + private static class MockClassVisitor extends ClassVisitor { + + final MockMethodVisitor mv = new MockMethodVisitor(); + + public MockClassVisitor() { + super(Opcodes.ASM6); + } + + @Override + public MethodVisitor visitMethod( + int access, String name, String desc, String signature, String[] exceptions) { + return mv; + } + } + + private static class MockMethodVisitor extends MethodVisitor { + + String owner; + String desc; + + public MockMethodVisitor() { + super(Opcodes.ASM6); + } + + @Override + public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) { + this.owner = owner; + this.desc = desc; + } + + @Override + public void visitFieldInsn(int opcode, String owner, String name, String desc) { + this.owner = owner; + this.desc = desc; + } + } +} -- cgit v1.2.3