-
-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Generate mixin configs automatically using custom AP (#305)
- Loading branch information
1 parent
ae8cfba
commit 06bfd71
Showing
27 changed files
with
555 additions
and
108 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' |
189 changes: 189 additions & 0 deletions
189
...ion-processor/src/main/java/org/fury_phoenix/mixinAp/annotation/ClientMixinValidator.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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("\\/", "."); | ||
} | ||
} |
115 changes: 115 additions & 0 deletions
115
annotation-processor/src/main/java/org/fury_phoenix/mixinAp/annotation/MixinProcessor.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]); | ||
} | ||
} | ||
} |
Oops, something went wrong.