Emit compile-time errors for mislabeled mixins that targets client side classes

This commit is contained in:
Fury_Phoenix 2023-12-03 11:15:43 -08:00
parent 4645c26227
commit 2660df6db3
No known key found for this signature in database
GPG Key ID: 0595F98084987DB8
4 changed files with 192 additions and 48 deletions

View File

@ -0,0 +1,114 @@
package org.fury_phoenix.mixinAp.annotation;
import java.lang.annotation.Annotation;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;
import javax.tools.Diagnostic;
import net.fabricmc.api.Environment;
import net.fabricmc.api.EnvType;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.util.asm.IAnnotationHandle;
import org.spongepowered.tools.obfuscation.AnnotatedMixinsAccessor;
import org.spongepowered.tools.obfuscation.interfaces.ITypeHandleProvider;
import org.spongepowered.tools.obfuscation.mirror.TypeHandle;
import static java.util.AbstractMap.SimpleImmutableEntry;
public class ClientMixinValidator {
private final ITypeHandleProvider typeHandleProvider;
private final ProcessingEnvironment processingEnv;
public ClientMixinValidator(ProcessingEnvironment env) {
typeHandleProvider = AnnotatedMixinsAccessor.getMixinAP(env);
processingEnv = env;
}
// some sort of javac bug with method reference resolution for mixed staticness
public boolean targetsClient(TypeElement annotatedMixinClass) {
return targetsClient(
ClientMixinValidator.getTargets(
getAnnotationHandle(annotatedMixinClass, Mixin.class)
)) && !getAnnotationHandle(annotatedMixinClass, ClientOnlyMixin.class).exists();
}
private boolean targetsClient(List<?> classTargets) {
return classTargets.stream()
.anyMatch(this::targetsClient);
}
private boolean targetsClient(Object classTarget) {
return switch (classTarget) {
case null -> throw new IllegalArgumentException("Can't be empty!");
case TypeMirror tm ->
EnvType.CLIENT == getEnvType(tm);
// If you're using a dollar sign in class names you are insane
case String s && (getEnvType(s) != null) ->
EnvType.CLIENT == getEnvType(s);
case String s -> warn(s);
default ->
throw new IllegalArgumentException("Unhandled type: " + classTarget.getClass() + "\n"
+ "Stringified contents: " + classTarget.toString());
};
}
private EnvType getEnvType(Object o) {
TypeHandle handle = getTypeHandle(o);
if(handle.isImaginary())
return null;
Environment env = handle.getElement().getAnnotation(Environment.class);
if(env == null)
return null;
return env.value();
}
private boolean warn(String s) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, s + "can't be loaded, so it is skipped!");
return false;
}
public SimpleImmutableEntry<? extends CharSequence, ? extends CharSequence>
getEntry(TypeElement annotatedMixinClass) {
return new SimpleImmutableEntry<>(
annotatedMixinClass.getQualifiedName(),
ClientMixinValidator.getTargets(
getAnnotationHandle(annotatedMixinClass, Mixin.class)
).stream().filter(this::targetsClient)
.map(Object::toString)
.map(ClientMixinValidator::toSourceString)
.collect(Collectors.joining(", "))
);
}
private TypeHandle getTypeHandle(Object annotatedClass) {
return typeHandleProvider.getTypeHandle(annotatedClass);
}
private IAnnotationHandle getAnnotationHandle(Object annotatedClass, Class<? extends Annotation> annotation) {
return getTypeHandle(annotatedClass).getAnnotation(annotation);
}
public static String toSourceString(String bytecodeName) {
return bytecodeName.replaceAll("\\/", ".");
}
private static List<Object> getTargets(IAnnotationHandle mixinAnnotation) {
Collection<? extends TypeMirror> clzss = mixinAnnotation.getList("value");
Collection<? extends String> imaginary = mixinAnnotation.getList("targets");
List<Object> targets =
Stream.of(clzss, imaginary)
.flatMap(Collection::stream)
.collect(Collectors.toList());
return targets;
}
}

View File

@ -1,21 +1,16 @@
package org.fury_phoenix.mixinAp.annotation;
import com.google.auto.service.AutoService;
import com.google.common.base.Throwables;
import com.google.gson.GsonBuilder;
import java.io.IOException;
import java.io.Writer;
import java.util.List;
import java.util.HashMap;
import java.util.Map;
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.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedOptions;
@ -24,7 +19,6 @@ import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic;
import javax.tools.StandardLocation;
import org.fury_phoenix.mixinAp.config.MixinConfig;
@ -40,16 +34,8 @@ public class MixinProcessor extends AbstractProcessor {
"ClientOnlyMixin", "client"
);
private String rootProjectName;
private final Map<String, List<String>> mixinConfigList = new HashMap<>();
@Override
public void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
rootProjectName = processingEnv.getOptions().get("rootProject.name");
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if(roundEnv.processingOver()){
@ -57,12 +43,10 @@ public class MixinProcessor extends AbstractProcessor {
// create record for serialization, compute package name
String packageName = mixinConfigList.get("mixins").get(0).split("(?<=mixin)")[0];
finalizeMixinConfig();
generateMixinConfig(
new MixinConfig(packageName,
mixinConfigList.get("mixins"),
mixinConfigList.get("client")
)
);
new MixinConfig(packageName,
mixinConfigList.get("mixins"),
mixinConfigList.get("client")
).generateMixinConfig(processingEnv);
} else {
processMixins(annotations, roundEnv);
}
@ -73,8 +57,14 @@ public class MixinProcessor extends AbstractProcessor {
for (TypeElement annotation : annotations) {
Set<? extends Element> annotatedMixins = roundEnv.getElementsAnnotatedWith(annotation);
List<String> mixins = annotatedMixins.stream()
.filter(TypeElement.class::isInstance)
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());
@ -84,36 +74,25 @@ public class MixinProcessor extends AbstractProcessor {
}
private void filterMixinSets() {
// set difference of mixins and client
List<String> commonSet = mixinConfigList.get("mixins");
commonSet.removeAll(mixinConfigList.get("client"));
}
private String computeMixinConfigPath() {
var projectName = processingEnv.getOptions().get("project.name");
return "resources/" +
rootProjectName +
(projectName != null ? "-" + projectName : "") +
".mixins.json";
private void validateCommonMixins(TypeElement annotation, Stream<TypeElement> mixins) {
if(!annotation.getSimpleName().toString().equals("Mixin"))
return;
ClientMixinValidator validator = new ClientMixinValidator(processingEnv);
mixins.parallel()
.filter(validator::targetsClient)
.map(validator::getEntry)
.forEach(this::logClientClassTarget);
}
private void generateMixinConfig(Object config) {
try (Writer mixinConfigWriter = processingEnv.getFiler()
.createResource(StandardLocation.SOURCE_OUTPUT, "", computeMixinConfigPath())
.openWriter()) {
String mixinConfig = new GsonBuilder()
.setPrettyPrinting()
.create()
.toJson(config);
mixinConfigWriter.write(mixinConfig);
mixinConfigWriter.write("\n");
} catch (IOException e) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Fatal error:" +
Throwables.getStackTraceAsString(e));
}
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()) {

View File

@ -1,8 +1,18 @@
package org.fury_phoenix.mixinAp.config;
import java.util.List;
import com.google.common.base.Throwables;
import com.google.gson.GsonBuilder;
import com.google.gson.annotations.SerializedName;
import java.io.IOException;
import java.io.Writer;
import java.util.List;
import java.util.Optional;
import javax.annotation.processing.ProcessingEnvironment;
import javax.tools.Diagnostic;
import javax.tools.StandardLocation;
public record MixinConfig(
boolean required,
String minVersion,
@ -26,4 +36,34 @@ public record MixinConfig(
public record OverwriteOptions(boolean conformVisibility) {
public static final OverwriteOptions DEFAULT = new OverwriteOptions(true);
}
}
public void generateMixinConfig(ProcessingEnvironment env) {
try (
Writer mixinConfigWriter = env.getFiler()
.createResource(StandardLocation.SOURCE_OUTPUT, "",
MixinConfig.computeMixinConfigPath(
env.getOptions().get("rootProject.name"),
Optional.ofNullable(env.getOptions().get("project.name"))
)
).openWriter()
) {
String mixinConfig = new GsonBuilder()
.setPrettyPrinting()
.create()
.toJson(this);
mixinConfigWriter.write(mixinConfig);
mixinConfigWriter.write("\n");
} catch (IOException e) {
env.getMessager().printMessage(Diagnostic.Kind.ERROR, "Fatal error:" +
Throwables.getStackTraceAsString(e));
}
}
private static String computeMixinConfigPath(String rootProjectName, Optional<String> projectName) {
return "resources/" +
rootProjectName +
projectName.orElse("") +
".mixins.json";
}
}

View File

@ -0,0 +1,11 @@
package org.spongepowered.tools.obfuscation;
import javax.annotation.processing.ProcessingEnvironment;
import org.spongepowered.tools.obfuscation.interfaces.ITypeHandleProvider;
public class AnnotatedMixinsAccessor {
public static ITypeHandleProvider getMixinAP(ProcessingEnvironment processingEnv) {
return AnnotatedMixins.getMixinsForEnvironment(processingEnv);
}
}