Ensure member classes are not non-member classes.
For anonymous classes (non-null outer and null inner-name), we can
recover by erasing outer-class references.
For local classes (non-null inner-name and enclosing member), though,
it is unclear whether the intention was a member class or local class.
So we can't remove outer-class references in this case, and rather fail
hard.
Bug: 137881258
Change-Id: I4f1159fe978729bc72d959575b3707172ab2edb0
diff --git a/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java b/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java
index eae05d2..ee88c49 100644
--- a/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java
+++ b/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java
@@ -417,6 +417,33 @@
directMethods.toArray(DexEncodedMethod.EMPTY_ARRAY),
virtualMethods.toArray(DexEncodedMethod.EMPTY_ARRAY),
application.getFactory().getSkipNameValidationForTesting());
+ InnerClassAttribute innerClassAttribute = clazz.getInnerClassAttributeForThisClass();
+ // A member class should not be a local or anonymous class.
+ if (innerClassAttribute != null && innerClassAttribute.getOuter() != null) {
+ if (innerClassAttribute.isAnonymous()) {
+ assert innerClassAttribute.getInnerName() == null;
+ // If the enclosing member is not null, the intention would be an anonymous class, and
+ // thus the outer-class reference should have been null.
+ // If the enclosing member is null, it is likely due to the missing enclosing member, and
+ // we will warn that below. In either case, we can recover InnerClasses attribute by
+ // erasing the outer-class reference.
+ application.options.warningInvalidNonMemberClasses(
+ origin, enclosingMember, innerClassAttribute);
+ InnerClassAttribute recoveredAttribute = new InnerClassAttribute(
+ innerClassAttribute.getAccess(), innerClassAttribute.getInner(), null, null);
+ clazz.replaceInnerClassAttributeForThisClass(recoveredAttribute);
+ } else if (enclosingMember != null) {
+ assert innerClassAttribute.isNamed();
+ // It is unclear whether the intention was a member class or a local class. Fail hard.
+ throw new CompilationError(
+ StringUtils.lines(
+ "A member class should be a (non-member) local class at the same time.",
+ "This is likely due to invalid EnclosingMethod and InnerClasses attributes:",
+ enclosingMember.toString(),
+ innerClassAttribute.toString()),
+ origin);
+ }
+ }
if (enclosingMember == null
&& (clazz.isLocalClass() || clazz.isAnonymousClass())
&& getMajorVersion() > V1_6) {
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index cccd363..e4b84f6 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -30,6 +30,8 @@
import com.android.tools.r8.graph.DexMethod;
import com.android.tools.r8.graph.DexString;
import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.EnclosingMethodAttribute;
+import com.android.tools.r8.graph.InnerClassAttribute;
import com.android.tools.r8.ir.code.IRCode;
import com.android.tools.r8.ir.optimize.Inliner;
import com.android.tools.r8.origin.Origin;
@@ -538,6 +540,30 @@
return ImmutableSet.of();
}
+ private static class InvalidNonMemberClassInfo {
+ final EnclosingMethodAttribute enclosingMember;
+ final InnerClassAttribute innerClassAttribute;
+
+ InvalidNonMemberClassInfo(
+ EnclosingMethodAttribute enclosingMember,
+ InnerClassAttribute innerClassAttribute) {
+ this.enclosingMember = enclosingMember;
+ this.innerClassAttribute = innerClassAttribute;
+ }
+
+ @Override
+ public String toString() {
+ List<String> attrs = new ArrayList<>();
+ if (enclosingMember != null) {
+ attrs.add(" EnclosingMethod: " + enclosingMember.toString());
+ }
+ if (innerClassAttribute != null) {
+ attrs.add(" InnerClasses: " + innerClassAttribute.toString());
+ }
+ return StringUtils.join(attrs, System.lineSeparator());
+ }
+ }
+
public static class InvalidParameterAnnotationInfo {
final DexMethod method;
@@ -565,6 +591,9 @@
private final Map<Origin, List<TypeVersionPair>> missingEnclosingMembers = new HashMap<>();
+ private final Map<Origin, InvalidNonMemberClassInfo> warningInvalidNonMemberClasses =
+ new HashMap<>();
+
private final Map<Origin, List<InvalidParameterAnnotationInfo>> warningInvalidParameterAnnotations
= new HashMap<>();
@@ -771,6 +800,17 @@
}
}
+ public void warningInvalidNonMemberClasses(
+ Origin origin,
+ EnclosingMethodAttribute enclosingMethodAttribute,
+ InnerClassAttribute innerClassAttribute) {
+ InvalidNonMemberClassInfo info =
+ new InvalidNonMemberClassInfo(enclosingMethodAttribute, innerClassAttribute);
+ synchronized (warningInvalidNonMemberClasses) {
+ warningInvalidNonMemberClasses.put(origin, info);
+ }
+ }
+
public void warningInvalidParameterAnnotations(
DexMethod method, Origin origin, int expected, int actual) {
InvalidParameterAnnotationInfo info =
@@ -837,6 +877,21 @@
printed = true;
printOutdatedToolchain = true;
}
+ if (warningInvalidNonMemberClasses.size() > 0) {
+ reporter.info(
+ new StringDiagnostic(
+ "A member class should not be a local or anonymous class at the same time."
+ + " Such InnerClasses attributes are recovered."));
+ for (Origin origin : new TreeSet<>(warningInvalidNonMemberClasses.keySet())) {
+ StringBuilder builder =
+ new StringBuilder("Invalid EnclosingMethod/InnerClasses attributes:");
+ InvalidNonMemberClassInfo info = warningInvalidNonMemberClasses.get(origin);
+ builder.append(System.lineSeparator());
+ builder.append(info.toString());
+ reporter.info(new StringDiagnostic(builder.toString(), origin));
+ }
+ printed = true;
+ }
if (missingEnclosingMembers.size() > 0) {
reporter.info(
new StringDiagnostic(
diff --git a/src/test/java/com/android/tools/r8/d8/MemberAndLocalClassTest.java b/src/test/java/com/android/tools/r8/d8/MemberAndLocalClassTest.java
new file mode 100644
index 0000000..e4afafd
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/d8/MemberAndLocalClassTest.java
@@ -0,0 +1,135 @@
+// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.d8;
+
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.fail;
+
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.TestBase;
+import org.junit.Test;
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.Label;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+
+public class MemberAndLocalClassTest extends TestBase {
+
+ @Test
+ public void testD8() throws Exception {
+ try {
+ testForD8()
+ .addProgramClassFileData(Dump.dump())
+ .compile();
+ fail("Expected to fail due to invalid EnclosingMethod attribute");
+ } catch (CompilationFailedException e) {
+ String message = e.getCause().getMessage();
+
+ // We want to avoid showing mysterious messages.
+ assertThat(message, not(containsString("Unsorted annotation set")));
+ assertThat(message, not(containsString("dalvik.annotation.EnclosingClass")));
+
+ assertThat(message, containsString("invalid EnclosingMethod"));
+ }
+ }
+
+ // Compiled the following kt code:
+ // class WebContext {
+ // companion object {
+ // val debug = true
+ // }
+ // }
+ // and added EnclosingMethod attribute as described at b/137881258#comment7
+ private static class Dump implements Opcodes {
+
+ public static byte[] dump () throws Exception {
+
+ ClassWriter classWriter = new ClassWriter(0);
+ MethodVisitor methodVisitor;
+
+ classWriter.visit(
+ V1_6,
+ ACC_PUBLIC | ACC_FINAL | ACC_SUPER,
+ "WebContext$Companion",
+ null,
+ "java/lang/Object",
+ null);
+
+ classWriter.visitSource("WebContext.kt", null);
+
+ // Manually added, indicating this could be a local class.
+ classWriter.visitOuterClass("WebContext", null, null);
+
+ // But, it's actually a member class.
+ classWriter.visitInnerClass(
+ "WebContext$Companion", "WebContext", "Companion", ACC_PUBLIC | ACC_FINAL | ACC_STATIC);
+
+ {
+ methodVisitor =
+ classWriter.visitMethod(ACC_PUBLIC | ACC_FINAL, "getDebug", "()Z", null, null);
+ methodVisitor.visitCode();
+ Label label0 = new Label();
+ methodVisitor.visitLabel(label0);
+ methodVisitor.visitLineNumber(3, label0);
+ methodVisitor.visitMethodInsn(
+ INVOKESTATIC, "WebContext", "access$getDebug$cp", "()Z", false);
+ methodVisitor.visitInsn(IRETURN);
+ Label label1 = new Label();
+ methodVisitor.visitLabel(label1);
+ methodVisitor.visitLocalVariable("this", "LWebContext$Companion;", null, label0, label1, 0);
+ methodVisitor.visitMaxs(1, 1);
+ methodVisitor.visitEnd();
+ }
+ {
+ methodVisitor = classWriter.visitMethod(ACC_PRIVATE, "<init>", "()V", null, null);
+ methodVisitor.visitCode();
+ Label label0 = new Label();
+ methodVisitor.visitLabel(label0);
+ methodVisitor.visitLineNumber(2, label0);
+ methodVisitor.visitVarInsn(ALOAD, 0);
+ methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
+ methodVisitor.visitInsn(RETURN);
+ Label label1 = new Label();
+ methodVisitor.visitLabel(label1);
+ methodVisitor.visitLocalVariable("this", "LWebContext$Companion;", null, label0, label1, 0);
+ methodVisitor.visitMaxs(1, 1);
+ methodVisitor.visitEnd();
+ }
+ {
+ methodVisitor =
+ classWriter.visitMethod(
+ ACC_PUBLIC | ACC_SYNTHETIC,
+ "<init>",
+ "(Lkotlin/jvm/internal/DefaultConstructorMarker;)V",
+ null,
+ null);
+ methodVisitor.visitCode();
+ Label label0 = new Label();
+ methodVisitor.visitLabel(label0);
+ methodVisitor.visitLineNumber(2, label0);
+ methodVisitor.visitVarInsn(ALOAD, 0);
+ methodVisitor.visitMethodInsn(
+ INVOKESPECIAL, "WebContext$Companion", "<init>", "()V", false);
+ methodVisitor.visitInsn(RETURN);
+ Label label1 = new Label();
+ methodVisitor.visitLabel(label1);
+ methodVisitor.visitLocalVariable("this", "LWebContext$Companion;", null, label0, label1, 0);
+ methodVisitor.visitLocalVariable(
+ "$constructor_marker",
+ "Lkotlin/jvm/internal/DefaultConstructorMarker;",
+ null,
+ label0,
+ label1,
+ 1);
+ methodVisitor.visitMaxs(1, 2);
+ methodVisitor.visitEnd();
+ }
+ classWriter.visitEnd();
+
+ return classWriter.toByteArray();
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/d8/NonNamedMemberClassTest.java b/src/test/java/com/android/tools/r8/d8/NonNamedMemberClassTest.java
new file mode 100644
index 0000000..4ba6754
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/d8/NonNamedMemberClassTest.java
@@ -0,0 +1,122 @@
+// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.d8;
+
+import static org.hamcrest.CoreMatchers.containsString;
+
+import com.android.tools.r8.TestBase;
+import org.junit.Test;
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.Label;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+
+public class NonNamedMemberClassTest extends TestBase {
+
+ @Test
+ public void testD8() throws Exception {
+ testForD8()
+ .addProgramClassFileData(Dump.dump())
+ .compile()
+ .assertOnlyInfos()
+ .assertInfoMessageThatMatches(containsString("InnerClasses attributes are recovered"))
+ .assertInfoMessageThatMatches(containsString("inner: LWebContext$Companion;"))
+ .assertInfoMessageThatMatches(containsString("outer: LWebContext;"))
+ .assertInfoMessageThatMatches(containsString("innerName: (anonymous)"));
+ }
+
+ // Compiled the following kt code:
+ // class WebContext {
+ // companion object {
+ // val debug = true
+ // }
+ // }
+ // and removed inner-name, Companion.
+ private static class Dump implements Opcodes {
+
+ public static byte[] dump () throws Exception {
+
+ ClassWriter classWriter = new ClassWriter(0);
+ MethodVisitor methodVisitor;
+
+ classWriter.visit(
+ V1_6,
+ ACC_PUBLIC | ACC_FINAL | ACC_SUPER,
+ "WebContext$Companion",
+ null,
+ "java/lang/Object",
+ null);
+
+ classWriter.visitSource("WebContext.kt", null);
+
+ // Removed the inner-name, Companion.
+ classWriter.visitInnerClass(
+ "WebContext$Companion", "WebContext", null, ACC_PUBLIC | ACC_FINAL | ACC_STATIC);
+
+ {
+ methodVisitor =
+ classWriter.visitMethod(ACC_PUBLIC | ACC_FINAL, "getDebug", "()Z", null, null);
+ methodVisitor.visitCode();
+ Label label0 = new Label();
+ methodVisitor.visitLabel(label0);
+ methodVisitor.visitLineNumber(3, label0);
+ methodVisitor.visitMethodInsn(
+ INVOKESTATIC, "WebContext", "access$getDebug$cp", "()Z", false);
+ methodVisitor.visitInsn(IRETURN);
+ Label label1 = new Label();
+ methodVisitor.visitLabel(label1);
+ methodVisitor.visitLocalVariable("this", "LWebContext$Companion;", null, label0, label1, 0);
+ methodVisitor.visitMaxs(1, 1);
+ methodVisitor.visitEnd();
+ }
+ {
+ methodVisitor = classWriter.visitMethod(ACC_PRIVATE, "<init>", "()V", null, null);
+ methodVisitor.visitCode();
+ Label label0 = new Label();
+ methodVisitor.visitLabel(label0);
+ methodVisitor.visitLineNumber(2, label0);
+ methodVisitor.visitVarInsn(ALOAD, 0);
+ methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
+ methodVisitor.visitInsn(RETURN);
+ Label label1 = new Label();
+ methodVisitor.visitLabel(label1);
+ methodVisitor.visitLocalVariable("this", "LWebContext$Companion;", null, label0, label1, 0);
+ methodVisitor.visitMaxs(1, 1);
+ methodVisitor.visitEnd();
+ }
+ {
+ methodVisitor =
+ classWriter.visitMethod(
+ ACC_PUBLIC | ACC_SYNTHETIC,
+ "<init>",
+ "(Lkotlin/jvm/internal/DefaultConstructorMarker;)V",
+ null,
+ null);
+ methodVisitor.visitCode();
+ Label label0 = new Label();
+ methodVisitor.visitLabel(label0);
+ methodVisitor.visitLineNumber(2, label0);
+ methodVisitor.visitVarInsn(ALOAD, 0);
+ methodVisitor.visitMethodInsn(
+ INVOKESPECIAL, "WebContext$Companion", "<init>", "()V", false);
+ methodVisitor.visitInsn(RETURN);
+ Label label1 = new Label();
+ methodVisitor.visitLabel(label1);
+ methodVisitor.visitLocalVariable("this", "LWebContext$Companion;", null, label0, label1, 0);
+ methodVisitor.visitLocalVariable(
+ "$constructor_marker",
+ "Lkotlin/jvm/internal/DefaultConstructorMarker;",
+ null,
+ label0,
+ label1,
+ 1);
+ methodVisitor.visitMaxs(1, 2);
+ methodVisitor.visitEnd();
+ }
+ classWriter.visitEnd();
+
+ return classWriter.toByteArray();
+ }
+ }
+}