diff --git a/src/main/java/net/fabricmc/loom/task/ValidateMixinNameTask.java b/src/main/java/net/fabricmc/loom/task/ValidateMixinNameTask.java new file mode 100644 index 00000000..53869b85 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/task/ValidateMixinNameTask.java @@ -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 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 getSoftFailures(); + } + + public abstract static class ValidateMixinAction implements WorkAction { + public static final Logger LOGGER = LoggerFactory.getLogger(ValidateMixinAction.class); + + @Override + public void execute() { + final Set files = getParameters().getInputClasses().getAsFileTree().getFiles(); + final List 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; + } + } + } +}