blob: fca830b730f24717854330f756c0abb4c0b98407 [file]
// Copyright (c) 2026, 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.keepanno;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import com.android.tools.r8.TestBase;
import com.android.tools.r8.ToolHelper;
import com.android.tools.r8.keepanno.annotations.KeepItemKind;
import com.android.tools.r8.keepanno.annotations.UsedByReflection;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import org.junit.Test;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
public class KeepAnnoChainingTest extends TestBase {
@UsedByReflection(kind = KeepItemKind.CLASS_AND_MEMBERS)
static class AnnotatedClass {
@UsedByReflection(kind = KeepItemKind.CLASS_AND_MEMBERS)
public int field;
@UsedByReflection(kind = KeepItemKind.CLASS_AND_MEMBERS)
public void method() {}
}
private static class RecordingVisitor extends ClassVisitor {
int visitCount = 0;
int visitAnnotationCount = 0;
int visitFieldCount = 0;
int visitMethodCount = 0;
List<String> visitedAnnotations = new ArrayList<>();
List<String> visitedFields = new ArrayList<>();
List<String> visitedMethods = new ArrayList<>();
List<String> visitedFieldAnnotations = new ArrayList<>();
List<String> visitedMethodAnnotations = new ArrayList<>();
RecordingVisitor() {
super(Opcodes.ASM9);
}
@Override
public void visit(
int version,
int access,
String name,
String signature,
String superName,
String[] interfaces) {
visitCount++;
super.visit(version, access, name, signature, superName, interfaces);
}
@Override
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
visitAnnotationCount++;
visitedAnnotations.add(descriptor);
return super.visitAnnotation(descriptor, visible);
}
@Override
public FieldVisitor visitField(
int access, String name, String descriptor, String signature, Object value) {
visitFieldCount++;
visitedFields.add(name);
FieldVisitor fieldVisitor = super.visitField(access, name, descriptor, signature, value);
return new FieldVisitor(Opcodes.ASM9, fieldVisitor) {
@Override
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
visitedFieldAnnotations.add(descriptor);
return super.visitAnnotation(descriptor, visible);
}
};
}
@Override
public MethodVisitor visitMethod(
int access, String name, String descriptor, String signature, String[] exceptions) {
visitMethodCount++;
visitedMethods.add(name);
MethodVisitor methodVisitor =
super.visitMethod(access, name, descriptor, signature, exceptions);
return new MethodVisitor(Opcodes.ASM9, methodVisitor) {
@Override
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
visitedMethodAnnotations.add(descriptor);
return super.visitAnnotation(descriptor, visible);
}
};
}
}
@Test
public void testChainingRulesExtractor() throws Exception {
byte[] bytes = Files.readAllBytes(ToolHelper.getClassFileForTestClass(AnnotatedClass.class));
ClassReader reader = new ClassReader(bytes);
List<String> rules = new ArrayList<>();
RecordingVisitor recorder = new RecordingVisitor();
ClassVisitor extractor =
KeepAnno.createClassVisitorForKeepRulesExtraction(rules::add, recorder);
reader.accept(extractor, ClassReader.SKIP_CODE);
// Verify rules were extracted
assertTrue(rules.size() > 0);
// Verify recorder saw everything
assertEquals(1, recorder.visitCount);
// AnnotatedClass has @UsedByReflection on class
assertTrue(recorder.visitedAnnotations.stream().anyMatch(d -> d.contains("UsedByReflection")));
// And it has field and method
assertTrue(recorder.visitedFields.contains("field"));
assertTrue(recorder.visitedMethods.contains("method"));
// Verify field and method annotations were delegated
assertTrue(
recorder.visitedFieldAnnotations.stream().anyMatch(d -> d.contains("UsedByReflection")));
assertTrue(
recorder.visitedMethodAnnotations.stream().anyMatch(d -> d.contains("UsedByReflection")));
}
@Test
public void testChainingRulesExtractorWithItself() throws Exception {
byte[] bytes = Files.readAllBytes(ToolHelper.getClassFileForTestClass(AnnotatedClass.class));
ClassReader reader = new ClassReader(bytes);
List<String> rules = new ArrayList<>();
ClassVisitor extractor = KeepAnno.createClassVisitorForKeepRulesExtraction(rules::add);
List<String> rules2 = new ArrayList<>();
ClassVisitor extractor2 =
KeepAnno.createClassVisitorForKeepRulesExtraction(rules2::add, extractor);
reader.accept(extractor2, ClassReader.SKIP_CODE);
// As the visitor is not making any changes but only reading the annotations chaining it with
// itself should extract the same rules twice.
assertTrue(rules.size() > 0);
assertEquals(rules2, rules);
}
}