Skip to content

Commit

Permalink
Generate mixin configs automatically using custom AP (#305)
Browse files Browse the repository at this point in the history
  • Loading branch information
Phoenix-Starlight authored Dec 28, 2023
1 parent ae8cfba commit 06bfd71
Show file tree
Hide file tree
Showing 27 changed files with 555 additions and 108 deletions.
60 changes: 60 additions & 0 deletions annotation-processor/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
plugins {
id 'com.github.johnrengelman.shadow'
id 'java-library'
id 'com.diffplug.spotless'
}

repositories {
mavenCentral()
maven { url uri("https://maven.fabricmc.net") }
maven { url "https://maven.neoforged.net/releases" }
}

dependencies {
annotationProcessor 'com.google.auto.service:auto-service:1.1.1'
compileOnly 'com.google.auto.service:auto-service:1.1.1'

implementation 'com.google.code.gson:gson:2.10.1'
shadow 'com.google.code.gson:gson:2.10.1'
implementation 'com.google.auto:auto-common:1.2.1'
shadow 'com.google.auto:auto-common:1.2.1'
implementation 'com.google.guava:guava:21.0'
shadow 'com.google.guava:guava:21.0'

implementation project(":annotations")
shadow project(":annotations")
// Shadow annotations
implementation 'net.fabricmc:sponge-mixin:0.12.5+'
implementation "net.fabricmc:fabric-loader:${rootProject.fabric_loader_version}"
implementation 'net.minecraftforge:mergetool:1.1.7'
implementation 'net.neoforged:mergetool:2.0.2'
}

tasks.withType(JavaCompile) {
options.compilerArgs += '--enable-preview'
options.release = 17
}

shadowJar {
dependencies {
include(dependency('net.fabricmc:sponge-mixin:'))
include(dependency('net.fabricmc:fabric-loader:'))
include(dependency(':mergetool:'))
}
// shadowJar bug
include '*.jar'
include 'META-INF/services/javax.annotation.processing.Processor'
include 'org/spongepowered/asm/mixin/Mixin.class'
include 'org/fury_phoenix/**/*'
include {it.getName() == 'OnlyIn.class'}
include {it.getName() == 'Dist.class'}
include {it.getName() == 'Environment.class'}
include {it.getName() == 'EnvType.class'}
}

spotless {
java {
removeUnusedImports()
}
}
version = '1.1.4'
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
package org.fury_phoenix.mixinAp.annotation;

import java.lang.annotation.Annotation;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic;

import net.fabricmc.api.Environment;

import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.embeddedt.modernfix.annotation.IgnoreMixin;
import org.fury_phoenix.mixinAp.util.TypedAccessorMap;
import org.spongepowered.asm.mixin.Mixin;

import static com.google.auto.common.AnnotationMirrors.getAnnotationValue;
import static java.util.AbstractMap.SimpleImmutableEntry;

public class ClientMixinValidator {

private final Messager messager;

private final Elements elemUtils;

private final Types types;

private final boolean debug;

private static final TypedAccessorMap<Annotation> markers = new TypedAccessorMap<>();

private static final Map.Entry<Class<Environment>, Function<? super Environment, ?>>
FabricAccessor = new SimpleImmutableEntry<>(Environment.class, Environment::value);

private static final Map.Entry<
Class<net.minecraftforge.api.distmarker.OnlyIn>,
Function<? super net.minecraftforge.api.distmarker.OnlyIn, ?>>
ForgeAccessor = new SimpleImmutableEntry<>(
net.minecraftforge.api.distmarker.OnlyIn.class,
net.minecraftforge.api.distmarker.OnlyIn::value
);

private static final Map.Entry<
Class<net.neoforged.api.distmarker.OnlyIn>,
Function<? super net.neoforged.api.distmarker.OnlyIn, ?>>
NeoForgeAccessor = new SimpleImmutableEntry<>(
net.neoforged.api.distmarker.OnlyIn.class,
net.neoforged.api.distmarker.OnlyIn::value
);

static {
markers.put(FabricAccessor);
markers.put(ForgeAccessor);
markers.put(NeoForgeAccessor);
}

private static final Collection<String> unannotatedClasses = new HashSet<>();

public ClientMixinValidator(ProcessingEnvironment env) {
debug = Boolean.valueOf(env.getOptions().get("org.fury_phoenix.mixinAp.validator.debug"));
messager = env.getMessager();
elemUtils = env.getElementUtils();
types = env.getTypeUtils();
}

public boolean validateMixin(TypeElement annotatedMixinClass) {
return targetsClient(annotatedMixinClass) &&
(annotatedMixinClass.getAnnotation(ClientOnlyMixin.class) == null);
}

public boolean targetsClient(TypeElement annotatedMixinClass) {
return targetsClient(getTargets(annotatedMixinClass)) &&
!isIgnored(annotatedMixinClass);
}

private boolean targetsClient(Collection<?> classTargets) {
return classTargets.stream().anyMatch(this::targetsClient);
}

private boolean targetsClient(Object classTarget) {
return switch (classTarget) {
case TypeElement te ->
isClientMarked(te);
case TypeMirror tm -> {
var el = types.asElement(tm);
yield el != null ? targetsClient(el) : warn("TypeMirror of " + tm);
}
// If you're using a dollar sign in class names you are insane
case String s -> {
var te =
elemUtils.getTypeElement(toSourceString(s.split("\\$")[0]));
yield te != null ? targetsClient(te) : warn(s);
}
default ->
throw new IllegalArgumentException("Unhandled type: "
+ classTarget.getClass() + "\n" + "Stringified contents: "
+ classTarget.toString());
};
}

private boolean isClientMarked(TypeElement te) {
for (var entry : markers.entrySet()) {
var marker = te.getAnnotation(entry.getKey());
if(marker == null) continue;

return entry.getValue().apply(marker).toString().equals("CLIENT");
}
if(debug && unannotatedClasses.add(te.toString())) {
messager.printMessage(Diagnostic.Kind.WARNING,
"No marker annotations present on " + te + "!");
}
return false;
}

private boolean isIgnored(TypeElement te) {
if(te.getAnnotation(IgnoreMixin.class) != null) {
messager.printMessage(Diagnostic.Kind.WARNING,
toSourceString(te.toString()) + " is ignored!");
return true;
}
return false;
}

private boolean warn(Object o) {
messager.printMessage(Diagnostic.Kind.WARNING,
toSourceString(o.toString()) + " can't be loaded, so it is skipped!");
return false;
}

public Map.Entry<? extends CharSequence, ? extends CharSequence>
getClientMixinEntry(TypeElement annotatedMixinClass) {
return new SimpleImmutableEntry<>(
annotatedMixinClass.getQualifiedName(),
getTargets(annotatedMixinClass)
.stream()
.filter(this::targetsClient)
.map(Object::toString)
.map(ClientMixinValidator::toSourceString)
.collect(Collectors.joining(", "))
);
}

private Collection<Object> getTargets(TypeElement annotatedMixinClass) {
Collection<? extends TypeMirror> clzsses = Set.of();
Collection<? extends String> imaginaries = Set.of();
TypeMirror MixinElement = elemUtils.getTypeElement(Mixin.class.getName()).asType();
for (var mirror : annotatedMixinClass.getAnnotationMirrors()) {
if(!types.isSameType(mirror.getAnnotationType(), MixinElement))
continue;

@SuppressWarnings("unchecked")
var wrappedClzss = (List<? extends AnnotationValue>)
getAnnotationValue(mirror, "value").getValue();

clzsses = wrappedClzss.stream()
.map(AnnotationValue::getValue)
.map(TypeMirror.class::cast)
.collect(Collectors.toSet());

@SuppressWarnings("unchecked")
var wrappedStrings = (List<? extends AnnotationValue>)
getAnnotationValue(mirror, "targets").getValue();

imaginaries = wrappedStrings.stream()
.map(AnnotationValue::getValue)
.map(String.class::cast)
.collect(Collectors.toSet());
}
return Stream.of(clzsses, imaginaries)
.flatMap(Collection::stream)
.collect(Collectors.toSet());
}

public static String toSourceString(String bytecodeName) {
return bytecodeName.replaceAll("\\/", ".");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package org.fury_phoenix.mixinAp.annotation;

import com.google.auto.service.AutoService;
import com.google.common.base.Throwables;

import java.util.List;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedOptions;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic;

import org.fury_phoenix.mixinAp.config.MixinConfig;

@SupportedAnnotationTypes({"org.spongepowered.asm.mixin.Mixin", "org.embeddedt.modernfix.annotation.ClientOnlyMixin"})
@SupportedOptions({"rootProject.name", "project.name", "org.fury_phoenix.mixinAp.validator.debug"})
@SupportedSourceVersion(SourceVersion.RELEASE_17)
@AutoService(Processor.class)
public class MixinProcessor extends AbstractProcessor {

// Remember to call toString when using aliases
private static final Map<String, String> aliases = Map.of(
"Mixin", "mixins",
"ClientOnlyMixin", "client"
);

private final Map<String, List<String>> mixinConfigList = new HashMap<>();

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
try {
if(roundEnv.processingOver()){
filterMixinSets();
// create record for serialization, compute package name
String packageName = Optional.ofNullable(mixinConfigList.get("mixins"))
.orElse(mixinConfigList.get("client"))
.get(0).split("(?<=mixin)")[0];
finalizeMixinConfig();
new MixinConfig(packageName,
mixinConfigList.get("mixins"),
mixinConfigList.get("client")
).generateMixinConfig(processingEnv);
} else {
processMixins(annotations, roundEnv);
}
} catch (Exception e) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Fatal error:" +
Throwables.getStackTraceAsString(e));
throw new RuntimeException(e);
// Halt the AP to prevent nonsense errors
}
return false;
}

private void processMixins(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
for (TypeElement annotation : annotations) {
Set<? extends Element> annotatedMixins = roundEnv.getElementsAnnotatedWith(annotation);

Stream<TypeElement> mixinStream =
annotatedMixins.stream()
.map(TypeElement.class::cast);

validateCommonMixins(annotation, mixinStream);

List<String> mixins =
annotatedMixins.stream()
.map(TypeElement.class::cast)
.map(TypeElement::toString)
.collect(Collectors.toList());

mixinConfigList.putIfAbsent(aliases.get(annotation.getSimpleName().toString()), mixins);
}
}

private void filterMixinSets() {
List<String> commonSet = mixinConfigList.get("mixins");
if(commonSet == null) return;
commonSet.removeAll(mixinConfigList.get("client"));
}

private void validateCommonMixins(TypeElement annotation, Stream<TypeElement> mixins) {
if(!annotation.getSimpleName().toString().equals("Mixin"))
return;
ClientMixinValidator validator = new ClientMixinValidator(processingEnv);
// The implementation may throw a CME
mixins.sequential()
.filter(validator::validateMixin)
.map(validator::getClientMixinEntry)
.forEach(this::logClientClassTarget);
}

private void logClientClassTarget(Map.Entry<? extends CharSequence, ? extends CharSequence> mixin) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
"Mixin " + mixin.getKey() + " targets client-side classes: " + mixin.getValue());
}

private void finalizeMixinConfig() {
// relativize class names
for(var list : mixinConfigList.values()) {
list.replaceAll(className -> className.split("(?<=mixin.)")[1]);
}
}
}
Loading

0 comments on commit 06bfd71

Please sign in to comment.