Add ValidateMixinNameTask

This commit is contained in:
modmuss50
2022-07-29 20:55:30 +01:00
parent a0187c95e4
commit 37850a5868

View File

@@ -0,0 +1,216 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2022 FabricMC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.fabricmc.loom.task;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import javax.inject.Inject;
import org.gradle.api.GradleException;
import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.SourceTask;
import org.gradle.api.tasks.TaskAction;
import org.gradle.workers.WorkAction;
import org.gradle.workers.WorkParameters;
import org.gradle.workers.WorkQueue;
import org.gradle.workers.WorkerExecutor;
import org.jetbrains.annotations.Nullable;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.Type;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.fabricmc.loom.util.Constants;
import net.fabricmc.tinyremapper.extension.mixin.common.data.Constant;
/**
* task validateMixinNames(type: net.fabricmc.loom.task.ValidateMixinNameTask) {
* source(sourceSets.main.output)
* softFailures = false
* }
*/
public abstract class ValidateMixinNameTask extends SourceTask {
@Input
abstract Property<Boolean> getSoftFailures();
@Inject
protected abstract WorkerExecutor getWorkerExecutor();
@Inject
public ValidateMixinNameTask() {
setGroup("verification");
getProject().getTasks().getByName("check").dependsOn(this);
getSoftFailures().convention(false);
}
@TaskAction
public void run() {
final WorkQueue workQueue = getWorkerExecutor().noIsolation();
workQueue.submit(ValidateMixinAction.class, params -> {
params.getInputClasses().from(getSource().matching(pattern -> pattern.include("**/*.class")));
params.getSoftFailures().set(getSoftFailures());
});
}
public interface ValidateMixinsParams extends WorkParameters {
ConfigurableFileCollection getInputClasses();
Property<Boolean> getSoftFailures();
}
public abstract static class ValidateMixinAction implements WorkAction<ValidateMixinsParams> {
public static final Logger LOGGER = LoggerFactory.getLogger(ValidateMixinAction.class);
@Override
public void execute() {
final Set<File> files = getParameters().getInputClasses().getAsFileTree().getFiles();
final List<String> errors = new LinkedList<>();
for (File file : files) {
final Mixin mixin = getMixin(file);
if (mixin == null) {
continue;
}
final String mixinClassName = toSimpleName(mixin.className);
final String expectedMixinClassName = innerName(toSimpleName(mixin.target.getInternalName())) + "Mixin";
if (expectedMixinClassName.startsWith("class_")) {
// Don't enforce intermediary named mixins.
continue;
}
if (!expectedMixinClassName.equals(mixinClassName)) {
errors.add("%s -> %s".formatted(mixin.className, expectedMixinClassName));
}
}
if (errors.isEmpty()) {
return;
}
final String message = "Mixin name validation failed: " + errors.stream().collect(Collectors.joining(System.lineSeparator()));
if (getParameters().getSoftFailures().get()) {
LOGGER.warn(message);
return;
}
throw new GradleException("Mixin name validation failed: " + errors.stream().collect(Collectors.joining(System.lineSeparator())));
}
private static String toSimpleName(String internalName) {
return internalName.substring(internalName.lastIndexOf("/") + 1);
}
private static String innerName(String simpleName) {
return simpleName.substring(simpleName.lastIndexOf("$") + 1);
}
@Nullable
private Mixin getMixin(File file) {
try (InputStream is = new FileInputStream(file)) {
ClassReader reader = new ClassReader(is);
var classVisitor = new MixinTargetClassVisitor();
reader.accept(classVisitor, ClassReader.SKIP_CODE);
if (classVisitor.mixinTarget != null) {
return new Mixin(classVisitor.className, classVisitor.mixinTarget);
}
} catch (IOException e) {
throw new UncheckedIOException("Failed to read input file: " + file, e);
}
return null;
}
}
private record Mixin(String className, Type target) { }
private static class MixinTargetClassVisitor extends ClassVisitor {
Type mixinTarget;
String className;
protected MixinTargetClassVisitor() {
super(Constants.ASM_VERSION);
}
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
this.className = name;
super.visit(version, access, name, signature, superName, interfaces);
}
@Override
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
AnnotationVisitor av = super.visitAnnotation(descriptor, visible);
if ("Lorg/spongepowered/asm/mixin/Mixin;".equals(descriptor)) {
av = new MixinAnnotationVisitor(av);
}
return av;
}
private class MixinAnnotationVisitor extends AnnotationVisitor {
MixinAnnotationVisitor(AnnotationVisitor annotationVisitor) {
super(Constants.ASM_VERSION, annotationVisitor);
}
@Override
public AnnotationVisitor visitArray(String name) {
final AnnotationVisitor av = super.visitArray(name);
if ("value".equals(name)) {
return new AnnotationVisitor(Constant.ASM_VERSION, av) {
@Override
public void visit(String name, Object value) {
mixinTarget = Objects.requireNonNull((Type) value);
super.visit(name, value);
}
};
}
return av;
}
}
}
}