/* * This file is part of Mixin, licensed under the MIT License (MIT). * * Copyright (c) SpongePowered <https://www.spongepowered.org> * Copyright (c) contributors * * 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 org.spongepowered.asm.mixin.transformer; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.PrintStream; import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashSet; import java.util.Set; import org.apache.commons.io.IOUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.spongepowered.asm.mixin.MixinEnvironment; import org.spongepowered.asm.mixin.MixinEnvironment.Option; import org.spongepowered.asm.mixin.transformer.ClassInfo.Method; import org.spongepowered.asm.mixin.transformer.ClassInfo.SearchType; import org.spongepowered.asm.mixin.transformer.ClassInfo.Traversal; import org.spongepowered.asm.util.PrettyPrinter; import org.spongepowered.asm.util.SignaturePrinter; import com.google.common.base.Charsets; import com.google.common.collect.HashMultimap; import com.google.common.collect.Multimap; import com.google.common.io.Files; /** * Checks whether interfaces declared on mixin target classes are actually fully * implemented and generates reports to the console and to files on disk */ public class MixinTransformerModuleInterfaceChecker implements IMixinTransformerModule { private static final Logger logger = LogManager.getLogger("mixin"); /** * CSV Report file */ private final File csv; /** * Text Report file */ private final File report; /** * Methods from interfaces that are already in the class before mixins are * applied. */ private final Multimap<ClassInfo, Method> interfaceMethods = HashMultimap.create(); public MixinTransformerModuleInterfaceChecker() { File debugOutputFolder = new File(MixinTransformer.DEBUG_OUTPUT, "audit"); debugOutputFolder.mkdirs(); this.csv = new File(debugOutputFolder, "mixin_implementation_report.csv"); this.report = new File(debugOutputFolder, "mixin_implementation_report.txt"); try { Files.write("Class,Method,Signature,Interface\n", this.csv, Charsets.ISO_8859_1); } catch (IOException ex) { // well this sucks } try { String dateTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()); Files.write("Mixin Implementation Report generated on " + dateTime + "\n", this.report, Charsets.ISO_8859_1); } catch (IOException ex) { // hmm :( } } /* (non-Javadoc) * @see org.spongepowered.asm.mixin.transformer.IMixinTransformerModule * #preApply(org.spongepowered.asm.mixin.transformer.TargetClassContext) */ @Override public void preApply(TargetClassContext context) { ClassInfo targetClassInfo = context.getClassInfo(); for (Method m : targetClassInfo.getInterfaceMethods(false)) { this.interfaceMethods.put(targetClassInfo, m); } } /* (non-Javadoc) * @see org.spongepowered.asm.mixin.transformer.IMixinTransformerModule * #postApply(org.spongepowered.asm.mixin.transformer.TargetClassContext) */ @Override public void postApply(TargetClassContext context) { ClassInfo targetClassInfo = context.getClassInfo(); // If the target is abstract and strict mode is not enabled, skip this class if (targetClassInfo.isAbstract() && !MixinEnvironment.getCurrentEnvironment().getOption(Option.CHECK_IMPLEMENTS_STRICT)) { MixinTransformerModuleInterfaceChecker.logger.info("{} is skipping abstract target {}", this.getClass().getSimpleName(), context); return; } String className = targetClassInfo.getName().replace('/', '.'); int missingMethodCount = 0; PrettyPrinter printer = new PrettyPrinter(); printer.add("Class: %s", className).hr(); printer.add("%-32s %-47s %s", "Return Type", "Missing Method", "From Interface").hr(); Set<Method> interfaceMethods = targetClassInfo.getInterfaceMethods(true); Set<Method> implementedMethods = new HashSet<Method>(targetClassInfo.getSuperClass().getInterfaceMethods(true)); implementedMethods.addAll(this.interfaceMethods.removeAll(targetClassInfo)); for (Method method : interfaceMethods) { Method found = targetClassInfo.findMethodInHierarchy(method.getName(), method.getDesc(), SearchType.ALL_CLASSES, Traversal.ALL); // If method IS found and IS implemented, then do nothing (don't print an error) if (found != null && !found.isAbstract()) { continue; } // Don't blame the subclass for not implementing methods that it does not need to implement. if (implementedMethods.contains(method)) { continue; } if (missingMethodCount > 0) { printer.add(); } SignaturePrinter signaturePrinter = new SignaturePrinter(method.getName(), method.getDesc()).setModifiers(""); String iface = method.getOwner().getName().replace('/', '.'); missingMethodCount++; printer.add("%-32s%s", signaturePrinter.getReturnType(), signaturePrinter); printer.add("%-80s %s", "", iface); this.appendToCSVReport(className, method, iface); } if (missingMethodCount > 0) { printer.hr().add("%82s%s: %d", "", "Total unimplemented", missingMethodCount); printer.print(System.err); this.appendToTextReport(printer); } } private void appendToCSVReport(String className, Method method, String iface) { try { Files.append(String.format("%s,%s,%s,%s\n", className, method.getName(), method.getDesc(), iface), this.csv, Charsets.ISO_8859_1); } catch (IOException ex) { // Not the end of the world } } private void appendToTextReport(PrettyPrinter printer) { FileOutputStream fos = null; try { fos = new FileOutputStream(this.report, true); PrintStream stream = new PrintStream(fos); stream.print("\n"); printer.print(stream); } catch (Exception ex) { // never mind } finally { IOUtils.closeQuietly(fos); } } }