/* * 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.IOException; import java.lang.reflect.Constructor; import java.text.DecimalFormat; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; import java.util.UUID; import java.util.regex.Pattern; import org.apache.commons.io.FileUtils; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.spongepowered.asm.lib.tree.ClassNode; import org.spongepowered.asm.mixin.MixinEnvironment; import org.spongepowered.asm.mixin.MixinEnvironment.Option; import org.spongepowered.asm.mixin.MixinEnvironment.Phase; import org.spongepowered.asm.mixin.Mixins; import org.spongepowered.asm.mixin.extensibility.IMixinConfig; import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin; import org.spongepowered.asm.mixin.extensibility.IMixinErrorHandler; import org.spongepowered.asm.mixin.extensibility.IMixinErrorHandler.ErrorAction; import org.spongepowered.asm.mixin.extensibility.IMixinInfo; import org.spongepowered.asm.mixin.throwables.ClassAlreadyLoadedException; import org.spongepowered.asm.mixin.throwables.MixinApplyError; import org.spongepowered.asm.mixin.throwables.MixinException; import org.spongepowered.asm.mixin.throwables.MixinPrepareError; import org.spongepowered.asm.mixin.transformer.MixinConfig.IListener; import org.spongepowered.asm.mixin.transformer.MixinTransformerModuleCheckClass.ValidationFailedException; import org.spongepowered.asm.mixin.transformer.debug.IDecompiler; import org.spongepowered.asm.mixin.transformer.debug.IHotSwap; import org.spongepowered.asm.mixin.transformer.meta.MixinMerged; import org.spongepowered.asm.mixin.transformer.throwables.InvalidMixinException; import org.spongepowered.asm.mixin.transformer.throwables.MixinTransformerError; import org.spongepowered.asm.transformers.TreeTransformer; import org.spongepowered.asm.util.Constants; import org.spongepowered.asm.util.PrettyPrinter; import net.minecraft.launchwrapper.IClassTransformer; import net.minecraft.launchwrapper.Launch; /** * Transformer which manages the mixin configuration and application process */ public class MixinTransformer extends TreeTransformer { /** * Phase during which an error occurred, delegates to functionality in * available handler */ static enum ErrorPhase { /** * Error during initialisation of a MixinConfig */ PREPARE { @Override ErrorAction onError(IMixinErrorHandler handler, String context, InvalidMixinException ex, IMixinInfo mixin, ErrorAction action) { try { return handler.onPrepareError(mixin.getConfig(), ex, mixin, action); } catch (AbstractMethodError ame) { // Catch if error handler is pre-0.5.4 return action; } } @Override protected String getContext(IMixinInfo mixin, String context) { return String.format("preparing %s in %s", mixin.getName(), context); } }, /** * Error during application of a mixin to a target class */ APPLY { @Override ErrorAction onError(IMixinErrorHandler handler, String context, InvalidMixinException ex, IMixinInfo mixin, ErrorAction action) { try { return handler.onApplyError(context, ex, mixin, action); } catch (AbstractMethodError ame) { // Catch if error handler is pre-0.5.4 return action; } } @Override protected String getContext(IMixinInfo mixin, String context) { return String.format("%s -> %s", mixin, context); } }; /** * Human-readable name */ private final String text; private ErrorPhase() { this.text = this.name().toLowerCase(); } abstract ErrorAction onError(IMixinErrorHandler handler, String context, InvalidMixinException ex, IMixinInfo mixin, ErrorAction action); protected abstract String getContext(IMixinInfo mixin, String context); public String getLogMessage(String context, InvalidMixinException ex, IMixinInfo mixin) { return String.format("Mixin %s failed %s: %s %s", this.text, this.getContext(mixin, context), ex.getClass().getName(), ex.getMessage()); } public String getErrorMessage(IMixinInfo mixin, IMixinConfig config, Phase phase) { return String.format("Mixin [%s] from phase [%s] in config [%s] FAILED during %s", mixin, phase, config, this.name()); } } /** * Proxy transformer for the mixin transformer. These transformers are used * to allow the mixin transformer to be re-registered in the transformer * chain at a later stage in startup without having to fully re-initialise * the mixin transformer itself. Only the latest proxy to be instantiated * will actually provide callbacks to the underlying mixin transformer. */ public static class Proxy implements IClassTransformer { /** * All existing proxies */ private static List<Proxy> proxies = new ArrayList<Proxy>(); /** * Actual mixin transformer instance */ private static MixinTransformer transformer = new MixinTransformer(); /** * True if this is the active proxy, newer proxies disable their older * siblings */ private boolean isActive = true; public Proxy() { for (Proxy hook : Proxy.proxies) { hook.isActive = false; } Proxy.proxies.add(this); LogManager.getLogger("mixin").debug("Adding new mixin transformer proxy #{}", Proxy.proxies.size()); } @Override public byte[] transform(String name, String transformedName, byte[] basicClass) { if (this.isActive) { return Proxy.transformer.transform(name, transformedName, basicClass); } return basicClass; } } /** * Re-entrance semaphore used to share re-entrance data with the TreeInfo */ class ReEntranceState { /** * Max valid depth */ private final int maxDepth; /** * Re-entrance depth */ private int depth = 0; /** * Semaphore set when check exceeds a depth of 1 */ private boolean semaphore = false; public ReEntranceState(int maxDepth) { this.maxDepth = maxDepth; } /** * Get max depth */ public int getMaxDepth() { return this.maxDepth; } /** * Get current depth */ public int getDepth() { return this.depth; } /** * Increase the re-entrance depth counter and set the semaphore if depth * exceeds max depth * * @return fluent interface */ ReEntranceState push() { this.depth++; this.checkAndSet(); return this; } /** * Decrease the re-entrance depth * * @return fluent interface */ ReEntranceState pop() { if (this.depth == 0) { throw new IllegalStateException("ReEntranceState pop() with zero depth"); } this.depth--; return this; } /** * Run the depth check but do not set the semaphore * * @return true if depth has exceeded max */ boolean check() { return this.depth > this.maxDepth; } /** * Run the depth check and set the semaphore if depth is exceeded * * @return true if semaphore is set */ boolean checkAndSet() { return this.semaphore |= this.check(); } /** * Set the semaphore * * @return fluent interface */ ReEntranceState set() { this.semaphore = true; return this; } /** * Get whether the semaphore is set */ boolean isSet() { return this.semaphore; } /** * Clear the semaphore * * @return fluent interface */ ReEntranceState clear() { this.semaphore = false; return this; } } /** * Debug exporter */ static class Exporter { /** * Directory to export classes to when debug.export is enabled */ private final File classExportDir = new File(MixinTransformer.DEBUG_OUTPUT, "class"); /** * Runtime decompiler for exported classes */ private final IDecompiler decompiler; Exporter() { this.decompiler = this.initDecompiler(new File(MixinTransformer.DEBUG_OUTPUT, "java")); try { FileUtils.deleteDirectory(this.classExportDir); } catch (IOException ex) { MixinTransformer.logger.warn("Error cleaning class output directory: {}", ex.getMessage()); } } private IDecompiler initDecompiler(File outputPath) { MixinEnvironment env = MixinEnvironment.getCurrentEnvironment(); if (!env.getOption(Option.DEBUG_EXPORT_DECOMPILE)) { return null; } try { boolean as = env.getOption(Option.DEBUG_EXPORT_DECOMPILE_THREADED); MixinTransformer.logger.info("Attempting to load Fernflower decompiler{}", as ? " (Threaded mode)" : ""); String className = "org.spongepowered.asm.mixin.transformer.debug.RuntimeDecompiler" + (as ? "Async" : ""); @SuppressWarnings("unchecked") Class<? extends IDecompiler> clazz = (Class<? extends IDecompiler>)Class.forName(className); Constructor<? extends IDecompiler> ctor = clazz.getDeclaredConstructor(File.class); IDecompiler decompiler = ctor.newInstance(outputPath); MixinTransformer.logger.info("Fernflower decompiler was successfully initialised, exported classes will be decompiled{}", as ? " in a separate thread" : ""); return decompiler; } catch (Throwable th) { MixinTransformer.logger.info("Fernflower could not be loaded, exported classes will not be decompiled. {}: {}", th.getClass().getSimpleName(), th.getMessage()); } return null; } private String prepareFilter(String filter) { filter = "^\\Q" + filter.replace("**", "\201").replace("*", "\202").replace("?", "\203") + "\\E$"; return filter.replace("\201", "\\E.*\\Q").replace("\202", "\\E[^\\.]+\\Q").replace("\203", "\\E.\\Q").replace("\\Q\\E", ""); } private boolean applyFilter(String filter, String subject) { return Pattern.compile(this.prepareFilter(filter), Pattern.CASE_INSENSITIVE).matcher(subject).matches(); } void export(String transformedName, boolean force, byte[] bytes) { // Export transformed class for debugging purposes MixinEnvironment environment = MixinEnvironment.getCurrentEnvironment(); if (force || environment.getOption(Option.DEBUG_EXPORT)) { String filter = environment.getOptionValue(Option.DEBUG_EXPORT_FILTER); if (force || filter == null || this.applyFilter(filter, transformedName)) { File outputFile = this.dumpClass(transformedName.replace('.', '/'), bytes); if (this.decompiler != null) { this.decompiler.decompile(outputFile); } } } } File dumpClass(String fileName, byte[] bytes) { File outputFile = new File(this.classExportDir, fileName + ".class"); try { FileUtils.writeByteArrayToFile(outputFile, bytes); } catch (IOException ex) { // don't care } return outputFile; } } static final File DEBUG_OUTPUT = new File(Constants.DEBUG_OUTPUT_PATH); /** * Log all the things */ static final Logger logger = LogManager.getLogger("mixin"); /** * All mixin configuration bundles */ private final List<MixinConfig> configs = new ArrayList<MixinConfig>(); /** * Uninitialised mixin configuration bundles */ private final List<MixinConfig> pendingConfigs = new ArrayList<MixinConfig>(); /** * Transformer modules */ private final List<IMixinTransformerModule> modules = new ArrayList<IMixinTransformerModule>(); /** * Re-entrance detector */ private final ReEntranceState lock = new ReEntranceState(1); /** * Session ID, used as a check when parsing {@link MixinMerged} annotations * to prevent them being applied at compile time by people trying to * circumvent mixin application */ private final String sessionId = UUID.randomUUID().toString(); /** * Export manager */ private final Exporter exporter; /** * Hot-Swap agent */ private final IHotSwap hotSwapper; /** * Postprocessor for passthrough */ private final MixinPostProcessor postProcessor; /** * Current environment */ private MixinEnvironment currentEnvironment; /** * Logging level for verbose messages */ private Level verboseLoggingLevel = Level.DEBUG; /** * Handling an error state, do not process further mixins */ private boolean errorState = false; /** * Number of classes transformed in the current phase */ private int transformedCount = 0; /** * ctor */ MixinTransformer() { MixinEnvironment environment = MixinEnvironment.getCurrentEnvironment(); Object globalMixinTransformer = environment.getActiveTransformer(); if (globalMixinTransformer instanceof IClassTransformer) { throw new MixinException("Terminating MixinTransformer instance " + this); } // I am a leaf on the wind environment.setActiveTransformer(this); TreeInfo.setLock(this.lock); this.exporter = new Exporter(); this.hotSwapper = this.initHotSwapper(); this.postProcessor = new MixinPostProcessor(); } private IHotSwap initHotSwapper() { if (!MixinEnvironment.getCurrentEnvironment().getOption(Option.HOT_SWAP)) { return null; } try { MixinTransformer.logger.info("Attempting to load Hot-Swap agent"); @SuppressWarnings("unchecked") Class<? extends IHotSwap> clazz = (Class<? extends IHotSwap>)Class.forName("org.spongepowered.tools.agent.MixinAgent"); Constructor<? extends IHotSwap> ctor = clazz.getDeclaredConstructor(MixinTransformer.class); return ctor.newInstance(this); } catch (Throwable th) { MixinTransformer.logger.info("Hot-swap agent could not be loaded, hot swapping of mixins won't work. {}: {}", th.getClass().getSimpleName(), th.getMessage()); } return null; } /** * Force-load all classes targetted by mixins but not yet applied */ public void audit() { Set<String> unhandled = new HashSet<String>(); for (MixinConfig config : this.configs) { unhandled.addAll(config.getUnhandledTargets()); } Logger auditLogger = LogManager.getLogger("mixin/audit"); for (String target : unhandled) { try { auditLogger.info("Force-loading class {}", target); Class.forName(target, true, Launch.classLoader); } catch (ClassNotFoundException ex) { auditLogger.error("Could not force-load " + target, ex); } } for (MixinConfig config : this.configs) { for (String target : config.getUnhandledTargets()) { ClassAlreadyLoadedException ex = new ClassAlreadyLoadedException(target + " was already classloaded"); auditLogger.error("Could not force-load " + target, ex); } } } /* (non-Javadoc) * @see net.minecraft.launchwrapper.IClassTransformer * #transform(java.lang.String, java.lang.String, byte[]) */ @Override public synchronized byte[] transform(String name, String transformedName, byte[] basicClass) { if (basicClass == null || transformedName == null || this.errorState) { return basicClass; } boolean locked = this.lock.push().check(); MixinEnvironment environment = MixinEnvironment.getCurrentEnvironment(); if (!locked) { try { this.checkSelect(environment); } catch (Exception ex) { this.lock.pop(); throw new MixinException(ex); } } try { if (this.postProcessor.canTransform(transformedName)) { byte[] bytes = this.postProcessor.transform(name, transformedName, basicClass); this.exporter.export(transformedName, false, bytes); return bytes; } SortedSet<MixinInfo> mixins = null; boolean invalidRef = false; for (MixinConfig config : this.configs) { if (config.packageMatch(transformedName)) { invalidRef = true; continue; } if (config.hasMixinsFor(transformedName)) { if (mixins == null) { mixins = new TreeSet<MixinInfo>(); } // Get and sort mixins for the class mixins.addAll(config.getMixinsFor(transformedName)); } } if (invalidRef) { throw new NoClassDefFoundError(String.format("%s is a mixin class and cannot be referenced directly", transformedName)); } if (mixins != null) { // Re-entrance is "safe" as long as we don't need to apply any mixins, if there are mixins then we need to panic now if (locked) { MixinTransformer.logger.warn("Re-entrance detected, this will cause serious problems.", new MixinException()); throw new MixinApplyError("Re-entrance error."); } if (this.hotSwapper != null) { this.hotSwapper.registerTargetClass(transformedName, basicClass); } try { // Tree for target class ClassNode targetClassNode = this.readClass(basicClass, true); TargetClassContext context = new TargetClassContext(this.sessionId, transformedName, targetClassNode, mixins); basicClass = this.applyMixins(context); this.transformedCount++; } catch (InvalidMixinException th) { this.dumpClassOnFailure(transformedName, basicClass, environment); this.handleMixinApplyError(transformedName, th, environment); } } return basicClass; } catch (Throwable th) { th.printStackTrace(); this.dumpClassOnFailure(transformedName, basicClass, environment); throw new MixinTransformerError("An unexpected critical error was encountered", th); } finally { this.lock.pop(); } } /** * Update a mixin class with new bytecode. * * @param mixinClass Name of the mixin * @param bytes New bytecode * @return List of classes that need to be updated */ public List<String> reload(String mixinClass, byte[] bytes) { if (this.lock.getDepth() > 0) { throw new MixinApplyError("Cannot reload mixin if re-entrant lock entered"); } List<String> targets = new ArrayList<String>(); for (MixinConfig config : this.configs) { targets.addAll(config.reloadMixin(mixinClass, bytes)); } return targets; } private void checkSelect(MixinEnvironment environment) { if (this.currentEnvironment != environment) { this.select(environment); return; } int unvisitedCount = Mixins.getUnvisitedCount(); if (unvisitedCount > 0 && this.transformedCount == 0) { this.select(environment); } } private void select(MixinEnvironment environment) { this.verboseLoggingLevel = (environment.getOption(Option.DEBUG_VERBOSE)) ? Level.INFO : Level.DEBUG; if (this.transformedCount > 0) { MixinTransformer.logger.log(this.verboseLoggingLevel, "Ending {}, applied {} mixins", this.currentEnvironment, this.transformedCount); } String action = this.currentEnvironment == environment ? "Checking for additional" : "Preparing"; MixinTransformer.logger.log(this.verboseLoggingLevel, "{} mixins for {}", action, environment); long startTime = System.currentTimeMillis(); this.selectConfigs(environment); this.selectModules(environment); int totalMixins = this.prepareConfigs(environment); this.currentEnvironment = environment; this.transformedCount = 0; double elapsedTime = (System.currentTimeMillis() - startTime) * 0.001D; if (elapsedTime > 0.25D) { String elapsed = new DecimalFormat("###0.000").format(elapsedTime); String perMixinTime = new DecimalFormat("###0.0").format((elapsedTime / totalMixins) * 1000.0); MixinTransformer.logger.log(this.verboseLoggingLevel, "Prepared {} mixins in {} sec ({} msec avg.)", totalMixins, elapsed, perMixinTime); } } /** * Add configurations from the supplied mixin environment to the configs set * * @param environment Environment to query */ private void selectConfigs(MixinEnvironment environment) { for (Iterator<Config> iter = Mixins.getConfigs().iterator(); iter.hasNext();) { Config handle = iter.next(); try { MixinConfig config = handle.get(); if (config.select(environment)) { iter.remove(); MixinTransformer.logger.log(this.verboseLoggingLevel, "Selecting config {}", config); config.onSelect(); this.pendingConfigs.add(config); } } catch (Exception ex) { MixinTransformer.logger.warn(String.format("Failed to select mixin config: %s", handle), ex); } } Collections.sort(this.pendingConfigs); } /** * Set up this transformer using options from the supplied environment * * @param environment Environment to query */ private void selectModules(MixinEnvironment environment) { this.modules.clear(); // Run CheckClassAdapter on the mixin bytecode if debug option is enabled if (environment.getOption(Option.DEBUG_VERIFY)) { this.modules.add(new MixinTransformerModuleCheckClass()); } // Run implementation checker if option is enabled if (environment.getOption(Option.CHECK_IMPLEMENTS)) { this.modules.add(new MixinTransformerModuleInterfaceChecker()); } } /** * Prepare mixin configs * * @param environment Environment * @return total number of mixins initialised */ private int prepareConfigs(MixinEnvironment environment) { int totalMixins = 0; final IHotSwap hotSwapper = this.hotSwapper; for (MixinConfig config : this.pendingConfigs) { config.addListener(this.postProcessor); if (hotSwapper != null) { config.addListener(new IListener() { @Override public void onPrepare(MixinInfo mixin) { hotSwapper.registerMixinClass(mixin.getClassName()); } @Override public void onInit(MixinInfo mixin) { } }); } } for (MixinConfig config : this.pendingConfigs) { try { MixinTransformer.logger.log(this.verboseLoggingLevel, "Preparing {} ({})", config, config.getDeclaredMixinCount()); config.prepare(); totalMixins += config.getMixinCount(); } catch (InvalidMixinException ex) { this.handleMixinPrepareError(config, ex, environment); } catch (Exception ex) { String message = ex.getMessage(); MixinTransformer.logger.error("Error encountered whilst initialising mixin config '" + config.getName() + "': " + message, ex); } } for (MixinConfig config : this.pendingConfigs) { IMixinConfigPlugin plugin = config.getPlugin(); if (plugin == null) { continue; } Set<String> otherTargets = new HashSet<String>(); for (MixinConfig otherConfig : this.pendingConfigs) { if (!otherConfig.equals(config)) { otherTargets.addAll(otherConfig.getTargets()); } } plugin.acceptTargets(config.getTargets(), Collections.unmodifiableSet(otherTargets)); } for (MixinConfig config : this.pendingConfigs) { try { config.postInitialise(); } catch (InvalidMixinException ex) { this.handleMixinPrepareError(config, ex, environment); } catch (Exception ex) { String message = ex.getMessage(); MixinTransformer.logger.error("Error encountered during mixin config postInit step'" + config.getName() + "': " + message, ex); } } this.configs.addAll(this.pendingConfigs); Collections.sort(this.configs); this.pendingConfigs.clear(); return totalMixins; } /** * Apply mixins for specified target class to the class described by the * supplied byte array. * * @param context target class context * @return class bytecode after application of mixins */ private byte[] applyMixins(TargetClassContext context) { this.preApply(context); this.apply(context); try { this.postApply(context); } catch (ValidationFailedException ex) { MixinTransformer.logger.info(ex.getMessage()); // If verify is enabled and failed, write out the bytecode to allow us to inspect it if (context.isExportForced() || MixinEnvironment.getCurrentEnvironment().getOption(Option.DEBUG_EXPORT)) { this.writeClass(context); } } return this.writeClass(context); } /** * Process tasks before mixin application * * @param context Target class context */ private void preApply(TargetClassContext context) { for (IMixinTransformerModule module : this.modules) { module.preApply(context); } } /** * Apply the mixins to the target class * * @param context Target class context */ private void apply(TargetClassContext context) { context.applyMixins(); } /** * Process tasks after mixin application * * @param context Target class context */ private void postApply(TargetClassContext context) { for (IMixinTransformerModule module : this.modules) { module.postApply(context); } } private void handleMixinPrepareError(MixinConfig config, InvalidMixinException ex, MixinEnvironment environment) throws MixinPrepareError { this.handleMixinError(config.getName(), ex, environment, ErrorPhase.PREPARE); } private void handleMixinApplyError(String targetClass, InvalidMixinException ex, MixinEnvironment environment) throws MixinApplyError { this.handleMixinError(targetClass, ex, environment, ErrorPhase.APPLY); } private void handleMixinError(String context, InvalidMixinException ex, MixinEnvironment environment, ErrorPhase errorPhase) throws Error { this.errorState = true; IMixinInfo mixin = ex.getMixin(); if (mixin == null) { MixinTransformer.logger.error("InvalidMixinException has no mixin!", ex); throw ex; } IMixinConfig config = mixin.getConfig(); Phase phase = mixin.getPhase(); ErrorAction action = config.isRequired() ? ErrorAction.ERROR : ErrorAction.WARN; if (environment.getOption(Option.DEBUG_VERBOSE)) { new PrettyPrinter() .add("Invalid Mixin").centre() .hr('-') .kvWidth(10) .kv("Action", errorPhase.name()) .kv("Mixin", mixin.getClassName()) .kv("Config", config.getName()) .kv("Phase", phase) .hr('-') .add(" %s", ex.getClass().getName()) .hr('-') .addWrapped(" %s", ex.getMessage()) .hr('-') .add(ex, 8) .trace(action.logLevel); } for (IMixinErrorHandler handler : this.getErrorHandlers(mixin.getPhase())) { ErrorAction newAction = errorPhase.onError(handler, context, ex, mixin, action); if (newAction != null) { action = newAction; } } MixinTransformer.logger.log(action.logLevel, errorPhase.getLogMessage(context, ex, mixin), ex); this.errorState = false; if (action == ErrorAction.ERROR) { throw new MixinApplyError(errorPhase.getErrorMessage(mixin, config, phase), ex); } } private List<IMixinErrorHandler> getErrorHandlers(Phase phase) { List<IMixinErrorHandler> handlers = new ArrayList<IMixinErrorHandler>(); for (String handlerClassName : Mixins.getErrorHandlerClasses()) { try { MixinTransformer.logger.info("Instancing error handler class {}", handlerClassName); Class<?> handlerClass = Class.forName(handlerClassName, true, Launch.classLoader); IMixinErrorHandler handler = (IMixinErrorHandler)handlerClass.newInstance(); if (handler != null) { handlers.add(handler); } } catch (Throwable th) { // skip bad handlers } } return handlers; } private byte[] writeClass(TargetClassContext context) { return this.writeClass(context.getClassName(), context.getClassNode(), context.isExportForced()); } private byte[] writeClass(String transformedName, ClassNode targetClass, boolean forceExport) { // Collapse tree to bytes byte[] bytes = this.writeClass(targetClass); this.exporter.export(transformedName, forceExport, bytes); return bytes; } private void dumpClassOnFailure(String className, byte[] bytes, MixinEnvironment env) { if (env.getOption(Option.DUMP_TARGET_ON_FAILURE)) { this.exporter.dumpClass(className.replace('.', '/') + ".target", bytes); } } }