| // 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.shaking.constructor; |
| |
| import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent; |
| import static org.hamcrest.CoreMatchers.containsString; |
| import static org.hamcrest.CoreMatchers.not; |
| import static org.hamcrest.MatcherAssert.assertThat; |
| import static org.junit.Assert.assertFalse; |
| import static org.junit.Assert.fail; |
| import static org.junit.Assume.assumeTrue; |
| |
| import com.android.tools.r8.CompilationFailedException; |
| import com.android.tools.r8.ProguardTestCompileResult; |
| import com.android.tools.r8.R8TestCompileResult; |
| import com.android.tools.r8.TestBase; |
| import com.android.tools.r8.ToolHelper; |
| import com.android.tools.r8.utils.codeinspector.ClassSubject; |
| import com.android.tools.r8.utils.codeinspector.CodeInspector; |
| import com.android.tools.r8.utils.codeinspector.MethodSubject; |
| import com.google.common.collect.ImmutableList; |
| import java.util.Collection; |
| import java.util.List; |
| import org.junit.BeforeClass; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.junit.runners.Parameterized; |
| |
| class InitMatchingTestClass { |
| // Trivial <clinit> |
| static { |
| } |
| int field; |
| public InitMatchingTestClass(int arg) { |
| field = arg; |
| } |
| } |
| |
| @RunWith(Parameterized.class) |
| public class InitMatchingTest extends TestBase { |
| public static final List<String> INIT_NAMES = ImmutableList.of( |
| "<random>", "<clinit>", "<init>", "<*init>", "<in*>", "<in*", "*init>", "<1>", "<<1>init>", |
| "void <clinit>", "void <init>", "void <*init>", "void <in*>", "void <in*", "void *init>", |
| "void <1>", "void <<1>init>", "public void <init>", "public void <*init>", |
| "public void <clinit>", "static void <clinit>", "XYZ <clinit>", "private XYZ <init>" |
| ); |
| |
| @Parameterized.Parameters(name = "{0} \"{1}\"") |
| public static Collection<Object[]> data() { |
| return buildParameters(ToolHelper.getBackends(), INIT_NAMES); |
| } |
| |
| private final Backend backend; |
| private final String initName; |
| |
| public InitMatchingTest(Backend backend, String initName) { |
| this.backend = backend; |
| this.initName = initName; |
| } |
| |
| private String createKeepRule() { |
| return "-keep class * { " + initName + "(...); }"; |
| } |
| |
| private static final List<String> ALLOWED_INIT_NAMES_PG = ImmutableList.of( |
| "<init>", "void <clinit>", "void <init>", "void <1>", "public void <init>", |
| "void <*init>", "void <in*>", "void *init>", "public void <*init>", |
| "void <<1>init>", "public void <clinit>", "static void <clinit>", |
| "XYZ <clinit>", "private XYZ <init>"); |
| private static final List<String> EFFECTIVE_INIT_NAMES_PG = ImmutableList.of( |
| "<init>", "void <init>", "public void <init>", |
| "void <*init>", "void <in*>", "void *init>", "public void <*init>"); |
| private static final List<String> EFFECTIVE_CLINIT_NAMES_PG = ImmutableList.of( |
| "void <clinit>", "static void <clinit>", "void <*init>", "void *init>"); |
| |
| @BeforeClass |
| public static void checkPGInitNames() { |
| assert INIT_NAMES.containsAll(ALLOWED_INIT_NAMES_PG); |
| assert ALLOWED_INIT_NAMES_PG.containsAll(EFFECTIVE_INIT_NAMES_PG); |
| assert ALLOWED_INIT_NAMES_PG.containsAll(EFFECTIVE_CLINIT_NAMES_PG); |
| } |
| |
| @Test |
| public void testProguard() throws Exception { |
| assumeTrue(backend == Backend.CF); |
| ProguardTestCompileResult result; |
| try { |
| result = |
| testForProguard() |
| .addProgramClasses(InitMatchingTestClass.class) |
| .addKeepRules(createKeepRule()) |
| .compile(); |
| if (!ALLOWED_INIT_NAMES_PG.contains(initName)) { |
| fail("Expect to fail"); |
| } |
| } catch (CompilationFailedException e) { |
| assertFalse(ALLOWED_INIT_NAMES_PG.contains(initName)); |
| if (initName.equals("void <in*")) { |
| assertThat(e.getMessage(), containsString("Missing closing angular bracket")); |
| } else { |
| assertThat(e.getMessage(), |
| containsString("Expecting type and name instead of just '" + initName + "'")); |
| } |
| return; |
| } |
| result.inspect(this::inspectProguard); |
| } |
| |
| private void inspectProguard(CodeInspector inspector) { |
| ClassSubject classSubject = inspector.clazz(InitMatchingTestClass.class); |
| assertThat(classSubject, isPresent()); |
| MethodSubject init = classSubject.init(ImmutableList.of("int")); |
| if (EFFECTIVE_INIT_NAMES_PG.contains(initName)) { |
| assertThat(init, isPresent()); |
| } else { |
| assertThat(init, not(isPresent())); |
| } |
| MethodSubject clinit = classSubject.clinit(); |
| if (EFFECTIVE_CLINIT_NAMES_PG.contains(initName)) { |
| assertThat(clinit, isPresent()); |
| } else { |
| assertThat(clinit, not(isPresent())); |
| } |
| } |
| |
| // "[[access-flag]* void] <[cl]init>" is the only valid format. Plus legitimate back-references. |
| public static final List<String> ALLOWED_INIT_NAMES = ImmutableList.of( |
| "<clinit>", "<init>", "void <clinit>", "void <init>", "void <1>", |
| "public void <init>", "public void <clinit>", "static void <clinit>"); |
| private static final List<String> EFFECTIVE_INIT_NAMES = ImmutableList.of( |
| "<init>", "void <init>", "public void <init>"); |
| private static final List<String> EFFECTIVE_CLINIT_NAMES = ImmutableList.of( |
| "<clinit>", "void <clinit>", "static void <clinit>"); |
| |
| @BeforeClass |
| public static void checkR8InitNames() { |
| assert INIT_NAMES.containsAll(ALLOWED_INIT_NAMES); |
| assert ALLOWED_INIT_NAMES.containsAll(EFFECTIVE_INIT_NAMES); |
| } |
| |
| @Test |
| public void testR8() throws Exception { |
| R8TestCompileResult result; |
| try { |
| result = |
| testForR8(backend) |
| .addProgramClasses(InitMatchingTestClass.class) |
| .addKeepRules(createKeepRule()) |
| .compile(); |
| if (!ALLOWED_INIT_NAMES.contains(initName)) { |
| fail("Expect to fail"); |
| } |
| } catch (CompilationFailedException e) { |
| assertFalse(ALLOWED_INIT_NAMES.contains(initName)); |
| if (initName.contains("XYZ")) { |
| assertThat(e.getCause().getMessage(), |
| containsString("Expected [access-flag]* void ")); |
| return; |
| } |
| assertThat(e.getCause().getMessage(), |
| containsString("Unexpected character '" + (initName.contains("<") ? "<" : ">") + "'")); |
| assertThat(e.getCause().getMessage(), |
| containsString("only allowed in the method name '<init>'")); |
| return; |
| } |
| result |
| .assertNoMessages() |
| .inspect(this::inspectR8); |
| } |
| |
| private void inspectR8(CodeInspector inspector) { |
| ClassSubject classSubject = inspector.clazz(InitMatchingTestClass.class); |
| assertThat(classSubject, isPresent()); |
| MethodSubject init = classSubject.init(ImmutableList.of("int")); |
| if (EFFECTIVE_INIT_NAMES.contains(initName)) { |
| assertThat(init, isPresent()); |
| } else { |
| assertThat(init, not(isPresent())); |
| } |
| // We only keep class initializers in debug mode. |
| MethodSubject clinit = classSubject.clinit(); |
| assertThat(clinit, not(isPresent())); |
| } |
| } |