Skip to content

Commit

Permalink
Migrate: migrate to the new Instrumentation API (#17)
Browse files Browse the repository at this point in the history
* Migrate: migrate to new agp implement

* Chore: add comment marks

* Chore: mark final

* Improve: add inner class support & fix class name resolving

* Improve: let annotation processor compatible with legacy version

* Chore: update comments

---------

Co-authored-by: RikkaW <[email protected]>
  • Loading branch information
Kr328 and RikkaW authored Feb 3, 2023
1 parent b8cc80a commit b06bb67
Show file tree
Hide file tree
Showing 20 changed files with 236 additions and 515 deletions.
3 changes: 3 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
* text=auto eol=lf
*.bat text eol=crlf
*.jar binary
5 changes: 1 addition & 4 deletions annotation-processor/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,13 @@ dependencies {
annotationProcessor(libs.google.service.compiler)

implementation(libs.google.service.annotation)
implementation(libs.javassist)
implementation(libs.asm.all)
}

publishing {
publications {
create(project.name, MavenPublication::class) {
from(components["java"])

artifact(tasks["sourcesJar"])
artifact(tasks["javadocJar"])
}
}
}
Original file line number Diff line number Diff line change
@@ -1,157 +1,164 @@
package dev.rikka.tools.refine;

import com.google.auto.service.AutoService;
import javassist.bytecode.AnnotationsAttribute;
import javassist.bytecode.ClassFile;
import javassist.bytecode.annotation.Annotation;
import javassist.bytecode.annotation.ClassMemberValue;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.*;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.MirroredTypeException;
import javax.tools.FileObject;
import javax.tools.StandardLocation;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
* Annotation processor that generates classes which store metadata.
*/
@AutoService(Processor.class)
public class RefineProcessor extends AbstractProcessor {
public @interface Descriptor {}
/**
* Class name suffix.
*/
public static final String REFINE_METADATA_CLASS_NAME = String.valueOf(
'R' << 16 | 'E' << 16 | 'F' << 8 | 'I' << 8 | 'N' | 'E'
);

/**
* Annotation prefix that store refine to.
*/
public static final String REFINE_NS_PACKAGE = "refine";

public static final String DESCRIPTOR_REFINE_DESCRIPTOR = Descriptor.class.getName();
public static final String DESCRIPTOR_REFINE_FROM = "from";
public static final String DESCRIPTOR_REFINE_TO = "to";
@Override
public Set<String> getSupportedAnnotationTypes() {
return Set.of(RefineAs.class.getName());
}

private static final String REFINE_DESCRIPTION_SUFFIX = String.valueOf('R' << 16 | 'E' << 16 | 'F' << 8 | 'I' << 8 | 'N' | 'E');
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.RELEASE_8;
}

private static String resolveInternalName(Element element) {
private static String resolveClassName(final Element element) {
final Element enclosing = element.getEnclosingElement();

if (enclosing instanceof TypeElement) {
return resolveInternalName(enclosing) + "$" + element.getSimpleName();
return resolveClassName(enclosing) + "$" + element.getSimpleName();
} else if (enclosing instanceof PackageElement) {
return ((PackageElement) enclosing).getQualifiedName().toString()
.replace('.', '/') + "/" + element.getSimpleName();
return ((PackageElement) enclosing).getQualifiedName()+ "." + element.getSimpleName();
}

return element.getSimpleName().toString();
}

private static String resolveClassNameFromRefine(RefineAs refine) {
try {
return refine.value().getName();
} catch (MirroredTypeException e) {
return resolveInternalName(((DeclaredType) e.getTypeMirror()).asElement());
private void writeRefineMetadata(final String from, final String to, final Element... dependencies) throws IOException {
final String metadataName = from + "$" + REFINE_METADATA_CLASS_NAME;
final String refineToAnnotation = REFINE_NS_PACKAGE + "." + to;

final ClassWriter metadataWriter = new ClassWriter(0);
metadataWriter.visit(
Opcodes.V1_8,
Opcodes.ACC_FINAL | Opcodes.ACC_PUBLIC | Opcodes.ACC_SUPER,
metadataName.replace('.', '/'),
null,
Type.getInternalName(Object.class),
null
);

final AnnotationVisitor descriptor = metadataWriter.visitAnnotation(Type.getDescriptor(Descriptor.class), false);
descriptor.visit("from", Type.getType("L" + from.replace('.', '/') + ';'));
descriptor.visit("to", Type.getType("L" + to.replace('.', '/') + ";"));
descriptor.visitEnd();

metadataWriter.visitAnnotation("L" + refineToAnnotation.replace('.', '/') + ";", false).visitEnd();
metadataWriter.visitEnd();

final FileObject metadataFile = processingEnv.getFiler().createClassFile(metadataName, dependencies);
try (final OutputStream stream = metadataFile.openOutputStream()) {
stream.write(metadataWriter.toByteArray());
}
}

@Override
public Set<String> getSupportedAnnotationTypes() {
return Set.of(RefineAs.class.getName());
if (processingEnv.getElementUtils().getTypeElement(to.replace('$', '.')) == null) {
final ClassWriter stubWriter = new ClassWriter(0);
stubWriter.visit(
Opcodes.V1_8,
Opcodes.ACC_FINAL | Opcodes.ACC_PUBLIC | Opcodes.ACC_SUPER,
to.replace('.', '/'),
null,
Type.getInternalName(Object.class),
null
);
stubWriter.visitEnd();

final FileObject stubFile = processingEnv.getFiler().createClassFile(to, dependencies);
try (final OutputStream stream = stubFile.openOutputStream()) {
stream.write(stubWriter.toByteArray());
}
}
}

@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.RELEASE_8;
private void processElement(final TypeElement element, final String toClass) throws IOException {
final String fromClass = resolveClassName(element);

writeRefineMetadata(fromClass, toClass, element);

for (final Element enclosedElement : element.getEnclosedElements()) {
if (!(enclosedElement instanceof TypeElement)) {
continue;
}

if (enclosedElement.getAnnotation(RefineAs.class) != null) {
continue;
}

processElement((TypeElement) enclosedElement, toClass + "$" + enclosedElement.getSimpleName());
}
}

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
public boolean process(final Set<? extends TypeElement> annotations, final RoundEnvironment roundEnv) {
try {
for (final Element element : roundEnv.getElementsAnnotatedWith(RefineAs.class)) {
final TypeElement eRefineAs = processingEnv.getElementUtils().getTypeElement(RefineAs.class.getName());
final ExecutableElement eRefineAsValue = (ExecutableElement) eRefineAs.getEnclosedElements().stream()
.filter(e -> (e instanceof ExecutableElement) && e.getSimpleName().contentEquals("value"))
.findAny()
.orElseThrow(() -> new IllegalStateException("Invalid @RefineAs annotation."));

for (final Element element : roundEnv.getElementsAnnotatedWith(eRefineAs)) {
if (!(element instanceof TypeElement)) {
continue;
}

final RefineAs refine = element.getAnnotation(RefineAs.class);
if (refine == null) {
final AnnotationMirror refineAs = element.getAnnotationMirrors().stream()
.filter(a -> eRefineAs.equals(a.getAnnotationType().asElement()))
.findAny()
.orElse(null);
if (refineAs == null) {
continue;
}

final String original = resolveInternalName(element);
final String replaced = resolveClassNameFromRefine(refine);
final AnnotationValue refineAsValue = refineAs.getElementValues().get(eRefineAsValue);
if (refineAsValue == null) {
continue;
}

processType((TypeElement) element, element, original, replaced);
processElement((TypeElement) element, resolveClassName(((DeclaredType) refineAsValue.getValue()).asElement()));
}
} catch (IOException e) {
} catch (final Exception e) {
throw new RuntimeException(e);
}

return true;
}

private void processType(TypeElement type, Element root, String prefix, String replace) throws IOException {
final String sourceInternalName = resolveInternalName(type);

if (!sourceInternalName.startsWith(prefix)) {
return;
}

final String[] sourceFragments = sourceInternalName.split("/");
final String sourcePackageName = Stream.of(sourceFragments)
.limit(sourceFragments.length - 1)
.collect(Collectors.joining("."));
final String sourceClassName = sourceFragments[sourceFragments.length - 1];

final String descriptionName = sourceClassName + "$" + REFINE_DESCRIPTION_SUFFIX;
final String descriptionInternalName = sourcePackageName.replace('.', '/') + "/" + descriptionName;

final FileObject refineFile = processingEnv.getFiler()
.createResource(StandardLocation.CLASS_OUTPUT, sourcePackageName, descriptionName + ".class", type, root);

final String targetInternalName = replace + sourceInternalName.substring(prefix.length());

try (DataOutputStream stream = new DataOutputStream(refineFile.openOutputStream())) {
final ClassFile cls = new ClassFile(true, descriptionInternalName, null);
final AnnotationsAttribute annotations = new AnnotationsAttribute(cls.getConstPool(), AnnotationsAttribute.invisibleTag);
final Annotation annotation = new Annotation(DESCRIPTOR_REFINE_DESCRIPTOR, cls.getConstPool());

annotation.addMemberValue(DESCRIPTOR_REFINE_FROM, new ClassMemberValue(sourceInternalName, cls.getConstPool()));
annotation.addMemberValue(DESCRIPTOR_REFINE_TO, new ClassMemberValue(targetInternalName, cls.getConstPool()));

annotations.addAnnotation(annotation);

cls.addAttribute(annotations);

cls.write(stream);
}

final String[] targetFragments = targetInternalName.split("/");
final String targetPackageName = Stream.of(targetFragments)
.limit(sourceFragments.length - 1)
.collect(Collectors.joining("."));
final String targetClassName = targetFragments[targetFragments.length - 1];

final TypeElement target = processingEnv.getElementUtils()
.getTypeElement(targetPackageName + "." + targetClassName.replace('$', '.'));
if (target == null) {
final FileObject classFile = processingEnv.getFiler()
.createResource(StandardLocation.CLASS_OUTPUT, targetPackageName, targetClassName + ".class", type, root);

try (DataOutputStream stream = new DataOutputStream(classFile.openOutputStream())) {
new ClassFile(type.getKind().isInterface(), targetInternalName, null).write(stream);
}
}

for (final Element enclosed : type.getEnclosedElements()) {
if (!(enclosed instanceof TypeElement)) {
continue;
}

if (enclosed.getAnnotation(RefineAs.class) != null || enclosed.getAnnotation(Descriptor.class) != null) {
continue;
}

processType((TypeElement) enclosed, root, prefix, replace);
}
/**
* Mark class is a metadata class.
*/
public @interface Descriptor {
}
}
3 changes: 0 additions & 3 deletions annotation/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@ publishing {
publications {
create(project.name, MavenPublication::class) {
from(components["java"])

artifact(tasks["sourcesJar"])
artifact(tasks["javadocJar"])
}
}
}
6 changes: 6 additions & 0 deletions annotation/src/main/java/dev/rikka/tools/refine/RefineAs.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,14 @@
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Attach this annotation to a class to indicate that this class should be renamed to {@link #value()}.
*/
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface RefineAs {
/**
* Class to rename to.
*/
Class<?> value();
}
22 changes: 4 additions & 18 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,18 @@ task("clean", type = Delete::class) {

subprojects {
group = "dev.rikka.tools.refine"
version = "3.1.1"
version = "4.0.0"

plugins.withId("java") {
println("- Configuring `java`")

extensions.configure<JavaPluginExtension> {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
tasks.register("sourcesJar", type = Jar::class) {
archiveClassifier.set("sources")
from(project.extensions.getByType<SourceSetContainer>().getByName("main").allSource)
}
tasks.register("javadocJar", type = Jar::class) {
archiveClassifier.set("javadoc")
from(tasks["javadoc"])
}
tasks.withType(Javadoc::class) {
isFailOnError = false

withSourcesJar()
withJavadocJar()
}
}
plugins.withId("maven-publish") {
println("- Configuring `publishing`")

extensions.configure<PublishingExtension> {
publications {
withType(MavenPublication::class) {
Expand Down Expand Up @@ -66,8 +54,6 @@ subprojects {
}
}
plugins.withId("signing") {
println("- Configuring `signing`")

extensions.configure<SigningExtension> {
if (findProperty("signing.gnupg.keyName") != null) {
useGpgCmd()
Expand Down
6 changes: 1 addition & 5 deletions gradle-plugin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ dependencies {
implementation(project(":annotation-processor"))

compileOnly(libs.android.gradle)
implementation(libs.google.gson)
implementation(libs.javassist)
implementation(libs.asm.all)
}

gradlePlugin {
Expand All @@ -32,9 +31,6 @@ afterEvaluate {
publications {
named("pluginMaven", MavenPublication::class) {
artifactId = project.name

artifact(tasks["sourcesJar"])
artifact(tasks["javadocJar"])
}
}
}
Expand Down
Loading

0 comments on commit b06bb67

Please sign in to comment.